Merge branch 'develop' into 'master'

Redesign and Feature updates

Closes #111, #117, #120, #119, #121, #128, and #116

See merge request b0/spectral!43
This commit is contained in:
Black Hat 2019-03-03 12:18:37 +00:00
commit 0473f25d38
106 changed files with 9434 additions and 2848 deletions

View File

@ -3,13 +3,13 @@ image: Visual Studio 2017
environment: environment:
DEPLOY_DIR: Spectral-%APPVEYOR_BUILD_VERSION% DEPLOY_DIR: Spectral-%APPVEYOR_BUILD_VERSION%
matrix: matrix:
- QTDIR: C:\Qt\5.11\msvc2017_64 - QTDIR: C:\Qt\5.12.1\msvc2017_64
VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat"
PLATFORM: PLATFORM:
init: init:
- call "%QTDIR%\bin\qtenv2.bat" - call "%QTDIR%\bin\qtenv2.bat"
- set PATH=C:\Qt\Tools\QtCreator\bin;%PATH% - set PATH=%PATH%;C:\Qt\Tools\QtCreator\bin
- call "%VCVARS%" %platform% - call "%VCVARS%" %platform%
- cd /D "%APPVEYOR_BUILD_FOLDER%" - cd /D "%APPVEYOR_BUILD_FOLDER%"
@ -17,12 +17,12 @@ before_build:
- git submodule update --init --recursive - git submodule update --init --recursive
build_script: build_script:
- qmake spectral.pro CONFIG+=debug CONFIG+=qml_debug PREFIX="%DEPLOY_DIR%" - qmake spectral.pro CONFIG+=release CONFIG+=qtquickcompiler PREFIX="%DEPLOY_DIR%"
- nmake - nmake
after_build: after_build:
- nmake install - nmake install
- windeployqt --debug --qmldir qml --qmldir imports "%DEPLOY_DIR%\spectral.exe" - windeployqt --release --qmldir qml --qmldir imports "%DEPLOY_DIR%\spectral.exe"
- 7z a spectral.zip "%DEPLOY_DIR%\" - 7z a spectral.zip "%DEPLOY_DIR%\"
artifacts: artifacts:

View File

@ -22,11 +22,11 @@ build-appimage:
before_script: before_script:
- git submodule update --init --recursive - git submodule update --init --recursive
script: script:
- /opt/qt511/bin/qt511-env.sh - /opt/qt512/bin/qt512-env.sh
- /opt/qt511/bin/qmake CONFIG+=debug CONFIG+=qml_debug PREFIX=/usr - /opt/qt512/bin/qmake CONFIG+=debug CONFIG+=qml_debug PREFIX=/usr
- make - make
- make INSTALL_ROOT=appdir install - make INSTALL_ROOT=appdir install
- /usr/bin/linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/org.eu.encom.spectral.desktop -appimage -qmldir=qml -qmldir=imports -qmake=/opt/qt511/bin/qmake - /usr/bin/linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/org.eu.encom.spectral.desktop -appimage -qmldir=qml -qmldir=imports -qmake=/opt/qt512/bin/qmake
artifacts: artifacts:
paths: paths:
- Spectral-x86_64.AppImage - Spectral*.AppImage

BIN
assets/font/roboto.ttf Normal file

Binary file not shown.

BIN
assets/font/twemoji.ttf Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 697 KiB

29
assets/img/matrix.svg Normal file
View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14576) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="793.322px" height="340.809px" viewBox="0 0 793.322 340.809" enable-background="new 0 0 793.322 340.809" xml:space="preserve">
<path opacity="0.5" fill="#FFFFFF" d="M34.004,340.809H2c-1.104,0-2-0.896-2-2V2c0-1.104,0.896-2,2-2h32.004c1.104,0,2,0.896,2,2 v7.71c0,1.104-0.896,2-2,2h-21.13v317.386h21.13c1.104,0,2,0.896,2,2.001v7.712C36.004,339.913,35.108,340.809,34.004,340.809 L34.004,340.809z"/>
<path opacity="0.5" fill="#FFFFFF" d="M10.875,9.711v321.386h23.13v7.711H1.999V2.001h32.006v7.71H10.875z"/>
<path opacity="0.5" fill="#FFFFFF" d="M252.402,233.711h-32.993c-1.104,0-2-0.896-2-2v-68.073c0-3.949-0.154-7.722-0.457-11.213 c-0.289-3.282-1.074-6.153-2.332-8.53c-1.204-2.276-3.017-4.119-5.384-5.476c-2.393-1.362-5.775-2.056-10.042-2.056 c-4.238,0-7.674,0.798-10.213,2.371c-2.565,1.596-4.604,3.701-6.053,6.258c-1.498,2.643-2.51,5.694-3.013,9.067 c-0.526,3.513-0.793,7.125-0.793,10.741v66.91c0,1.104-0.896,2-2,2h-32.991c-1.104,0-2-0.896-2-2v-67.373 c0-3.435-0.078-6.964-0.228-10.485c-0.148-3.251-0.767-6.278-1.841-8.995c-1.018-2.571-2.667-4.584-5.047-6.153 c-2.372-1.552-6.029-2.341-10.865-2.341c-1.372,0-3.265,0.328-5.629,0.976c-2.28,0.624-4.536,1.826-6.705,3.577 c-2.152,1.732-4.036,4.306-5.605,7.655c-1.569,3.356-2.367,7.877-2.367,13.438v69.701c0,1.104-0.895,2-2,2H68.857 c-1.104,0-2-0.896-2-2V111.594c0-1.104,0.896-1.999,2-1.999h31.13c1.104,0,2,0.896,2,1.999v11.007 c3.834-4.499,8.248-8.152,13.173-10.896c6.396-3.559,13.799-5.362,22.002-5.362c7.846,0,15.127,1.548,21.642,4.604 c5.794,2.722,10.424,7.26,13.791,13.52c3.449-4.362,7.833-8.306,13.071-11.752c6.422-4.228,14.102-6.371,22.824-6.371 c6.499,0,12.625,0.807,18.209,2.399c5.686,1.628,10.635,4.271,14.712,7.857c4.088,3.605,7.318,8.357,9.601,14.123 c2.25,5.719,3.391,12.649,3.391,20.604v80.384C254.402,232.815,253.507,233.711,252.402,233.711L252.402,233.711z"/>
<path opacity="0.5" fill="#FFFFFF" d="M99.988,111.595v16.264h0.463c4.338-6.191,9.563-10.998,15.684-14.406 c6.117-3.402,13.129-5.11,21.027-5.11c7.588,0,14.521,1.475,20.793,4.415c6.274,2.945,11.038,8.131,14.291,15.567 c3.56-5.265,8.4-9.913,14.521-13.94c6.117-4.025,13.358-6.042,21.724-6.042c6.351,0,12.234,0.776,17.66,2.325 c5.418,1.549,10.065,4.027,13.938,7.434c3.869,3.41,6.889,7.863,9.062,13.357c2.167,5.504,3.253,12.122,3.253,19.869v80.385H219.41 v-68.074c0-4.025-0.154-7.82-0.465-11.385c-0.313-3.56-1.161-6.656-2.555-9.293c-1.395-2.631-3.45-4.724-6.157-6.274 c-2.711-1.543-6.391-2.322-11.037-2.322s-8.403,0.896-11.269,2.671c-2.868,1.784-5.112,4.109-6.737,6.971 c-1.626,2.869-2.711,6.12-3.252,9.762c-0.545,3.638-0.814,7.318-0.814,11.035v66.91h-32.991v-67.375c0-3.562-0.081-7.087-0.23-10.57 c-0.158-3.487-0.814-6.7-1.978-9.645c-1.162-2.94-3.099-5.304-5.809-7.088c-2.711-1.775-6.699-2.671-11.965-2.671 c-1.551,0-3.603,0.349-6.156,1.048c-2.556,0.697-5.036,2.016-7.435,3.949c-2.404,1.938-4.454,4.726-6.158,8.363 c-1.705,3.642-2.556,8.402-2.556,14.287v69.701h-32.99V111.595H99.988z"/>
<path opacity="0.5" fill="#FFFFFF" d="M304.909,236.733c-5.883,0-11.46-0.729-16.574-2.163c-5.192-1.464-9.806-3.774-13.713-6.871 c-3.944-3.117-7.068-7.111-9.282-11.871c-2.205-4.733-3.324-10.412-3.324-16.876c0-7.13,1.293-13.117,3.846-17.797 c2.542-4.674,5.877-8.464,9.912-11.263c3.97-2.752,8.556-4.842,13.63-6.209c4.901-1.322,9.937-2.394,14.961-3.184 c4.986-0.775,9.949-1.404,14.754-1.872c4.679-0.452,8.88-1.139,12.489-2.039c3.412-0.854,6.118-2.09,8.042-3.672 c1.666-1.37,2.416-3.384,2.292-6.151c-0.002-3.289-0.502-5.816-1.492-7.595c-0.998-1.798-2.283-3.15-3.927-4.138 c-1.703-1.02-3.725-1.713-6.012-2.062c-2.47-0.37-5.146-0.557-7.947-0.557c-6.034,0-10.789,1.271-14.135,3.783 c-3.233,2.424-5.155,6.64-5.714,12.527c-0.098,1.026-0.961,1.812-1.992,1.812h-32.992c-0.552,0-1.079-0.229-1.457-0.629 c-0.376-0.402-0.572-0.941-0.54-1.491c0.485-8.073,2.55-14.894,6.142-20.272c3.548-5.331,8.147-9.682,13.661-12.931 c5.424-3.191,11.612-5.498,18.392-6.857c6.684-1.335,13.5-2.013,20.26-2.013c6.096,0,12.365,0.437,18.626,1.296 c6.377,0.88,12.285,2.622,17.562,5.177c5.376,2.604,9.845,6.29,13.282,10.951c3.498,4.744,5.271,11.048,5.271,18.731v62.494 c0,5.307,0.306,10.462,0.915,15.319c0.576,4.64,1.572,8.116,2.963,10.338c0.385,0.616,0.407,1.395,0.055,2.031 c-0.353,0.635-1.022,1.03-1.75,1.03h-33.457c-0.861,0-1.624-0.55-1.898-1.367c-0.646-1.941-1.176-3.939-1.572-5.936 c-0.141-0.696-0.267-1.402-0.38-2.12c-4.825,4.184-10.349,7.24-16.474,9.105C320.033,235.609,312.489,236.733,304.909,236.733 L304.909,236.733z M341.941,176.661c-0.809,0.409-1.676,0.768-2.596,1.074c-2.161,0.72-4.511,1.326-6.988,1.807 c-2.442,0.475-5.033,0.872-7.699,1.186c-2.631,0.311-5.251,0.697-7.784,1.146c-2.329,0.433-4.705,1.035-7.051,1.792 c-2.194,0.711-4.114,1.667-5.699,2.842c-1.531,1.128-2.785,2.587-3.731,4.335c-0.917,1.709-1.385,3.97-1.385,6.719 c0,2.598,0.465,4.778,1.385,6.481c0.928,1.722,2.142,3.035,3.716,4.018c1.644,1.026,3.601,1.757,5.816,2.17 c2.344,0.439,4.799,0.663,7.297,0.663c6.105,0,10.836-0.996,14.063-2.961c3.244-1.973,5.666-4.349,7.199-7.062 c1.568-2.78,2.542-5.62,2.892-8.436c0.376-3.019,0.565-5.436,0.565-7.187V176.661L341.941,176.661z"/>
<path opacity="0.5" fill="#FFFFFF" d="M273.544,129.255c3.405-5.113,7.744-9.215,13.012-12.316 c5.264-3.097,11.186-5.303,17.771-6.621c6.582-1.315,13.205-1.976,19.865-1.976c6.042,0,12.158,0.428,18.354,1.277 c6.195,0.855,11.85,2.522,16.962,4.997c5.111,2.477,9.292,5.926,12.546,10.338c3.253,4.414,4.879,10.262,4.879,17.543v62.494 c0,5.428,0.31,10.611,0.931,15.567c0.615,4.959,1.701,8.676,3.251,11.153H347.66c-0.621-1.86-1.126-3.755-1.511-5.693 c-0.39-1.933-0.661-3.908-0.813-5.923c-5.267,5.422-11.465,9.217-18.585,11.386c-7.127,2.163-14.407,3.251-21.842,3.251 c-5.733,0-11.077-0.698-16.033-2.09c-4.958-1.395-9.293-3.562-13.01-6.51c-3.718-2.938-6.622-6.656-8.713-11.147 s-3.138-9.84-3.138-16.033c0-6.813,1.199-12.43,3.604-16.84c2.399-4.417,5.495-7.939,9.295-10.575 c3.793-2.632,8.129-4.607,13.01-5.923c4.878-1.315,9.795-2.358,14.752-3.137c4.957-0.772,9.835-1.393,14.638-1.857 c4.801-0.466,9.062-1.164,12.779-2.093c3.718-0.929,6.658-2.282,8.829-4.065c2.165-1.781,3.172-4.375,3.02-7.785 c0-3.56-0.58-6.389-1.742-8.479c-1.161-2.09-2.711-3.719-4.646-4.88c-1.937-1.161-4.183-1.936-6.737-2.325 c-2.557-0.382-5.309-0.58-8.248-0.58c-6.506,0-11.617,1.395-15.335,4.183c-3.716,2.788-5.889,7.437-6.506,13.94h-32.991 C268.199,140.794,270.132,134.363,273.544,129.255z M338.713,175.838c-2.09,0.696-4.337,1.275-6.736,1.741 c-2.402,0.465-4.918,0.853-7.551,1.161c-2.635,0.313-5.268,0.698-7.899,1.163c-2.48,0.461-4.919,1.086-7.317,1.857 c-2.404,0.779-4.495,1.822-6.274,3.138c-1.784,1.317-3.216,2.985-4.3,4.994c-1.085,2.014-1.626,4.571-1.626,7.668 c0,2.94,0.541,5.422,1.626,7.431c1.084,2.017,2.558,3.604,4.416,4.765s4.025,1.976,6.507,2.438c2.475,0.466,5.031,0.698,7.665,0.698 c6.505,0,11.537-1.082,15.103-3.253c3.561-2.166,6.192-4.762,7.899-7.785c1.702-3.019,2.749-6.072,3.137-9.174 c0.384-3.097,0.58-5.576,0.58-7.434v-12.316C342.547,174.173,340.805,175.14,338.713,175.838z"/>
<path opacity="0.5" fill="#FFFFFF" d="M444.542,234.874c-5.187,0-10.173-0.361-14.823-1.069c-4.802-0.732-9.104-2.183-12.779-4.313 c-3.789-2.185-6.821-5.341-9.006-9.375c-2.163-3.986-3.26-9.232-3.26-15.59v-68.859h-17.981c-1.104,0-2-0.896-2-1.999v-22.073 c0-1.104,0.896-1.999,2-1.999h17.981V75.582c0-1.104,0.896-2,2-2h32.992c1.104,0,2,0.896,2,2v34.014h22.162c1.104,0,2,0.896,2,1.999 v22.073c0,1.104-0.896,1.999-2,1.999h-22.162v57.479c0,6.229,1.198,8.731,2.202,9.733c1.004,1.007,3.506,2.205,9.738,2.205 c1.804,0,3.542-0.076,5.161-0.225c1.604-0.144,3.174-0.367,4.669-0.665c0.13-0.026,0.261-0.039,0.391-0.039 c0.458,0,0.907,0.159,1.27,0.454c0.463,0.379,0.73,0.946,0.73,1.546v25.555c0,0.979-0.707,1.813-1.672,1.974 c-2.834,0.472-6.041,0.794-9.527,0.957C451.015,234.798,447.718,234.874,444.542,234.874L444.542,234.874z"/>
<path opacity="0.5" fill="#FFFFFF" d="M463.825,111.595v22.072h-24.161v59.479c0,5.573,0.928,9.292,2.788,11.149 c1.856,1.859,5.576,2.788,11.152,2.788c1.859,0,3.638-0.076,5.343-0.232c1.703-0.152,3.33-0.388,4.878-0.696v25.557 c-2.788,0.465-5.887,0.773-9.293,0.931c-3.407,0.149-6.737,0.23-9.99,0.23c-5.111,0-9.953-0.35-14.521-1.048 c-4.571-0.695-8.597-2.047-12.081-4.063c-3.486-2.011-6.236-4.88-8.248-8.597c-2.016-3.714-3.021-8.595-3.021-14.639v-70.859h-19.98 v-22.072h19.98V75.583h32.992v36.012H463.825z"/>
<path opacity="0.5" fill="#FFFFFF" d="M512.613,233.711h-32.991c-1.104,0-2-0.896-2-2V111.594c0-1.104,0.896-1.999,2-1.999h31.366 c1.104,0,2,0.896,2,1.999v15.069c0.967-1.516,2.034-2.978,3.199-4.382c2.754-3.312,5.949-6.182,9.496-8.522 c3.545-2.332,7.385-4.169,11.415-5.462c4.056-1.298,8.327-1.954,12.691-1.954c2.341,0,4.953,0.418,7.766,1.243 c0.852,0.25,1.437,1.032,1.437,1.92v30.67c0,0.6-0.269,1.167-0.732,1.547c-0.361,0.296-0.808,0.452-1.265,0.452 c-0.133,0-0.265-0.013-0.398-0.039c-1.484-0.3-3.299-0.565-5.392-0.787c-2.098-0.224-4.136-0.339-6.062-0.339 c-5.706,0-10.572,0.95-14.467,2.823c-3.862,1.86-7.012,4.428-9.361,7.629c-2.389,3.263-4.115,7.12-5.127,11.47 c-1.043,4.479-1.574,9.409-1.574,14.647v54.132C514.613,232.815,513.717,233.711,512.613,233.711L512.613,233.711z"/>
<path opacity="0.5" fill="#FFFFFF" d="M510.988,111.595V133.9h0.465c1.546-3.72,3.636-7.163,6.272-10.341 c2.634-3.172,5.652-5.885,9.06-8.131c3.405-2.242,7.047-3.985,10.923-5.228c3.868-1.237,7.898-1.859,12.081-1.859 c2.168,0,4.566,0.39,7.202,1.163v30.67c-1.551-0.312-3.41-0.584-5.576-0.814c-2.17-0.233-4.26-0.35-6.274-0.35 c-6.041,0-11.152,1.01-15.332,3.021c-4.182,2.014-7.55,4.761-10.107,8.247c-2.555,3.487-4.379,7.55-5.462,12.198 c-1.083,4.645-1.625,9.682-1.625,15.102v54.133h-32.991V111.595H510.988z"/>
<path opacity="0.5" fill="#FFFFFF" d="M603.923,233.711H570.93c-1.104,0-2-0.896-2-2V111.594c0-1.104,0.896-1.999,2-1.999h32.994 c1.104,0,2,0.896,2,1.999v120.117C605.923,232.815,605.027,233.711,603.923,233.711L603.923,233.711z M603.923,95.006H570.93 c-1.104,0-2-0.896-2-1.999V65.825c0-1.104,0.896-2,2-2h32.994c1.104,0,2,0.896,2,2v27.182 C605.923,94.11,605.027,95.006,603.923,95.006L603.923,95.006z"/>
<path opacity="0.5" fill="#FFFFFF" d="M570.93,93.007V65.824h32.994v27.183H570.93z M603.924,111.595v120.117H570.93V111.595 H603.924z"/>
<path opacity="0.5" fill="#FFFFFF" d="M742.163,233.711h-37.64c-0.671,0-1.297-0.335-1.667-0.896l-23.426-35.352l-23.426,35.352 c-0.369,0.561-0.995,0.896-1.667,0.896h-36.938c-0.741,0-1.424-0.411-1.77-1.067c-0.345-0.654-0.3-1.449,0.118-2.061l42.435-62.055 l-38.71-55.793c-0.424-0.613-0.474-1.408-0.128-2.069c0.343-0.658,1.028-1.071,1.771-1.071h37.636c0.665,0,1.287,0.33,1.658,0.882 l19.477,28.893l19.255-28.884c0.372-0.556,0.996-0.891,1.665-0.891h36.475c0.746,0,1.43,0.415,1.776,1.078 c0.343,0.66,0.289,1.46-0.139,2.071l-38.69,55.082l43.578,62.744c0.424,0.61,0.474,1.408,0.128,2.066 C743.591,233.298,742.908,233.711,742.163,233.711L742.163,233.711z"/>
<path opacity="0.5" fill="#FFFFFF" d="M621.115,111.595h37.637l21.144,31.365l20.911-31.365h36.476l-39.496,56.226l44.377,63.892 h-37.64l-25.093-37.87l-25.094,37.87h-36.938l43.213-63.193L621.115,111.595z"/>
<path opacity="0.5" fill="#FFFFFF" d="M791.322,340.809h-32.008c-1.105,0-2-0.896-2-2v-7.712c0-1.105,0.896-2.001,2-2.001h21.13 V11.71h-21.13c-1.105,0-2-0.896-2-2V2c0-1.104,0.896-2,2-2h32.008c1.104,0,2,0.896,2,2v336.809 C793.322,339.913,792.426,340.809,791.322,340.809L791.322,340.809z"/>
<path opacity="0.5" fill="#FFFFFF" d="M782.443,331.097V9.711h-23.13v-7.71h32.008v336.807h-32.008v-7.711H782.443z"/>
<path d="M10.875,9.711v321.386h23.13v7.711H1.999V2.001h32.006v7.71H10.875z"/>
<path d="M99.988,111.595v16.264h0.463c4.338-6.191,9.563-10.998,15.684-14.406c6.117-3.402,13.129-5.11,21.027-5.11 c7.588,0,14.521,1.475,20.793,4.415c6.274,2.945,11.038,8.131,14.291,15.567c3.56-5.265,8.4-9.913,14.521-13.94 c6.117-4.025,13.358-6.042,21.724-6.042c6.351,0,12.234,0.776,17.66,2.325c5.418,1.549,10.065,4.027,13.938,7.434 c3.869,3.41,6.889,7.863,9.062,13.357c2.167,5.504,3.253,12.122,3.253,19.869v80.385H219.41v-68.074 c0-4.025-0.154-7.82-0.465-11.385c-0.313-3.56-1.161-6.656-2.555-9.293c-1.395-2.631-3.45-4.724-6.157-6.274 c-2.711-1.543-6.391-2.322-11.037-2.322s-8.403,0.896-11.269,2.671c-2.868,1.784-5.112,4.109-6.737,6.971 c-1.626,2.869-2.711,6.12-3.252,9.762c-0.545,3.638-0.814,7.318-0.814,11.035v66.91h-32.991v-67.375c0-3.562-0.081-7.087-0.23-10.57 c-0.158-3.487-0.814-6.7-1.978-9.645c-1.162-2.94-3.099-5.304-5.809-7.088c-2.711-1.775-6.699-2.671-11.965-2.671 c-1.551,0-3.603,0.349-6.156,1.048c-2.556,0.697-5.036,2.016-7.435,3.949c-2.404,1.938-4.454,4.726-6.158,8.363 c-1.705,3.642-2.556,8.402-2.556,14.287v69.701h-32.99V111.595H99.988z"/>
<path d="M273.544,129.255c3.405-5.113,7.744-9.215,13.012-12.316c5.264-3.097,11.186-5.303,17.771-6.621 c6.582-1.315,13.205-1.976,19.865-1.976c6.042,0,12.158,0.428,18.354,1.277c6.195,0.855,11.85,2.522,16.962,4.997 c5.111,2.477,9.292,5.926,12.546,10.338c3.253,4.414,4.879,10.262,4.879,17.543v62.494c0,5.428,0.31,10.611,0.931,15.567 c0.615,4.959,1.701,8.676,3.251,11.153H347.66c-0.621-1.86-1.126-3.755-1.511-5.693c-0.39-1.933-0.661-3.908-0.813-5.923 c-5.267,5.422-11.465,9.217-18.585,11.386c-7.127,2.163-14.407,3.251-21.842,3.251c-5.733,0-11.077-0.698-16.033-2.09 c-4.958-1.395-9.293-3.562-13.01-6.51c-3.718-2.938-6.622-6.656-8.713-11.147s-3.138-9.84-3.138-16.033 c0-6.813,1.199-12.43,3.604-16.84c2.399-4.417,5.495-7.939,9.295-10.575c3.793-2.632,8.129-4.607,13.01-5.923 c4.878-1.315,9.795-2.358,14.752-3.137c4.957-0.772,9.835-1.393,14.638-1.857c4.801-0.466,9.062-1.164,12.779-2.093 c3.718-0.929,6.658-2.282,8.829-4.065c2.165-1.781,3.172-4.375,3.02-7.785c0-3.56-0.58-6.389-1.742-8.479 c-1.161-2.09-2.711-3.719-4.646-4.88c-1.937-1.161-4.183-1.936-6.737-2.325c-2.557-0.382-5.309-0.58-8.248-0.58 c-6.506,0-11.617,1.395-15.335,4.183c-3.716,2.788-5.889,7.437-6.506,13.94h-32.991 C268.199,140.794,270.132,134.363,273.544,129.255z M338.713,175.838c-2.09,0.696-4.337,1.275-6.736,1.741 c-2.402,0.465-4.918,0.853-7.551,1.161c-2.635,0.313-5.268,0.698-7.899,1.163c-2.48,0.461-4.919,1.086-7.317,1.857 c-2.404,0.779-4.495,1.822-6.274,3.138c-1.784,1.317-3.216,2.985-4.3,4.994c-1.085,2.014-1.626,4.571-1.626,7.668 c0,2.94,0.541,5.422,1.626,7.431c1.084,2.017,2.558,3.604,4.416,4.765s4.025,1.976,6.507,2.438c2.475,0.466,5.031,0.698,7.665,0.698 c6.505,0,11.537-1.082,15.103-3.253c3.561-2.166,6.192-4.762,7.899-7.785c1.702-3.019,2.749-6.072,3.137-9.174 c0.384-3.097,0.58-5.576,0.58-7.434v-12.316C342.547,174.173,340.805,175.14,338.713,175.838z"/>
<path d="M463.825,111.595v22.072h-24.161v59.479c0,5.573,0.928,9.292,2.788,11.149c1.856,1.859,5.576,2.788,11.152,2.788 c1.859,0,3.638-0.076,5.343-0.232c1.703-0.152,3.33-0.388,4.878-0.696v25.557c-2.788,0.465-5.887,0.773-9.293,0.931 c-3.407,0.149-6.737,0.23-9.99,0.23c-5.111,0-9.953-0.35-14.521-1.048c-4.571-0.695-8.597-2.047-12.081-4.063 c-3.486-2.011-6.236-4.88-8.248-8.597c-2.016-3.714-3.021-8.595-3.021-14.639v-70.859h-19.98v-22.072h19.98V75.583h32.992v36.012 H463.825z"/>
<path d="M510.988,111.595V133.9h0.465c1.546-3.72,3.636-7.163,6.272-10.341c2.634-3.172,5.652-5.885,9.06-8.131 c3.405-2.242,7.047-3.985,10.923-5.228c3.868-1.237,7.898-1.859,12.081-1.859c2.168,0,4.566,0.39,7.202,1.163v30.67 c-1.551-0.312-3.41-0.584-5.576-0.814c-2.17-0.233-4.26-0.35-6.274-0.35c-6.041,0-11.152,1.01-15.332,3.021 c-4.182,2.014-7.55,4.761-10.107,8.247c-2.555,3.487-4.379,7.55-5.462,12.198c-1.083,4.645-1.625,9.682-1.625,15.102v54.133h-32.991 V111.595H510.988z"/>
<path d="M570.93,93.007V65.824h32.994v27.183H570.93z M603.924,111.595v120.117H570.93V111.595H603.924z"/>
<path d="M621.115,111.595h37.637l21.144,31.365l20.911-31.365h36.476l-39.496,56.226l44.377,63.892h-37.64l-25.093-37.87 l-25.094,37.87h-36.938l43.213-63.193L621.115,111.595z"/>
<path d="M782.443,331.097V9.711h-23.13v-7.71h32.008v336.807h-32.008v-7.711H782.443z"/>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,219 @@
<?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>

After

Width:  |  Height:  |  Size: 22 KiB

219
assets/img/roompanel.svg Normal file
View File

@ -0,0 +1,219 @@
<?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>

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,6 +1,6 @@
id: org.eu.encom.spectral id: org.eu.encom.spectral
runtime: org.kde.Platform runtime: org.kde.Platform
runtime-version: 5.11 runtime-version: 5.12
sdk: org.kde.Sdk sdk: org.kde.Sdk
command: spectral command: spectral
finish-args: finish-args:
@ -15,6 +15,8 @@ finish-args:
modules: modules:
- name: spectral - name: spectral
buildsystem: qmake buildsystem: qmake
config-opts:
- "BUNDLE_FONT=true"
sources: sources:
- type: dir - type: dir
path: ../ path: ../

6
font.qrc Normal file
View File

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

View File

@ -1,4 +1,4 @@
import QtQuick 2.9 import QtQuick 2.12
ListView { ListView {
ScrollHelper { ScrollHelper {

View File

@ -1,4 +1,4 @@
import QtQuick 2.9 import QtQuick 2.12
import Spectral.Setting 0.1 import Spectral.Setting 0.1

View File

@ -1,5 +1,5 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
TextField { TextField {
selectByMouse: true selectByMouse: true

View File

@ -0,0 +1,64 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtGraphicalEffects 1.0
Item {
property string hint: "H"
property string source: ""
readonly property url realSource: source ? "image://mxc/" + source : ""
id: root
Image {
anchors.fill: parent
id: image
visible: realSource
source: width < 1 ? "" : realSource
sourceSize.width: width
sourceSize.height: width
fillMode: Image.PreserveAspectCrop
mipmap: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: image.width
height: image.width
radius: width / 2
}
}
}
Rectangle {
anchors.fill: parent
visible: !realSource || image.status != Image.Ready
radius: height / 2
color: stringToColor(hint)
Label {
anchors.centerIn: parent
color: "white"
text: hint[0].toUpperCase()
font.pixelSize: root.width / 2
font.bold: true
}
}
function stringToColor(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
var colour = '#';
for (var j = 0; j < 3; j++) {
var value = (hash >> (j * 8)) & 0xFF;
colour += ('00' + value.toString(16)).substr(-2);
}
return colour;
}
}

View File

@ -1,63 +1,32 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.12
import Spectral.Component 2.0
import Spectral 0.1 import Spectral 0.1
import Spectral.Setting 0.1
Popup {
property var emojiModel
property var textArea
property string emojiCategory: "people"
ColumnLayout { ColumnLayout {
anchors.fill: parent property string emojiCategory: "people"
property var textArea
property var emojiModel
GridView { spacing: 0
ListView {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.preferredHeight: 48
Layout.leftMargin: 24
cellWidth: 36 Layout.rightMargin: 24
cellHeight: 36
boundsBehavior: Flickable.DragOverBounds boundsBehavior: Flickable.DragOverBounds
clip: true clip: true
model: emojiModel.model[emojiCategory] orientation: ListView.Horizontal
delegate: ItemDelegate {
width: 36
height: 36
contentItem: Text {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pointSize: 20
font.family: "Emoji"
text: modelData.unicode
}
hoverEnabled: true
ToolTip.text: modelData.shortname
ToolTip.visible: hovered
onClicked: textArea.insert(textArea.cursorPosition, modelData.unicode)
}
ScrollBar.vertical: ScrollBar {}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 2
color: Material.accent
}
Row {
Repeater {
model: ListModel { model: ListModel {
ListElement { label: "😏"; category: "people" } ListElement { label: "😏"; category: "people" }
ListElement { label: "🌲"; category: "nature" } ListElement { label: "🌲"; category: "nature" }
@ -70,25 +39,69 @@ Popup {
} }
delegate: ItemDelegate { delegate: ItemDelegate {
width: 36 width: 64
height: 36 height: 48
contentItem: Text { contentItem: Label {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
font.pointSize: 20 font.pixelSize: 24
font.family: "Emoji"
text: label text: label
} }
hoverEnabled: true Rectangle {
ToolTip.text: category anchors.bottom: parent.bottom
ToolTip.visible: hovered
width: parent.width
height: 2
visible: emojiCategory === category
color: Material.accent
}
onClicked: emojiCategory = category onClicked: emojiCategory = category
} }
} }
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
Layout.leftMargin: 12
Layout.rightMargin: 12
color: MSettings.darkTheme ? "#424242" : "#e7ebeb"
}
GridView {
Layout.fillWidth: true
Layout.preferredHeight: 180
cellWidth: 48
cellHeight: 48
boundsBehavior: Flickable.DragOverBounds
clip: true
model: emojiModel.model[emojiCategory]
delegate: ItemDelegate {
width: 48
height: 48
contentItem: Label {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 32
text: modelData.unicode
}
onClicked: textArea.insert(textArea.cursorPosition, modelData.unicode)
}
ScrollBar.vertical: ScrollBar {}
} }
} }

View File

@ -1,6 +1,6 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.12
import Spectral.Setting 0.1 import Spectral.Setting 0.1
import Spectral.Font 0.1 import Spectral.Font 0.1
@ -10,8 +10,8 @@ Text {
id: materialLabel id: materialLabel
color: MSettings.darkTheme ? "white" : "dark" color: MPalette.foreground
font.pointSize: 16 font.pixelSize: 24
font.family: MaterialFont.name font.family: MaterialFont.name
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter

View File

@ -1,5 +1,5 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
MouseArea { MouseArea {
id: root id: root
@ -66,14 +66,13 @@ MouseArea {
} }
onWheel: { onWheel: {
var newPos = calculateNewPosition(flickable, wheel);
// console.warn("Delta: ", wheel.pixelDelta.y); // console.warn("Delta: ", wheel.pixelDelta.y);
// console.warn("Old position: ", flickable.contentY); // console.warn("Old position: ", flickable.contentY);
// console.warn("New position: ", newPos); // console.warn("New position: ", newPos);
// Show the scrollbars // Show the scrollbars
flickable.flick(0, 0); flickable.flick(0, 0);
flickable.contentY = newPos; flickable.contentY = calculateNewPosition(flickable, wheel);
cancelFlickStateTimer.start() cancelFlickStateTimer.start()
} }

View File

@ -1,25 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import "qrc:/js/util.js" as Util
ItemDelegate {
property var page
property bool selected: stackView.currentItem === page
property color highlightColor: Material.accent
Rectangle {
width: selected ? 4 : 0
height: parent.height
color: highlightColor
Behavior on width {
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
}
}
onClicked: Util.pushToStack(stackView, page)
}

View File

@ -37,10 +37,11 @@
** **
****************************************************************************/ ****************************************************************************/
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.12
import QtQuick.Window 2.1 import QtQuick.Window 2.1
import Spectral.Setting 0.1
Item { Item {
id: root id: root
@ -64,7 +65,7 @@ Item {
property Component handleDelegate: Rectangle { property Component handleDelegate: Rectangle {
width: 1 width: 1
height: 1 height: 1
visible: false color: MSettings.darkTheme ? "#424242" : "#E1E1E1"
} }
/*! /*!
@ -88,7 +89,7 @@ Item {
/*! \qmlmethod void SplitView::addItem(Item item) /*! \qmlmethod void SplitView::addItem(Item item)
Add an item to the end of the view. Add an item to the end of the view.
\since QtQuick.Controls 1.3 */ \since QtQuick.Controls 1.12 */
function addItem(item) { function addItem(item) {
d.updateLayoutGuard = true d.updateLayoutGuard = true
d.addItem_impl(item) d.addItem_impl(item)

View File

@ -1,6 +1,6 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.12
import Qt.labs.platform 1.0 import Qt.labs.platform 1.0
Item { Item {

View File

@ -0,0 +1,174 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import QtGraphicalEffects 1.0
import Qt.labs.platform 1.0 as Platform
import Spectral 0.1
import Spectral.Setting 0.1
import Spectral.Component 2.0
import Spectral.Font 0.1
ColumnLayout {
readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other")
readonly property bool sentByMe: author === currentRoom.localUser
property bool openOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
id: root
spacing: 0
onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile()
Label {
Layout.leftMargin: 48
text: author.displayName
visible: avatarVisible
font.pixelSize: 13
verticalAlignment: Text.AlignVCenter
}
RowLayout {
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
z: -5
id: messageRow
spacing: 4
Avatar {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
Layout.alignment: Qt.AlignTop
visible: avatarVisible
hint: author.displayName
source: author.avatarMediaId
}
Label {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
Layout.alignment: Qt.AlignTop
visible: !(sentByMe || avatarVisible)
text: Qt.formatDateTime(time, "hh:mm")
color: "#5B7480"
font.pixelSize: 10
horizontalAlignment: Label.AlignHCenter
verticalAlignment: Label.AlignVCenter
}
Control {
Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48
padding: 12
contentItem: RowLayout {
ToolButton {
contentItem: MaterialIcon {
icon: progressInfo.completed ? "\ue5ca" : "\ue2c4"
}
onClicked: progressInfo.completed ? openSavedFile() : saveFileAs()
}
ColumnLayout {
Label {
Layout.alignment: Qt.AlignVCenter
text: display
font.pixelSize: 18
font.weight: Font.Medium
font.capitalization: Font.AllUppercase
}
Label {
text: progressInfo.active ? (progressInfo.progress + "/" + progressInfo.total) : content.info.size
color: MPalette.lighter
}
}
}
background: Rectangle {
color: MPalette.banner
radius: 18
AutoMouseArea {
anchors.fill: parent
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: {
roomPanelInput.replyUser = author
roomPanelInput.replyEventID = eventId
roomPanelInput.replyContent = message
roomPanelInput.isReply = true
roomPanelInput.focus()
}
}
MenuItem {
text: "Redact"
onTriggered: currentRoom.redactEvent(eventId)
}
}
}
}
}
}
function saveFileAs() { currentRoom.saveFileAs(eventId) }
function downloadAndOpen()
{
if (downloaded) openSavedFile()
else
{
openOnFinished = true
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_") + (message || ".tmp"))
}
}
function openSavedFile()
{
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
}

View File

@ -1,37 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral.Setting 0.1
Control {
property bool highlighted: false
property bool colored: false
readonly property bool darkBackground: highlighted ? true : MSettings.darkTheme
padding: 12
AutoMouseArea {
anchors.fill: parent
onSecondaryClicked: {
messageContextMenu.row = messageRow
messageContextMenu.model = model
messageContextMenu.selectedText = contentLabel.selectedText
messageContextMenu.popup()
}
}
background: Rectangle {
color: colored ? Material.accent : highlighted ? Material.primary : Material.background
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 1
}
}
}

View File

@ -0,0 +1,168 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import QtGraphicalEffects 1.0
import Qt.labs.platform 1.0 as Platform
import Spectral 0.1
import Spectral.Setting 0.1
import Spectral.Component 2.0
import Spectral.Font 0.1
ColumnLayout {
readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other")
readonly property bool sentByMe: author === currentRoom.localUser
property bool openOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
id: root
spacing: 0
onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile()
Label {
Layout.leftMargin: 48
text: author.displayName
visible: avatarVisible
font.pixelSize: 13
verticalAlignment: Text.AlignVCenter
}
RowLayout {
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
z: -5
id: messageRow
spacing: 4
Avatar {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
Layout.alignment: Qt.AlignTop
visible: avatarVisible
hint: author.displayName
source: author.avatarMediaId
}
Label {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
Layout.alignment: Qt.AlignTop
visible: !(sentByMe || avatarVisible)
text: Qt.formatDateTime(time, "hh:mm")
color: "#5B7480"
font.pixelSize: 10
horizontalAlignment: Label.AlignHCenter
verticalAlignment: Label.AlignVCenter
}
Image {
Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48
id: img
source: downloaded ? progressInfo.localPath : "image://mxc/" +
(content.info && content.info.thumbnail_info ?
content.thumbnailMediaId : content.mediaId)
sourceSize.width: 200
sourceSize.height: 200
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: img.width
height: img.height
radius: 24
}
}
Rectangle {
anchors.fill: parent
color: "transparent"
radius: 24
border.width: 2
border.color: MPalette.banner
}
AutoMouseArea {
anchors.fill: parent
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: {
roomPanelInput.replyUser = author
roomPanelInput.replyEventID = eventId
roomPanelInput.replyContent = message
roomPanelInput.isReply = true
roomPanelInput.focus()
}
}
MenuItem {
text: "Redact"
onTriggered: currentRoom.redactEvent(eventId)
}
}
}
}
}
function saveFileAs() { currentRoom.saveFileAs(eventId) }
function downloadAndOpen()
{
if (downloaded) openSavedFile()
else
{
openOnFinished = true
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_") + (message || ".tmp"))
}
}
function openSavedFile()
{
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
}

View File

@ -1,78 +1,180 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.12
import Spectral 0.1 import Spectral 0.1
import Spectral.Setting 0.1 import Spectral.Setting 0.1
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Font 0.1
RowLayout { ColumnLayout {
readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other") readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other")
readonly property bool highlighted: !(sentByMe || eventType === "notice" )
readonly property bool sentByMe: author === currentRoom.localUser readonly property bool sentByMe: author === currentRoom.localUser
readonly property bool isText: eventType === "notice" || eventType === "message"
signal saveFileAs() signal saveFileAs()
signal openExternally() signal openExternally()
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
id: root
spacing: 0
Label {
Layout.leftMargin: 48
text: author.displayName
visible: avatarVisible
font.pixelSize: 13
verticalAlignment: Text.AlignVCenter
}
RowLayout {
z: -5 z: -5
id: messageRow id: messageRow
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft spacing: 4
spacing: 6 Avatar {
Layout.preferredWidth: 32
ImageItem { Layout.preferredHeight: 32
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
round: false
visible: avatarVisible visible: avatarVisible
hint: author.displayName hint: author.displayName
source: author.paintable source: author.avatarMediaId
} }
Rectangle { Label {
Layout.preferredWidth: 40 Layout.preferredWidth: 32
Layout.preferredHeight: 40 Layout.preferredHeight: 32
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
color: "transparent"
visible: !(sentByMe || avatarVisible) visible: !(sentByMe || avatarVisible)
text: Qt.formatDateTime(time, "hh:mm")
color: MPalette.lighter
font.pixelSize: 10
horizontalAlignment: Label.AlignHCenter
verticalAlignment: Label.AlignVCenter
} }
GenericBubble { Control {
Layout.maximumWidth: messageListView.width - (!sentByMe ? 40 + messageRow.spacing : 0) Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48
id: genericBubble verticalPadding: 8
horizontalPadding: 16
highlighted: messageRow.highlighted background: Rectangle {
colored: highlighted && (eventType === "notice" || highlight) color: sentByMe ? "#009DC2" : eventType === "notice" ? "#4285F4" : "#673AB7"
radius: 18
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: {
roomPanelInput.replyUser = author
roomPanelInput.replyEventID = eventId
roomPanelInput.replyContent = messageContextMenu.selectedText || message
roomPanelInput.isReply = true
roomPanelInput.focus()
}
}
MenuItem {
text: "Redact"
onTriggered: currentRoom.redactEvent(eventId)
}
}
}
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
id: messageColumn Control {
Layout.fillWidth: true
visible: replyEventId || ""
padding: 8
background: Item {
Rectangle {
anchors.leftMargin: 0
width: 2
height: parent.height
color: "white"
}
MouseArea {
anchors.fill: parent
onClicked: goToEvent(replyEventId)
}
}
contentItem: RowLayout {
spacing: 8
Avatar {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
Layout.alignment: Qt.AlignTop
source: replyAuthor ? replyAuthor.avatarMediaId : ""
hint: replyAuthor ? replyAuthor.displayName : "H"
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0 spacing: 0
TimelineLabel { Label {
Layout.fillWidth: true Layout.fillWidth: true
id: authorLabel color: "white"
text: replyAuthor ? replyAuthor.displayName : ""
visible: messageRow.avatarVisible font.pixelSize: 13
text: author.displayName font.weight: Font.Medium
Material.foreground: Material.accent }
coloredBackground: highlighted
font.bold: true
MouseArea { Label {
anchors.fill: parent Layout.fillWidth: true
cursorShape: Qt.PointingHandCursor
onClicked: roomPanelInput.insert(author.displayName) color: "white"
text: replyDisplay || ""
wrapMode: Label.Wrap
textFormat: Label.RichText
}
}
} }
} }
@ -81,21 +183,30 @@ RowLayout {
id: contentLabel id: contentLabel
text: (highlighted ? "<style>a{color: white;} .user-pill{color: white}</style>" : "<style>a{color: " + Material.accent + ";} .user-pill{color: " + Material.accent + "}</style>") + display text: "<style>a{color: white;} .user-pill{}</style>" + display
visible: isText color: "white"
color: highlighted ? "white": Material.foreground
font.family: authorLabel.font.family font.family: CommonFont.font.family
font.pointSize: 10 font.pixelSize: 14
selectByMouse: true selectByMouse: true
readOnly: true readOnly: true
wrapMode: Label.Wrap wrapMode: Label.Wrap
selectedTextColor: highlighted ? Material.accent : "white" selectedTextColor: Material.accent
selectionColor: highlighted ? "white" : Material.accent selectionColor: "white"
textFormat: Text.RichText textFormat: Text.RichText
onLinkActivated: Qt.openUrlExternally(link) onLinkActivated: {
if (link.startsWith("https://matrix.to/")) {
var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)")
if (result.length < 3) return
if (result[1] != currentRoom.id) return
if (!result[2]) return
goToEvent(result[2])
} else {
Qt.openUrlExternally(link)
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@ -103,154 +214,6 @@ RowLayout {
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
} }
} }
Loader {
sourceComponent: {
switch (eventType) {
case "image":
return imageComponent
case "file":
return fileComponent
case "audio":
return audioComponent
}
}
active: eventType === "image" || eventType === "file" || eventType === "audio"
}
Row {
Layout.alignment: Qt.AlignRight
spacing: 4
TimelineLabel {
visible: userMarker.length > 5
text: userMarker.length - 5 + "+"
coloredBackground: highlighted
Material.foreground: "grey"
font.pointSize: 8
}
Repeater {
model: userMarker.length > 5 ? userMarker.slice(0, 5) : userMarker
ImageItem {
width: parent.height
height: parent.height
hint: modelData.displayName
source: modelData.paintable
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
readMarkerDialog.listModel = userMarker
readMarkerDialog.open()
}
}
}
}
TimelineLabel {
id: timeLabel
visible: Math.abs(time - aboveTime) > 600000 || index == 0
text: Qt.formatTime(time, "hh:mm")
coloredBackground: highlighted
Material.foreground: "grey"
font.pointSize: 8
}
}
}
Component {
id: imageComponent
DownloadableContent {
width: messageImage.width
height: messageImage.height
id: downloadable
TimelineImage {
z: -4
id: messageImage
sourceSize: 128
source: "image://mxc/" + (content.thumbnail_url ? content.thumbnail_url : content.url)
onClicked: downloadAndOpen()
}
Component.onCompleted: {
messageRow.saveFileAs.connect(saveFileAs)
messageRow.openExternally.connect(downloadAndOpen)
}
}
}
Component {
id: fileComponent
TimelineLabel {
Layout.fillWidth: true
id: downloadDelegate
text: "<b>File: </b>" + content.body
coloredBackground: highlighted
background: DownloadableContent {
id: downloadable
Component.onCompleted: {
messageRow.saveFileAs.connect(saveFileAs)
messageRow.openExternally.connect(downloadAndOpen)
}
}
}
}
Component {
id: audioComponent
TimelineLabel {
id: downloadDelegate
text: content.info.duration / 1000 + '"'
coloredBackground: highlighted
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
onClicked: {
if (downloadable.downloaded)
spectralController.playAudio(progressInfo.localPath)
else
{
playOnFinished = true
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_") + ".tmp")
}
}
}
background: DownloadableContent {
id: downloadable
onDownloadedChanged: downloaded && playOnFinished ? spectralController.playAudio(progressInfo.localPath) : {}
Component.onCompleted: {
messageRow.saveFileAs.connect(saveFileAs)
messageRow.openExternally.connect(downloadAndOpen)
}
}
} }
} }
} }

View File

@ -0,0 +1,12 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import Spectral.Setting 0.1
Label {
text: section + " • " + Qt.formatTime(time, "hh:mm")
color: MPalette.foreground
font.pixelSize: 13
font.weight: Font.Medium
font.capitalization: Font.AllUppercase
verticalAlignment: Text.AlignVCenter
}

View File

@ -1,24 +1,27 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.12
import Spectral.Setting 0.1 import Spectral.Setting 0.1
Label { Label {
Layout.alignment: Qt.AlignHCenter
text: "<b>" + author.displayName + "</b> " + display text: "<b>" + author.displayName + "</b> " + display
color: "white" color: MPalette.foreground
font.pixelSize: 13
font.weight: Font.Medium
padding: 8 topPadding: 8
bottomPadding: 8
leftPadding: 24
rightPadding: 24
wrapMode: Label.Wrap wrapMode: Label.Wrap
linkColor: "white"
textFormat: MSettings.richText ? Text.RichText : Text.StyledText textFormat: MSettings.richText ? Text.RichText : Text.StyledText
onLinkActivated: Qt.openUrlExternally(link) onLinkActivated: Qt.openUrlExternally(link)
background: Rectangle { background: Rectangle {
color: MSettings.darkTheme ? "#484848" : "grey" color: MPalette.banner
radius: 4
} }
} }

View File

@ -1,34 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
Item {
property alias source: baseImage.source
property alias sourceSize: baseImage.sourceSize.width
readonly property bool loading: baseImage.status == Image.Loading
signal clicked()
width: loading ? 128 : baseImage.implicitWidth
height: loading ? progressBar.height : baseImage.implicitHeight
id: rekt
Image { id: baseImage }
ProgressBar {
width: parent.width
visible: loading
id: progressBar
indeterminate: true
}
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
onClicked: rekt.clicked()
}
}

View File

@ -1,17 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import Spectral.Setting 0.1
Label {
property bool coloredBackground
color: coloredBackground ? "white": Material.foreground
wrapMode: Label.Wrap
linkColor: coloredBackground ? "white" : Material.accent
textFormat: Text.RichText
onLinkActivated: Qt.openUrlExternally(link)
}

View File

@ -1,4 +1,6 @@
module Spectral.Component.Timeline module Spectral.Component.Timeline
MessageDelegate 2.0 MessageDelegate.qml MessageDelegate 2.0 MessageDelegate.qml
StateDelegate 2.0 StateDelegate.qml StateDelegate 2.0 StateDelegate.qml
SectionDelegate 2.0 SectionDelegate.qml
ImageDelegate 2.0 ImageDelegate.qml
FileDelegate 2.0 FileDelegate.qml

View File

@ -6,3 +6,4 @@ ScrollHelper 2.0 ScrollHelper.qml
AutoListView 2.0 AutoListView.qml AutoListView 2.0 AutoListView.qml
AutoTextField 2.0 AutoTextField.qml AutoTextField 2.0 AutoTextField.qml
SplitView 2.0 SplitView.qml SplitView 2.0 SplitView.qml
Avatar 2.0 Avatar.qml

View File

@ -0,0 +1,29 @@
import QtQuick 2.12
import QtGraphicalEffects 1.0
Item {
id: item
property alias source: mask.source
Rectangle {
id: circleMask
width: parent.width
height: parent.height
smooth: true
visible: false
radius: Math.max(width/2, height/2)
}
OpacityMask {
id: mask
width: parent.width
height: parent.height
maskSource: circleMask
}
}

View File

@ -1,4 +1,4 @@
import QtQuick 2.9 import QtQuick 2.12
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
/*! /*!
@ -7,7 +7,8 @@ import QtGraphicalEffects 1.0
Item { Item {
id: effect id: effect
property variant source property var source
readonly property Item sourceItem: source.sourceItem
property int elevation: 0 property int elevation: 0
@ -134,7 +135,7 @@ Item {
glowRadius: modelData.blur/2 glowRadius: modelData.blur/2
spread: 0.05 spread: 0.05
color: _shadowColors[index] color: _shadowColors[index]
cornerRadius: modelData.blur + (effect.source.radius || 0) cornerRadius: modelData.blur + (effect.sourceItem.radius || 0)
} }
} }

View File

@ -0,0 +1,239 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtGraphicalEffects 1.0
import Spectral.Component 2.0
import Spectral.Setting 0.1
AutoMouseArea {
id: ripple
property color color: MSettings.darkTheme ? Qt.rgba(255, 255, 255, 0.16) : Qt.rgba(0, 0, 0, 0.08)
property bool circular: false
property bool centered: false
property bool focused
property color focusColor: "transparent"
property int focusWidth: width - 32
property Item control
clip: true
Connections {
target: control
onPressedChanged: {
if (!control.pressed)
__private.removeLastCircle()
}
}
onPressed: {
__private.createTapCircle(mouse.x, mouse.y)
if (control)
mouse.accepted = false
}
onReleased: __private.removeLastCircle()
onCanceled: __private.removeLastCircle()
QtObject {
id: __private
property int startRadius: 0
property int endRadius
property bool showFocus: true
property Item lastCircle
function createTapCircle(x, y) {
endRadius = centered ? width/2 : radius(x, y) + 5
showFocus = false
lastCircle = tapCircle.createObject(ripple, {
"circleX": centered ? width/2 : x,
"circleY": centered ? height/2 : y
})
}
function removeLastCircle() {
if (lastCircle)
lastCircle.removeCircle()
}
function radius(x, y) {
var dist1 = Math.max(dist(x, y, 0, 0), dist(x, y, width, height))
var dist2 = Math.max(dist(x, y, width, 0), dist(x, y, 0, height))
return Math.max(dist1, dist2)
}
function dist(x1, y1, x2, y2) {
var distX = x2 - x1
var distY = y2 - y1
return Math.sqrt(distX * distX + distY * distY)
}
}
Rectangle {
id: focusBackground
objectName: "focusBackground"
width: parent.width
height: parent.height
color: Qt.rgba(0,0,0,0.2)
opacity: __private.showFocus && focused ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: 500; easing.type: Easing.InOutQuad }
}
}
Rectangle {
id: focusCircle
objectName: "focusRipple"
property bool focusedState
x: (parent.width - width)/2
y: (parent.height - height)/2
width: focused
? focusedState ? focusWidth
: Math.min(parent.width - 8, focusWidth + 12)
: parent.width/5
height: width
radius: width/2
opacity: __private.showFocus && focused ? 1 : 0
color: focusColor.a === 0 ? Qt.rgba(1,1,1,0.4) : focusColor
Behavior on opacity {
NumberAnimation { duration: 500; easing.type: Easing.InOutQuad }
}
Behavior on width {
NumberAnimation { duration: focusTimer.interval; }
}
Timer {
id: focusTimer
running: focused
repeat: true
interval: 800
onTriggered: focusCircle.focusedState = !focusCircle.focusedState
}
}
Component {
id: tapCircle
Item {
id: circleItem
objectName: "tapRipple"
property bool done
property real circleX
property real circleY
property bool closed
width: parent.width
height: parent.height
function removeCircle() {
done = true
if (fillSizeAnimation.running) {
fillOpacityAnimation.stop()
closeAnimation.start()
circleItem.destroy(500);
} else {
__private.showFocus = true
fadeAnimation.start();
circleItem.destroy(300);
}
}
Item {
id: circleParent
width: parent.width
height: parent.height
visible: !circular
Rectangle {
id: circleRectangle
x: circleItem.circleX - radius
y: circleItem.circleY - radius
width: radius * 2
height: radius * 2
opacity: 0
color: ripple.color
NumberAnimation {
id: fillSizeAnimation
running: true
target: circleRectangle; property: "radius"; duration: 500;
from: __private.startRadius; to: __private.endRadius;
easing.type: Easing.InOutQuad
onStopped: {
if (done)
__private.showFocus = true
}
}
NumberAnimation {
id: fillOpacityAnimation
running: true
target: circleRectangle; property: "opacity"; duration: 300;
from: 0; to: 1; easing.type: Easing.InOutQuad
}
NumberAnimation {
id: fadeAnimation
target: circleRectangle; property: "opacity"; duration: 300;
from: 1; to: 0; easing.type: Easing.InOutQuad
}
SequentialAnimation {
id: closeAnimation
NumberAnimation {
target: circleRectangle; property: "opacity"; duration: 250;
to: 1; easing.type: Easing.InOutQuad
}
NumberAnimation {
target: circleRectangle; property: "opacity"; duration: 250;
from: 1; to: 0; easing.type: Easing.InOutQuad
}
}
}
}
CircleMask {
anchors.fill: parent
source: circleParent
visible: circular
}
}
}
}

View File

@ -1,2 +1,3 @@
module Spectral.Effect module Spectral.Effect
ElevationEffect 2.0 ElevationEffect.qml ElevationEffect 2.0 ElevationEffect.qml
RippleEffect 2.0 RippleEffect.qml

View File

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

View File

@ -1,5 +1,5 @@
pragma Singleton pragma Singleton
import QtQuick 2.9 import QtQuick 2.12
FontLoader { FontLoader {
source: "qrc:/assets/font/material.ttf" source: "qrc:/assets/font/material.ttf"

View File

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

View File

@ -1,53 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
Menu {
property var row: null
property var model: null
property string selectedText
readonly property bool isFile: model && (model.eventType === "video" || model.eventType === "audio" || model.eventType === "file" || model.eventType === "image")
id: messageContextMenu
MenuItem {
text: "View Source"
onTriggered: {
sourceDialog.sourceText = model.toolTip
sourceDialog.open()
}
}
MenuItem {
visible: isFile
height: visible ? undefined : 0
text: "Open Externally"
onTriggered: row.openExternally()
}
MenuItem {
visible: isFile
height: visible ? undefined : 0
text: "Save As"
onTriggered: row.saveFileAs()
}
MenuItem {
height: visible ? undefined : 0
text: "Reply"
onTriggered: {
roomPanelInput.isReply = true
roomPanelInput.replyUserID = model.author.id
roomPanelInput.replyEventID = model.eventId
roomPanelInput.replyContent = selectedText != "" ? selectedText : model.message
}
}
MenuItem {
visible: model && model.author === currentRoom.localUser
height: visible ? undefined : 0
text: "Redact"
onTriggered: currentRoom.redactEvent(model.eventId)
}
}

View File

@ -1,35 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import Spectral 0.1
Menu {
property var model: null
id: roomListMenu
MenuItem {
text: "Favourite"
checkable: true
checked: model && model.category === RoomType.Favorite
onTriggered: model.category === RoomType.Favorite ? model.currentRoom.removeTag("m.favourite") : model.currentRoom.addTag("m.favourite", 1.0)
}
MenuItem {
text: "Deprioritize"
checkable: true
checked: model && model.category === RoomType.Deprioritized
onTriggered: model.category === RoomType.Deprioritized ? model.currentRoom.removeTag("m.lowpriority") : model.currentRoom.addTag("m.lowpriority", 1.0)
}
MenuSeparator {}
MenuItem {
text: "Mark as Read"
onTriggered: model.currentRoom.markAllMessagesAsRead()
}
MenuItem {
text: "Leave Room"
onTriggered: model.currentRoom.forget()
}
}

View File

@ -1,3 +0,0 @@
module Spectral.Menu
MessageContextMenu 2.0 MessageContextMenu.qml
RoomContextMenu 2.0 RoomContextMenu.qml

View File

@ -1,27 +0,0 @@
import QtQuick 2.9
LoginForm {
loginButton.onClicked: doLogin()
Shortcut {
sequence: "Return"
onActivated: doLogin()
}
function doLogin() {
if (!(serverField.text.startsWith("http") && serverField.text.includes("://"))) {
loginButtonTooltip.text = "Server address should start with http(s)://"
loginButtonTooltip.open()
return
}
loginButton.text = "Logging in..."
loginButton.enabled = false
controller.loginWithCredentials(serverField.text, usernameField.text, passwordField.text)
controller.connectionAdded.connect(function(conn) {
stackView.pop()
accountListView.currentConnection = conn
})
}
}

View File

@ -1,156 +0,0 @@
import QtQuick 2.9
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import Spectral.Component 2.0
import Spectral.Setting 0.1
Page {
property var controller
property alias loginButton: loginButton
property alias serverField: serverField
property alias usernameField: usernameField
property alias passwordField: passwordField
property alias loginButtonTooltip: loginButtonTooltip
Row {
anchors.fill: parent
Pane {
width: parent.width / 2
height: parent.height
background: Item {
Image {
id: background
anchors.fill: parent
source: "qrc:/assets/img/background.jpg"
fillMode: Image.PreserveAspectCrop
cache: false
}
ColorOverlay {
anchors.fill: background
source: background
color: Material.accent
opacity: 0.7
}
}
Column {
x: 32
anchors.verticalCenter: parent.verticalCenter
Label {
text: "MATRIX LOGIN."
font.pointSize: 28
font.bold: true
color: "white"
}
Label {
text: "A NEW METHOD OF MESSAGING"
font.pointSize: 12
color: "white"
}
}
}
Pane {
width: parent.width / 2
height: parent.height
padding: 64
ColumnLayout {
width: parent.width
id: mainCol
AutoTextField {
Layout.fillWidth: true
id: serverField
leftPadding: 16
topPadding: 0
bottomPadding: 0
text: "https://matrix.org"
placeholderText: "Server"
background: Rectangle {
implicitHeight: 48
color: MSettings.darkTheme ? "#242424" : "#eaeaea"
border.color: parent.activeFocus ? Material.accent : "transparent"
border.width: 2
}
}
AutoTextField {
Layout.fillWidth: true
id: usernameField
leftPadding: 16
topPadding: 0
bottomPadding: 0
placeholderText: "Username"
background: Rectangle {
implicitHeight: 48
color: MSettings.darkTheme ? "#242424" : "#eaeaea"
border.color: parent.activeFocus ? Material.accent : "transparent"
border.width: 2
}
}
AutoTextField {
Layout.fillWidth: true
id: passwordField
leftPadding: 16
topPadding: 0
bottomPadding: 0
placeholderText: "Password"
echoMode: TextInput.Password
background: Rectangle {
implicitHeight: 48
color: MSettings.darkTheme ? "#242424" : "#eaeaea"
border.color: parent.activeFocus ? Material.accent : "transparent"
border.width: 2
}
}
Button {
Layout.fillWidth: true
id: loginButton
text: "LOGIN"
highlighted: true
ToolTip {
id: loginButtonTooltip
}
}
}
}
}
}
/*##^## Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -1,67 +0,0 @@
import QtQuick 2.9
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Spectral.Panel 2.0
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
Page {
property alias connection: roomListModel.connection
property alias enteredRoom: roomListForm.enteredRoom
property alias filter: roomListForm.filter
id: page
RoomListModel {
id: roomListModel
onNewMessage: if (!window.active) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon, iconPath)
}
SplitView {
anchors.fill: parent
RoomListPanel {
width: page.width * 0.35
Layout.minimumWidth: 64
id: roomListForm
listModel: roomListModel
onWidthChanged: {
if (width < 240) width = 64
}
ElevationEffect {
anchors.fill: source
z: source.z - 1
source: parent
elevation: 4
}
onLeaveRoom: roomForm.saveReadMarker(room)
}
RoomPanel {
Layout.fillWidth: true
Layout.minimumWidth: 480
id: roomForm
currentRoom: roomListForm.enteredRoom
}
}
function goToEvent(eventID) {
roomForm.goToEvent(eventID)
}
}

View File

@ -1,5 +0,0 @@
import QtQuick 2.9
SettingForm {
addAccountButton.onClicked: stackView.push(loginPage)
}

View File

@ -1,135 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import Spectral.Component 2.0
import Spectral 0.1
import Spectral.Setting 0.1
Column {
property bool expanded: false
spacing: 8
ItemDelegate {
width: accountSettingsListView.width
height: 64
Row {
anchors.fill: parent
anchors.margins: 8
spacing: 8
ImageItem {
width: parent.height
height: parent.height
hint: user.displayName
source: user.paintable
}
ColumnLayout {
Label {
text: user.displayName
}
Label {
text: user.id
}
}
}
onClicked: expanded = !expanded
}
ColumnLayout {
width: parent.width - 32
height: expanded ? implicitHeight : 0
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0
clip: true
AutoListView {
Layout.fillWidth: true
Layout.preferredHeight: 24
orientation: ListView.Horizontal
spacing: 8
model: ["#498882", "#42a5f5", "#5c6bc0", "#7e57c2", "#ab47bc", "#ff7043"]
delegate: Rectangle {
width: parent.height
height: parent.height
radius: width / 2
color: modelData
MouseArea {
anchors.fill: parent
onClicked: spectralController.setColor(connection.localUserId, modelData)
}
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Homeserver:"
}
AutoTextField {
Layout.fillWidth: true
text: connection.homeserver
readOnly: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: 16
Label {
text: "Device ID:"
}
AutoTextField {
Layout.fillWidth: true
text: connection.deviceId
readOnly: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: 16
Label {
text: "Access Token:"
}
AutoTextField {
Layout.fillWidth: true
text: connection.accessToken
readOnly: true
}
}
Button {
Layout.fillWidth: true
highlighted: true
text: "Logout"
onClicked: spectralController.logout(connection)
}
}
}

View File

@ -1,11 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
ItemDelegate {
text: category
onClicked: {
settingStackView.clear()
settingStackView.push([accountForm, generalForm, appearanceForm, aboutForm][form])
}
}

View File

@ -1,185 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import QtQuick.Layouts 1.3
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
Page {
property alias listModel: accountSettingsListView.model
property alias addAccountButton: addAccountButton
implicitWidth: 400
implicitHeight: 300
Page {
id: accountForm
parent: null
padding: 64
ColumnLayout {
anchors.fill: parent
AutoListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: accountSettingsListView
boundsBehavior: Flickable.DragOverBounds
clip: true
delegate: SettingAccountDelegate {}
}
Button {
Layout.fillWidth: true
id: addAccountButton
text: "Add Account"
flat: true
highlighted: true
}
}
}
Page {
id: generalForm
parent: null
padding: 64
Column {
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: "Confirm on Exit"
checked: MSettings.confirmOnExit
onCheckedChanged: MSettings.confirmOnExit = checked
}
}
}
Page {
id: appearanceForm
parent: null
padding: 64
Column {
Switch {
text: "Dark theme"
checked: MSettings.darkTheme
onCheckedChanged: MSettings.darkTheme = checked
}
}
}
Page {
id: aboutForm
parent: null
padding: 64
ColumnLayout {
spacing: 16
Image {
Layout.preferredWidth: 64
Layout.preferredHeight: 64
source: "qrc:/assets/img/icon.png"
}
Label {
text: "Spectral, an IM client for the Matrix protocol."
}
Label {
text: "Released under GNU General Public License, version 3."
}
}
}
Rectangle {
width: 240
height: parent.height
z: 10
id: settingDrawer
color: MSettings.darkTheme ? "#323232" : "#f3f3f3"
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 4
}
Column {
anchors.fill: parent
Repeater {
model: ListModel {
ListElement {
category: "Accounts"
form: 0
}
ListElement {
category: "General"
form: 1
}
ListElement {
category: "Appearance"
form: 2
}
ListElement {
category: "About"
form: 3
}
}
delegate: SettingCategoryDelegate {
width: parent.width
}
}
}
}
StackView {
anchors.fill: parent
anchors.leftMargin: settingDrawer.width
id: settingStackView
initialItem: aboutForm
}
}
/*##^## Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -1,39 +1,30 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.12
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.12
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral 0.1 import Spectral 0.1
import "qrc:/js/util.js" as Util
Drawer { Drawer {
property var room property var room
id: roomDrawer id: roomDrawer
edge: Qt.RightEdge edge: Qt.RightEdge
interactive: false
ToolButton {
contentItem: MaterialIcon { icon: "\ue5c4" }
onClicked: roomDrawer.close()
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 32 anchors.margins: 32
ImageItem { Avatar {
Layout.preferredWidth: 96 Layout.preferredWidth: 96
Layout.preferredHeight: 96 Layout.preferredHeight: 96
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
hint: room ? room.displayName : "No name" hint: room ? room.displayName : "No name"
source: room ? room.paintable : null source: room ? room.avatarMediaId : null
} }
Label { Label {
@ -57,7 +48,7 @@ Drawer {
wrapMode: Label.Wrap wrapMode: Label.Wrap
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: room ? room.memberCount + " Members" : "No Member Count" text: room ? room.totalMemberCount + " Members" : "No Member Count"
} }
RowLayout { RowLayout {
@ -124,11 +115,11 @@ Drawer {
anchors.margins: 8 anchors.margins: 8
spacing: 12 spacing: 12
ImageItem { Avatar {
Layout.preferredWidth: height Layout.preferredWidth: height
Layout.fillHeight: true Layout.fillHeight: true
source: paintable source: avatar
hint: name hint: name
} }

View File

@ -1,25 +1,31 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.12
import Spectral 0.1 import Spectral 0.1
import Spectral.Effect 2.0
import Spectral.Component 2.0
import Spectral.Setting 0.1
Rectangle { Control {
property alias paintable: headerImage.source property alias avatar: headerImage.source
property alias topic: headerTopicLabel.text property alias topic: headerTopicLabel.text
property bool atTop: false
signal clicked() signal clicked()
id: header id: header
color: Material.accent background: Rectangle {
color: Material.background
ItemDelegate { opacity: atTop ? 0 : 1
anchors.fill: parent
id: roomHeader layer.enabled: true
layer.effect: ElevationEffect {
onClicked: header.clicked() elevation: 2
}
}
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@ -27,13 +33,13 @@ Rectangle {
spacing: 12 spacing: 12
ImageItem { Avatar {
Layout.preferredWidth: height Layout.preferredWidth: height
Layout.fillHeight: true Layout.fillHeight: true
id: headerImage id: headerImage
source: currentRoom.paintable source: currentRoom.avatarMediaId
hint: currentRoom ? currentRoom.displayName : "No name" hint: currentRoom ? currentRoom.displayName : "No name"
} }
@ -48,8 +54,8 @@ Rectangle {
Layout.fillHeight: true Layout.fillHeight: true
text: currentRoom ? currentRoom.displayName : "" text: currentRoom ? currentRoom.displayName : ""
color: "white" color: MPalette.foreground
font.pointSize: 12 font.pixelSize: 16
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
} }
@ -60,21 +66,16 @@ Rectangle {
id: headerTopicLabel id: headerTopicLabel
color: "white" color: MPalette.lighter
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
} }
} }
} }
}
ProgressBar { RippleEffect {
width: parent.width anchors.fill: parent
z: 10
anchors.bottom: parent.bottom
Material.accent: "white" onClicked: header.clicked()
visible: currentRoom && currentRoom.busy
indeterminate: true
} }
} }

View File

@ -1,100 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Spectral 0.1
import Spectral.Setting 0.1
import Spectral.Component 2.0
Rectangle {
color: MSettings.darkTheme ? "#303030" : "#fafafa"
AutoMouseArea {
anchors.fill: parent
hoverEnabled: miniMode
onSecondaryClicked: {
roomContextMenu.model = model
roomContextMenu.popup()
}
onPrimaryClicked: {
if (category === RoomType.Invited) {
inviteDialog.currentRoom = currentRoom
inviteDialog.open()
} else {
leaveRoom(enteredRoom)
enterRoom(currentRoom)
enteredRoom = currentRoom
}
}
ToolTip.visible: miniMode && containsMouse
ToolTip.text: name
}
Rectangle {
anchors.fill: parent
visible: highlightCount > 0 || currentRoom === enteredRoom
color: Material.accent
opacity: 0.1
}
Rectangle {
width: unreadCount > 0 ? 4 : 0
height: parent.height
color: Material.accent
Behavior on width {
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
ImageItem {
id: imageItem
Layout.preferredWidth: height
Layout.fillHeight: true
source: paintable
hint: name || "No Name"
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: parent.width > 64
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: name || "No Name"
font.pointSize: 12
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm,"")
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}

View File

@ -1,15 +1,42 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
RoomListPanelForm { Item {
model: sortedRoomListModel property var controller: null
readonly property var user: controller.connection ? controller.connection.localUser : null
property int filter: 0
property var enteredRoom: null
property alias errorControl: errorControl
signal enterRoom(var room)
signal leaveRoom(var room)
id: root
RoomListModel {
id: roomListModel
connection: controller.connection
onNewMessage: if (!window.active && MSettings.showNotification) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon)
}
SortFilterProxyModel { SortFilterProxyModel {
id: sortedRoomListModel id: sortedRoomListModel
sourceModel: listModel sourceModel: roomListModel
proxyRoles: ExpressionRole { proxyRoles: ExpressionRole {
name: "display" name: "display"
@ -53,9 +80,841 @@ RoomListPanelForm {
] ]
} }
Shortcut { Drawer {
sequence: StandardKey.Find width: Math.max(root.width, 400)
onActivated: searchField.forceActiveFocus() 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
Control {
Layout.fillWidth: true
Layout.preferredHeight: 64
id: roomListHeader
topPadding: 12
bottomPadding: 12
leftPadding: 12
rightPadding: 18
contentItem: RowLayout {
ItemDelegate {
Layout.preferredWidth: height
Layout.fillHeight: true
visible: !searchField.active
contentItem: MaterialIcon {
icon: {
switch (filter) {
case 0: return "\ue8b6"
case 1: return "\ue7f5"
case 2: return "\ue7ff"
case 3: return "\ue7fc"
}
}
}
Menu {
id: filterMenu
MenuItem {
text: "All"
onClicked: filter = 0
}
MenuSeparator {}
MenuItem {
text: "New"
onClicked: filter = 1
}
MenuItem {
text: "People"
onClicked: filter = 2
}
MenuItem {
text: "Group"
onClicked: filter = 3
}
}
onClicked: filterMenu.popup()
}
ItemDelegate {
Layout.preferredWidth: height
Layout.fillHeight: true
visible: searchField.active
contentItem: MaterialIcon { icon: "\ue5cd" }
onClicked: searchField.clear()
}
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
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 {
Layout.preferredWidth: height
Layout.fillHeight: true
Layout.alignment: Qt.AlignRight
visible: !searchField.active
source: root.user ? root.user.avatarMediaId : null
hint: root.user ? root.user.displayName : "?"
MouseArea {
anchors.fill: parent
onClicked: drawer.open()
}
}
}
background: Rectangle {
color: Material.background
opacity: listView.atYBeginning ? 0 : 1
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 2
}
}
}
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
id: listView
spacing: 0
clip: true
model: sortedRoomListModel
boundsBehavior: Flickable.DragOverBounds
ScrollBar.vertical: ScrollBar {}
delegate: Item {
width: listView.width
height: 64
Rectangle {
anchors.fill: parent
visible: currentRoom === enteredRoom
color: Material.accent
opacity: 0.1
}
Rectangle {
width: unreadCount > 0 ? 4 : 0
height: parent.height
color: Material.accent
Behavior on width {
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
Avatar {
Layout.preferredWidth: height
Layout.fillHeight: true
source: avatar
hint: name || "No Name"
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: name || "No Name"
color: MPalette.foreground
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm,"")
color: MPalette.lighter
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
Label {
visible: notificationCount > 0 && highlightCount == 0
color: "white"
text: notificationCount
leftPadding: 12
rightPadding: 12
topPadding: 4
bottomPadding: 4
font.bold: true
background: Rectangle {
radius: height / 2
color: MPalette.lighter
}
}
Label {
visible: highlightCount > 0
color: "white"
text: highlightCount
leftPadding: 12
rightPadding: 12
topPadding: 4
bottomPadding: 4
font.bold: true
background: Rectangle {
radius: height / 2
color: MPalette.accent
}
}
}
RippleEffect {
anchors.fill: parent
onSecondaryClicked: roomContextMenu.popup()
onPrimaryClicked: {
if (category === RoomType.Invited) {
inviteDialog.currentRoom = currentRoom
inviteDialog.open()
} else {
if (enteredRoom) {
enteredRoom.displayed = false
leaveRoom(enteredRoom)
}
currentRoom.displayed = true
enterRoom(currentRoom)
enteredRoom = currentRoom
}
}
}
Menu {
id: roomContextMenu
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()
}
}
}
section.property: "display"
section.criteria: ViewSection.FullString
section.delegate: Label {
width: parent.width
height: 24
text: section
color: MPalette.lighter
leftPadding: 16
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
} }
Dialog { Dialog {
@ -70,11 +929,30 @@ RoomListPanelForm {
title: "Action Required" title: "Action Required"
modal: true modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: Label { text: "Accept this invitation?" } contentItem: Label { text: "Accept this invitation?" }
onAccepted: currentRoom.acceptInvitation() footer: DialogButtonBox {
onRejected: currentRoom.forget() Button {
text: "Accept"
flat: true
onClicked: currentRoom.acceptInvitation()
}
Button {
text: "Reject"
flat: true
onClicked: currentRoom.forget()
}
Button {
text: "Cancel"
flat: true
onClicked: inviteDialog.close()
}
}
} }
} }

View File

@ -1,138 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls.Material 2.2
import QtQml.Models 2.3
import Spectral.Component 2.0
import Spectral.Menu 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
import SortFilterProxyModel 0.2
import "qrc:/js/util.js" as Util
Rectangle {
property var listModel
property int filter: 0
property var enteredRoom: null
property alias searchField: searchField
property alias model: listView.model
property bool miniMode: width == 64
signal enterRoom(var room)
signal leaveRoom(var room)
color: MSettings.darkTheme ? "#323232" : "#f3f3f3"
Label {
text: miniMode ? "Empty" : "Here? No, not here."
anchors.centerIn: parent
visible: listView.count === 0
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.margins: 12
color: MSettings.darkTheme ? "#303030" : "#fafafa"
RowLayout {
anchors.fill: parent
spacing: 0
MaterialIcon {
Layout.preferredWidth: height
Layout.fillHeight: true
visible: !miniMode && !searchField.text
icon: "\ue8b6"
color: "grey"
}
ItemDelegate {
Layout.preferredWidth: height
Layout.fillHeight: true
visible: !miniMode && searchField.text
contentItem: MaterialIcon {
icon: "\ue5cd"
color: "grey"
}
onClicked: searchField.text = ""
}
AutoTextField {
Layout.fillWidth: true
Layout.fillHeight: true
id: searchField
topPadding: 0
bottomPadding: 0
placeholderText: "Search..."
background: Item {
}
}
}
}
AutoListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: listView
spacing: 1
clip: true
boundsBehavior: Flickable.DragOverBounds
ScrollBar.vertical: ScrollBar {
}
delegate: RoomListDelegate {
width: parent.width
height: 64
}
section.property: "display"
section.criteria: ViewSection.FullString
section.delegate: Label {
width: parent.width
height: 24
text: section
color: "grey"
leftPadding: miniMode ? undefined : 16
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
horizontalAlignment: miniMode ? Text.AlignHCenter : undefined
}
RoomContextMenu {
id: roomContextMenu
}
}
}
}
/*##^## Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -1,50 +1,425 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import Qt.labs.qmlmodels 1.0
RoomPanelForm { import Spectral.Component 2.0
roomHeader.paintable: currentRoom ? currentRoom.paintable : null import Spectral.Component.Emoji 2.0
roomHeader.topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : "" import Spectral.Component.Timeline 2.0
roomHeader.onClicked: roomDrawer.open() import Spectral.Effect 2.0
sortedMessageEventModel.onModelReset: { import Spectral 0.1
import Spectral.Setting 0.1
import SortFilterProxyModel 0.2
Item {
property var currentRoom: null
id: root
MessageEventModel {
id: messageEventModel
room: currentRoom
}
RoomDrawer {
width: Math.min(root.width * 0.7, 480)
height: root.height
id: roomDrawer
room: currentRoom
}
Label {
anchors.centerIn: parent
visible: !currentRoom
text: "Please choose a room."
}
Image {
anchors.fill: parent
visible: currentRoom && MSettings.enableTimelineBackground
source: MSettings.timelineBackground || MSettings.darkTheme ? "qrc:/assets/img/roompanel-dark.svg" : "qrc:/assets/img/roompanel.svg"
fillMode: Image.PreserveAspectCrop
}
ColumnLayout {
anchors.fill: parent
spacing: 0
visible: currentRoom
RoomHeader {
Layout.fillWidth: true
Layout.preferredHeight: 64
z: 10
id: roomHeader
avatar: currentRoom ? currentRoom.avatarMediaId : ""
topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : ""
atTop: messageListView.atYBeginning
onClicked: roomDrawer.open()
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: 960
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
Layout.alignment: Qt.AlignHCenter
spacing: 16
AutoListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: messageListView
spacing: 4
displayMarginBeginning: 100
displayMarginEnd: 100
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
boundsBehavior: Flickable.DragOverBounds
model: SortFilterProxyModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
filters: ExpressionFilter {
expression: marks !== 0x10 && eventType !== "other"
}
onModelReset: {
if (currentRoom) { if (currentRoom) {
var lastScrollPosition = sortedMessageEventModel.mapFromSource(currentRoom.savedTopVisibleIndex()) var lastScrollPosition = sortedMessageEventModel.mapFromSource(currentRoom.savedTopVisibleIndex())
messageListView.currentIndex = lastScrollPosition messageListView.currentIndex = lastScrollPosition
if (messageListView.contentY < messageListView.originY + 10 || currentRoom.timelineSize < 20) if (messageListView.contentY < messageListView.originY + 10 || currentRoom.timelineSize < 20)
currentRoom.getPreviousContent(100) currentRoom.getPreviousContent(50)
}
} }
} }
messageListView { property int largestVisibleIndex: count > 0 ? indexAt(contentX, contentY + height - 1) : -1
property int largestVisibleIndex: messageListView.count > 0 ? messageListView.indexAt(messageListView.contentX, messageListView.contentY + messageListView.height - 1) : -1
onContentYChanged: { onContentYChanged: {
if(currentRoom && messageListView.contentY - 5000 < messageListView.originY) if(currentRoom && contentY - 5000 < originY)
currentRoom.getPreviousContent(50); currentRoom.getPreviousContent(20);
} }
onMovementEnded: currentRoom.saveViewport(sortedMessageEventModel.mapToSource(messageListView.indexAt(messageListView.contentX, messageListView.contentY)), sortedMessageEventModel.mapToSource(largestVisibleIndex))
displaced: Transition { displaced: Transition {
NumberAnimation { NumberAnimation {
property: "y"; duration: 200 property: "y"; duration: 200
easing.type: Easing.OutQuad easing.type: Easing.OutQuad
} }
} }
delegate: DelegateChooser {
role: "eventType"
DelegateChoice {
roleValue: "state"
delegate: ColumnLayout {
width: messageListView.width
spacing: 4
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.margins: 16
visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000
} }
goBottomFab.onClicked: goToEvent(currentRoom.readMarkerEventId) StateDelegate {
goTopFab.onClicked: messageListView.positionViewAtBeginning() Layout.maximumWidth: parent.width
Layout.alignment: Qt.AlignHCenter
}
}
}
DelegateChoice {
roleValue: "emote"
delegate: ColumnLayout {
width: messageListView.width
spacing: 4
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.margins: 16
visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000
}
StateDelegate {
Layout.maximumWidth: parent.width
Layout.alignment: Qt.AlignHCenter
}
}
}
DelegateChoice {
roleValue: "message"
delegate: ColumnLayout {
width: messageListView.width
spacing: 4
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.margins: 16
visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000
}
MessageDelegate {
}
}
}
DelegateChoice {
roleValue: "notice"
delegate: ColumnLayout {
width: messageListView.width
spacing: 4
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.margins: 16
visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000
}
MessageDelegate {
}
}
}
DelegateChoice {
roleValue: "image"
delegate: ColumnLayout {
width: messageListView.width
spacing: 4
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.margins: 16
visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000
}
ImageDelegate {
Layout.maximumWidth: parent.width
}
}
}
DelegateChoice {
roleValue: "file"
delegate: ColumnLayout {
width: messageListView.width
spacing: 4
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.margins: 16
visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000
}
FileDelegate {
Layout.maximumWidth: parent.width
}
}
}
}
RoundButton {
width: 64
height: 64
anchors.right: parent.right
anchors.top: parent.top
id: goBottomFab
visible: currentRoom && currentRoom.hasUnreadMessages
contentItem: MaterialIcon {
anchors.fill: parent
icon: "\ue316"
color: "white"
}
Material.background: Material.accent
onClicked: goToEvent(currentRoom.readMarkerEventId)
}
RoundButton {
width: 64
height: 64
anchors.right: parent.right
anchors.bottom: parent.bottom
id: goTopFab
visible: !messageListView.atYEnd
contentItem: MaterialIcon {
anchors.fill: parent
icon: "\ue313"
color: "white"
}
Material.background: Material.accent
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 {
Layout.maximumWidth: parent.width * 0.8
visible: currentRoom && currentRoom.hasUsersTyping
padding: 8
contentItem: RowLayout {
spacing: 8
Repeater {
model: currentRoom && currentRoom.hasUsersTyping ? currentRoom.usersTyping : null
delegate: Avatar {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
source: modelData.avatarMediaId
hint: modelData.displayName
}
}
BusyIndicator {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
}
}
background: Rectangle {
color: MPalette.banner
radius: height / 2
}
}
RoomPanelInput {
Layout.fillWidth: true
id: roomPanelInput
}
}
}
function goToEvent(eventID) { function goToEvent(eventID) {
var index = messageEventModel.eventIDToIndex(eventID) var index = messageEventModel.eventIDToIndex(eventID)
if (index === -1) return if (index === -1) return
messageListView.currentIndex = -1 // messageListView.currentIndex = sortedMessageEventModel.mapFromSource(index)
messageListView.currentIndex = sortedMessageEventModel.mapFromSource(index) messageListView.positionViewAtIndex(sortedMessageEventModel.mapFromSource(index), ListView.Contain)
} }
function saveReadMarker(room) { function saveReadMarker(room) {
var readMarker = sortedMessageEventModel.get(messageListView.largestVisibleIndex).eventId var readMarker = sortedMessageEventModel.get(messageListView.largestVisibleIndex).eventId
if (!readMarker) return if (!readMarker) return
room.readMarkerEventId = readMarker room.readMarkerEventId = readMarker
currentRoom.saveViewport(sortedMessageEventModel.mapToSource(messageListView.indexAt(messageListView.contentX, messageListView.contentY)), sortedMessageEventModel.mapToSource(messageListView.largestVisibleIndex))
} }
} }

View File

@ -1,322 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Spectral.Component 2.0
import Spectral.Component.Emoji 2.0
import Spectral.Component.Timeline 2.0
import Spectral.Menu 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
import SortFilterProxyModel 0.2
import "qrc:/js/md.js" as Markdown
import "qrc:/js/util.js" as Util
Item {
property var currentRoom: null
property alias roomHeader: roomHeader
property alias messageListView: messageListView
property alias goTopFab: goTopFab
property alias goBottomFab: goBottomFab
property alias messageEventModel: messageEventModel
property alias sortedMessageEventModel: sortedMessageEventModel
property alias roomDrawer: roomDrawer
id: root
MessageEventModel {
id: messageEventModel
room: currentRoom
}
RoomDrawer {
width: Math.min(root.width * 0.7, 480)
height: root.height
id: roomDrawer
room: currentRoom
}
Label {
anchors.centerIn: parent
visible: !currentRoom
text: "Please choose a room."
}
ColumnLayout {
anchors.fill: parent
spacing: 0
visible: currentRoom
RoomHeader {
Layout.fillWidth: true
Layout.preferredHeight: 64
z: 10
id: roomHeader
}
AutoListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 16
Layout.rightMargin: 16
id: messageListView
displayMarginBeginning: 40
displayMarginEnd: 40
verticalLayoutDirection: ListView.BottomToTop
spacing: 8
boundsBehavior: Flickable.DragOverBounds
model: SortFilterProxyModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
filters: ExpressionFilter {
expression: marks !== 0x08 && marks !== 0x10
}
}
delegate: ColumnLayout {
width: parent.width
id: delegateColumn
spacing: 8
Label {
Layout.alignment: Qt.AlignHCenter
visible: section !== aboveSection
text: section
color: "white"
verticalAlignment: Text.AlignVCenter
leftPadding: 8
rightPadding: 8
topPadding: 4
bottomPadding: 4
background: Rectangle {
color: MSettings.darkTheme ? "#484848" : "grey"
}
}
MessageDelegate {
visible: eventType === "notice" || eventType === "message"
|| eventType === "image" || eventType === "video"
|| eventType === "audio" || eventType === "file"
}
StateDelegate {
Layout.maximumWidth: messageListView.width * 0.8
visible: eventType === "emote" || eventType === "state"
}
Label {
Layout.alignment: Qt.AlignHCenter
visible: eventType === "other"
text: display
color: "grey"
font.italic: true
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
visible: readMarker === true
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 2
color: Material.accent
}
Label {
text: "And Now"
color: Material.accent
verticalAlignment: Text.AlignVCenter
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 2
color: Material.accent
}
}
}
RoundButton {
width: 64
height: 64
anchors.right: parent.right
anchors.top: parent.top
id: goBottomFab
visible: currentRoom && currentRoom.hasUnreadMessages
contentItem: MaterialIcon {
anchors.fill: parent
icon: "\ue316"
color: "white"
}
Material.background: Material.accent
}
RoundButton {
width: 64
height: 64
anchors.right: parent.right
anchors.bottom: parent.bottom
id: goTopFab
visible: !messageListView.atYEnd
contentItem: MaterialIcon {
anchors.fill: parent
icon: "\ue313"
color: "white"
}
Material.background: Material.accent
}
MessageContextMenu {
id: messageContextMenu
}
Popup {
property string sourceText
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 480
id: sourceDialog
parent: ApplicationWindow.overlay
modal: true
padding: 16
closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside
contentItem: ScrollView {
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
ImageItem {
Layout.preferredWidth: height
Layout.fillHeight: true
source: modelData.paintable
hint: modelData.displayName
}
Label {
Layout.fillWidth: true
text: modelData.displayName
}
}
}
ScrollBar.vertical: ScrollBar {
}
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 40
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.leftMargin: 16
Layout.rightMargin: 16
color: Material.background
RoomPanelInput {
anchors.verticalCenter: parent.top
id: roomPanelInput
width: parent.width
height: 48
}
}
}
}
/*##^## Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -1,7 +1,7 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.12
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Component.Emoji 2.0 import Spectral.Component.Emoji 2.0
@ -10,88 +10,136 @@ import Spectral.Setting 0.1
import Spectral 0.1 import Spectral 0.1
import "qrc:/js/md.js" as Markdown Control {
property alias isReply: replyItem.visible
Rectangle { property var replyUser
property bool isReply
property string replyUserID
property string replyEventID property string replyEventID
property string replyContent property string replyContent
property bool isAutoCompleting property alias isAutoCompleting: autoCompleteListView.visible
property var autoCompleteModel property var autoCompleteModel
property int autoCompleteBeginPosition property int autoCompleteBeginPosition
property int autoCompleteEndPosition property int autoCompleteEndPosition
id: root
padding: 0
background: Rectangle {
color: MSettings.darkTheme ? "#303030" : "#fafafa" color: MSettings.darkTheme ? "#303030" : "#fafafa"
radius: 24
layer.enabled: true layer.enabled: true
layer.effect: ElevationEffect { layer.effect: ElevationEffect {
elevation: 2 elevation: 2
} }
}
Popup { contentItem: ColumnLayout {
x: 0 spacing: 0
y: -height - 10
width: Math.min(autoCompleteListView.contentWidth, parent.width)
height: 36
padding: 0
Material.elevation: 2 RowLayout {
Layout.fillWidth: true
Layout.margins: 8
id: autoComplete id: replyItem
visible: isAutoCompleting && autoCompleteModel.length !== 0 visible: false
spacing: 8
Avatar {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
source: replyUser ? replyUser.avatarMediaId : ""
hint: replyUser ? replyUser.displayName : "No name"
}
Label {
Layout.fillWidth: true
text: replyContent
font.pixelSize: 16
wrapMode: Label.Wrap
}
}
EmojiPicker {
Layout.fillWidth: true
id: emojiPicker
visible: false
textArea: inputField
emojiModel: EmojiModel { id: emojiModel }
}
ListView {
Layout.fillWidth: true
Layout.preferredHeight: 36
Layout.margins: 8
contentItem: ListView {
id: autoCompleteListView id: autoCompleteListView
visible: false
model: autoCompleteModel model: autoCompleteModel
clip: true clip: true
spacing: 4
orientation: ListView.Horizontal orientation: ListView.Horizontal
highlightFollowsCurrentItem: true highlightFollowsCurrentItem: true
keyNavigationWraps: true keyNavigationWraps: true
highlight: Rectangle { delegate: Control {
color: Material.accent
opacity: 0.4
}
delegate: ItemDelegate {
property string autoCompleteText: modelData.displayName || modelData.unicode property string autoCompleteText: modelData.displayName || modelData.unicode
property bool isEmoji: modelData.unicode != null property bool isEmoji: modelData.unicode != null
readonly property bool highlighted: autoCompleteListView.currentIndex === index
height: parent.height height: 36
padding: 4 padding: 8
background: Rectangle {
visible: !isEmoji
color: highlighted ? Material.accent : "transparent"
border.color: Material.accent
border.width: 2
radius: height / 2
}
contentItem: Row { contentItem: Row {
spacing: 8 spacing: 4
Text { Text {
width: parent.height width: 20
height: parent.height height: 20
visible: isEmoji visible: isEmoji
text: autoCompleteText text: autoCompleteText
font.pointSize: 16 font.pixelSize: 24
font.family: "Emoji" font.family: "Emoji"
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
ImageItem { Avatar {
width: parent.height width: 20
height: parent.height height: 20
visible: !isEmoji visible: !isEmoji
source: modelData.paintable || null source: modelData.avatarMediaId || null
} }
Label { Label {
height: parent.height height: parent.height
visible: !isEmoji visible: !isEmoji
text: autoCompleteText text: autoCompleteText
color: highlighted ? "white" : Material.accent
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
} }
MouseArea {
anchors.fill: parent
onClicked: { onClicked: {
autoCompleteListView.currentIndex = index autoCompleteListView.currentIndex = index
inputField.replaceAutoComplete(autoCompleteText) inputField.replaceAutoComplete(autoCompleteText)
@ -101,21 +149,25 @@ Rectangle {
} }
Rectangle { Rectangle {
width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0 Layout.fillWidth: true
height: parent.height Layout.preferredHeight: 1
Layout.leftMargin: 12
Layout.rightMargin: 12
opacity: 0.2 visible: emojiPicker.visible || replyItem.visible || autoCompleteListView.visible
color: Material.accent
color: MSettings.darkTheme ? "#424242" : "#e7ebeb"
} }
RowLayout { RowLayout {
anchors.fill: parent Layout.fillWidth: true
spacing: 0 spacing: 0
ItemDelegate { ToolButton {
Layout.preferredWidth: 48 Layout.preferredWidth: 48
Layout.preferredHeight: 48 Layout.preferredHeight: 48
Layout.alignment: Qt.AlignBottom
id: uploadButton id: uploadButton
visible: !isReply visible: !isReply
@ -133,11 +185,13 @@ Rectangle {
} }
} }
ItemDelegate { ToolButton {
Layout.preferredWidth: 48 Layout.preferredWidth: 48
Layout.preferredHeight: 48 Layout.preferredHeight: 48
Layout.alignment: Qt.AlignBottom
id: cancelReplyButton id: cancelReplyButton
visible: isReply visible: isReply
contentItem: MaterialIcon { contentItem: MaterialIcon {
@ -147,30 +201,31 @@ Rectangle {
onClicked: clearReply() onClicked: clearReply()
} }
ScrollView {
Layout.fillWidth: true
Layout.preferredHeight: 48
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
clip: true
TextArea { TextArea {
property real progress: 0 property real progress: 0
Layout.fillWidth: true
Layout.minimumHeight: 48
id: inputField id: inputField
wrapMode: Text.Wrap wrapMode: Text.Wrap
placeholderText: isReply ? "Reply to " + replyUserID : "Send a Message" placeholderText: "Send a Message"
leftPadding: 16
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
selectByMouse: true selectByMouse: true
verticalAlignment: TextEdit.AlignVCenter verticalAlignment: TextEdit.AlignVCenter
text: currentRoom ? currentRoom.cachedInput : "" text: currentRoom != null ? currentRoom.cachedInput : ""
background: Item { background: Item {}
Rectangle {
width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0
height: parent.height
opacity: 0.2
color: Material.accent
} }
Timer { Timer {
@ -193,19 +248,18 @@ Rectangle {
onTriggered: currentRoom.sendTypingNotification(true) onTriggered: currentRoom.sendTypingNotification(true)
} }
ToolTip.visible: currentRoom
&& currentRoom.hasUsersTyping
ToolTip.text: currentRoom ? currentRoom.usersTyping : ""
Keys.onReturnPressed: { Keys.onReturnPressed: {
if (event.modifiers & Qt.ShiftModifier) { if (event.modifiers & Qt.ShiftModifier) {
insert(cursorPosition, "\n") insert(cursorPosition, "\n")
} else { } else if (text) {
postMessage(text) postMessage(text)
text = "" text = ""
closeAll()
} }
} }
Keys.onEscapePressed: closeAll()
Keys.onBacktabPressed: if (isAutoCompleting) autoCompleteListView.decrementCurrentIndex() Keys.onBacktabPressed: if (isAutoCompleting) autoCompleteListView.decrementCurrentIndex()
Keys.onTabPressed: { Keys.onTabPressed: {
@ -259,8 +313,7 @@ Rectangle {
var PREFIX_MARKDOWN = '/md ' var PREFIX_MARKDOWN = '/md '
if (isReply) { if (isReply) {
currentRoom.sendReply(replyUserID, replyEventID, replyContent, text) currentRoom.sendReply(replyUser.id, replyEventID, replyContent, text)
clearReply()
return return
} }
@ -294,19 +347,18 @@ Rectangle {
} }
if (text.indexOf(PREFIX_MARKDOWN) === 0) { if (text.indexOf(PREFIX_MARKDOWN) === 0) {
text = text.substr(PREFIX_MARKDOWN.length) text = text.substr(PREFIX_MARKDOWN.length)
var parsedText = Markdown.markdown_parser(text) currentRoom.postMarkdownText(text)
currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text)
return return
} }
currentRoom.postPlainText(text) currentRoom.postPlainText(text)
} }
} }
}
ItemDelegate { ToolButton {
Layout.preferredWidth: 48 Layout.preferredWidth: 48
Layout.preferredHeight: 48 Layout.preferredHeight: 48
Layout.alignment: Qt.AlignBottom
id: emojiButton id: emojiButton
@ -314,24 +366,7 @@ Rectangle {
icon: "\ue24e" icon: "\ue24e"
} }
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.open() onClicked: emojiPicker.visible = !emojiPicker.visible
EmojiPicker {
x: -width + parent.width
y: -height - 16
width: 360
height: 320
id: emojiPicker
emojiModel: EmojiModel {
id: emojiModel
}
Material.elevation: 2
textArea: inputField
} }
} }
} }
@ -346,8 +381,18 @@ Rectangle {
function clearReply() { function clearReply() {
isReply = false isReply = false
replyUserID = "" replyUser = null
replyEventID = "" replyEventID = ""
replyContent = "" replyContent = ""
} }
function focus() {
inputField.forceActiveFocus()
}
function closeAll() {
replyItem.visible = false
autoCompleteListView.visible = false
emojiPicker.visible = false
}
} }

View File

@ -0,0 +1,14 @@
pragma Singleton
import QtQuick 2.12
import QtQuick.Controls.Material 2.12
QtObject {
readonly property int theme: MSettings.darkTheme ? Material.Dark : Material.Light
readonly property color primary: "#344955"
readonly property color accent: "#673AB7"
readonly property color foreground: MSettings.darkTheme ? "#FFFFFF" : "#1D333E"
readonly property color background: MSettings.darkTheme ? "#303030" : "#FFFFFF"
readonly property color lighter: MSettings.darkTheme ? "#FFFFFF" : "#5B7480"
readonly property color banner: MSettings.darkTheme ? "#404040" : "#F2F3F4"
}

View File

@ -1,11 +1,15 @@
pragma Singleton pragma Singleton
import QtQuick 2.9 import QtQuick 2.12
import Qt.labs.settings 1.0 import Qt.labs.settings 1.0
Settings { Settings {
property bool showNotification: true
property bool pressAndHold property bool pressAndHold
property bool showTray: true property bool showTray: true
property bool confirmOnExit: true
property bool darkTheme property bool darkTheme
property bool enableTimelineBackground: true
property string timelineBackground
} }

View File

@ -1,3 +1,3 @@
module Spectral.Setting module Spectral.Setting
singleton MSettings 0.1 Setting.qml singleton MSettings 0.1 Setting.qml
singleton MPalette 0.1 Palette.qml

281
include/hoedown/autolink.c Normal file
View File

@ -0,0 +1,281 @@
#include "autolink.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#ifndef _MSC_VER
#include <strings.h>
#else
#define strncasecmp _strnicmp
#endif
int
hoedown_autolink_is_safe(const uint8_t *data, size_t size)
{
static const size_t valid_uris_count = 6;
static const char *valid_uris[] = {
"http://", "https://", "/", "#", "ftp://", "mailto:"
};
static const size_t valid_uris_size[] = { 7, 8, 1, 1, 6, 7 };
size_t i;
for (i = 0; i < valid_uris_count; ++i) {
size_t len = valid_uris_size[i];
if (size > len &&
strncasecmp((char *)data, valid_uris[i], len) == 0 &&
isalnum(data[len]))
return 1;
}
return 0;
}
static size_t
autolink_delim(uint8_t *data, size_t link_end, size_t max_rewind, size_t size)
{
uint8_t cclose, copen = 0;
size_t i;
for (i = 0; i < link_end; ++i)
if (data[i] == '<') {
link_end = i;
break;
}
while (link_end > 0) {
if (strchr("?!.,:", data[link_end - 1]) != NULL)
link_end--;
else if (data[link_end - 1] == ';') {
size_t new_end = link_end - 2;
while (new_end > 0 && isalpha(data[new_end]))
new_end--;
if (new_end < link_end - 2 && data[new_end] == '&')
link_end = new_end;
else
link_end--;
}
else break;
}
if (link_end == 0)
return 0;
cclose = data[link_end - 1];
switch (cclose) {
case '"': copen = '"'; break;
case '\'': copen = '\''; break;
case ')': copen = '('; break;
case ']': copen = '['; break;
case '}': copen = '{'; break;
}
if (copen != 0) {
size_t closing = 0;
size_t opening = 0;
size_t i = 0;
/* Try to close the final punctuation sign in this same line;
* if we managed to close it outside of the URL, that means that it's
* not part of the URL. If it closes inside the URL, that means it
* is part of the URL.
*
* Examples:
*
* foo http://www.pokemon.com/Pikachu_(Electric) bar
* => http://www.pokemon.com/Pikachu_(Electric)
*
* foo (http://www.pokemon.com/Pikachu_(Electric)) bar
* => http://www.pokemon.com/Pikachu_(Electric)
*
* foo http://www.pokemon.com/Pikachu_(Electric)) bar
* => http://www.pokemon.com/Pikachu_(Electric))
*
* (foo http://www.pokemon.com/Pikachu_(Electric)) bar
* => foo http://www.pokemon.com/Pikachu_(Electric)
*/
while (i < link_end) {
if (data[i] == copen)
opening++;
else if (data[i] == cclose)
closing++;
i++;
}
if (closing != opening)
link_end--;
}
return link_end;
}
static size_t
check_domain(uint8_t *data, size_t size, int allow_short)
{
size_t i, np = 0;
if (!isalnum(data[0]))
return 0;
for (i = 1; i < size - 1; ++i) {
if (strchr(".:", data[i]) != NULL) np++;
else if (!isalnum(data[i]) && data[i] != '-') break;
}
if (allow_short) {
/* We don't need a valid domain in the strict sense (with
* least one dot; so just make sure it's composed of valid
* domain characters and return the length of the the valid
* sequence. */
return i;
} else {
/* a valid domain needs to have at least a dot.
* that's as far as we get */
return np ? i : 0;
}
}
size_t
hoedown_autolink__www(
size_t *rewind_p,
hoedown_buffer *link,
uint8_t *data,
size_t max_rewind,
size_t size,
unsigned int flags)
{
size_t link_end;
if (max_rewind > 0 && !ispunct(data[-1]) && !isspace(data[-1]))
return 0;
if (size < 4 || memcmp(data, "www.", strlen("www.")) != 0)
return 0;
link_end = check_domain(data, size, 0);
if (link_end == 0)
return 0;
while (link_end < size && !isspace(data[link_end]))
link_end++;
link_end = autolink_delim(data, link_end, max_rewind, size);
if (link_end == 0)
return 0;
hoedown_buffer_put(link, data, link_end);
*rewind_p = 0;
return (int)link_end;
}
size_t
hoedown_autolink__email(
size_t *rewind_p,
hoedown_buffer *link,
uint8_t *data,
size_t max_rewind,
size_t size,
unsigned int flags)
{
size_t link_end, rewind;
int nb = 0, np = 0;
for (rewind = 0; rewind < max_rewind; ++rewind) {
uint8_t c = data[-1 - rewind];
if (isalnum(c))
continue;
if (strchr(".+-_", c) != NULL)
continue;
break;
}
if (rewind == 0)
return 0;
for (link_end = 0; link_end < size; ++link_end) {
uint8_t c = data[link_end];
if (isalnum(c))
continue;
if (c == '@')
nb++;
else if (c == '.' && link_end < size - 1)
np++;
else if (c != '-' && c != '_')
break;
}
if (link_end < 2 || nb != 1 || np == 0 ||
!isalpha(data[link_end - 1]))
return 0;
link_end = autolink_delim(data, link_end, max_rewind, size);
if (link_end == 0)
return 0;
hoedown_buffer_put(link, data - rewind, link_end + rewind);
*rewind_p = rewind;
return link_end;
}
size_t
hoedown_autolink__url(
size_t *rewind_p,
hoedown_buffer *link,
uint8_t *data,
size_t max_rewind,
size_t size,
unsigned int flags)
{
size_t link_end, rewind = 0, domain_len;
if (size < 4 || data[1] != '/' || data[2] != '/')
return 0;
while (rewind < max_rewind && isalpha(data[-1 - rewind]))
rewind++;
if (!hoedown_autolink_is_safe(data - rewind, size + rewind))
return 0;
link_end = strlen("://");
domain_len = check_domain(
data + link_end,
size - link_end,
flags & HOEDOWN_AUTOLINK_SHORT_DOMAINS);
if (domain_len == 0)
return 0;
link_end += domain_len;
while (link_end < size && !isspace(data[link_end]))
link_end++;
link_end = autolink_delim(data, link_end, max_rewind, size);
if (link_end == 0)
return 0;
hoedown_buffer_put(link, data - rewind, link_end + rewind);
*rewind_p = rewind;
return link_end;
}

View File

@ -0,0 +1,46 @@
/* autolink.h - versatile autolinker */
#ifndef HOEDOWN_AUTOLINK_H
#define HOEDOWN_AUTOLINK_H
#include "buffer.h"
#ifdef __cplusplus
extern "C" {
#endif
/*************
* CONSTANTS *
*************/
typedef enum hoedown_autolink_flags {
HOEDOWN_AUTOLINK_SHORT_DOMAINS = (1 << 0)
} hoedown_autolink_flags;
/*************
* FUNCTIONS *
*************/
/* hoedown_autolink_is_safe: verify that a URL has a safe protocol */
int hoedown_autolink_is_safe(const uint8_t *data, size_t size);
/* hoedown_autolink__www: search for the next www link in data */
size_t hoedown_autolink__www(size_t *rewind_p, hoedown_buffer *link,
uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags);
/* hoedown_autolink__email: search for the next email in data */
size_t hoedown_autolink__email(size_t *rewind_p, hoedown_buffer *link,
uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags);
/* hoedown_autolink__url: search for the next URL in data */
size_t hoedown_autolink__url(size_t *rewind_p, hoedown_buffer *link,
uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags);
#ifdef __cplusplus
}
#endif
#endif /** HOEDOWN_AUTOLINK_H **/

308
include/hoedown/buffer.c Normal file
View File

@ -0,0 +1,308 @@
#include "buffer.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
void *
hoedown_malloc(size_t size)
{
void *ret = malloc(size);
if (!ret) {
fprintf(stderr, "Allocation failed.\n");
abort();
}
return ret;
}
void *
hoedown_calloc(size_t nmemb, size_t size)
{
void *ret = calloc(nmemb, size);
if (!ret) {
fprintf(stderr, "Allocation failed.\n");
abort();
}
return ret;
}
void *
hoedown_realloc(void *ptr, size_t size)
{
void *ret = realloc(ptr, size);
if (!ret) {
fprintf(stderr, "Allocation failed.\n");
abort();
}
return ret;
}
void
hoedown_buffer_init(
hoedown_buffer *buf,
size_t unit,
hoedown_realloc_callback data_realloc,
hoedown_free_callback data_free,
hoedown_free_callback buffer_free)
{
assert(buf);
buf->data = NULL;
buf->size = buf->asize = 0;
buf->unit = unit;
buf->data_realloc = data_realloc;
buf->data_free = data_free;
buf->buffer_free = buffer_free;
}
void
hoedown_buffer_uninit(hoedown_buffer *buf)
{
assert(buf && buf->unit);
buf->data_free(buf->data);
}
hoedown_buffer *
hoedown_buffer_new(size_t unit)
{
hoedown_buffer *ret = hoedown_malloc(sizeof (hoedown_buffer));
hoedown_buffer_init(ret, unit, hoedown_realloc, free, free);
return ret;
}
void
hoedown_buffer_free(hoedown_buffer *buf)
{
if (!buf) return;
assert(buf && buf->unit);
buf->data_free(buf->data);
if (buf->buffer_free)
buf->buffer_free(buf);
}
void
hoedown_buffer_reset(hoedown_buffer *buf)
{
assert(buf && buf->unit);
buf->data_free(buf->data);
buf->data = NULL;
buf->size = buf->asize = 0;
}
void
hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz)
{
size_t neoasz;
assert(buf && buf->unit);
if (buf->asize >= neosz)
return;
neoasz = buf->asize + buf->unit;
while (neoasz < neosz)
neoasz += buf->unit;
buf->data = buf->data_realloc(buf->data, neoasz);
buf->asize = neoasz;
}
void
hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size)
{
assert(buf && buf->unit);
if (buf->size + size > buf->asize)
hoedown_buffer_grow(buf, buf->size + size);
memcpy(buf->data + buf->size, data, size);
buf->size += size;
}
void
hoedown_buffer_puts(hoedown_buffer *buf, const char *str)
{
hoedown_buffer_put(buf, (const uint8_t *)str, strlen(str));
}
void
hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c)
{
assert(buf && buf->unit);
if (buf->size >= buf->asize)
hoedown_buffer_grow(buf, buf->size + 1);
buf->data[buf->size] = c;
buf->size += 1;
}
int
hoedown_buffer_putf(hoedown_buffer *buf, FILE *file)
{
assert(buf && buf->unit);
while (!(feof(file) || ferror(file))) {
hoedown_buffer_grow(buf, buf->size + buf->unit);
buf->size += fread(buf->data + buf->size, 1, buf->unit, file);
}
return ferror(file);
}
void
hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size)
{
assert(buf && buf->unit);
if (size > buf->asize)
hoedown_buffer_grow(buf, size);
memcpy(buf->data, data, size);
buf->size = size;
}
void
hoedown_buffer_sets(hoedown_buffer *buf, const char *str)
{
hoedown_buffer_set(buf, (const uint8_t *)str, strlen(str));
}
int
hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size)
{
if (buf->size != size) return 0;
return memcmp(buf->data, data, size) == 0;
}
int
hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str)
{
return hoedown_buffer_eq(buf, (const uint8_t *)str, strlen(str));
}
int
hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix)
{
size_t i;
for (i = 0; i < buf->size; ++i) {
if (prefix[i] == 0)
return 0;
if (buf->data[i] != prefix[i])
return buf->data[i] - prefix[i];
}
return 0;
}
void
hoedown_buffer_slurp(hoedown_buffer *buf, size_t size)
{
assert(buf && buf->unit);
if (size >= buf->size) {
buf->size = 0;
return;
}
buf->size -= size;
memmove(buf->data, buf->data + size, buf->size);
}
const char *
hoedown_buffer_cstr(hoedown_buffer *buf)
{
assert(buf && buf->unit);
if (buf->size < buf->asize && buf->data[buf->size] == 0)
return (char *)buf->data;
hoedown_buffer_grow(buf, buf->size + 1);
buf->data[buf->size] = 0;
return (char *)buf->data;
}
void
hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...)
{
va_list ap;
int n;
assert(buf && buf->unit);
if (buf->size >= buf->asize)
hoedown_buffer_grow(buf, buf->size + 1);
va_start(ap, fmt);
n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap);
va_end(ap);
if (n < 0) {
#ifndef _MSC_VER
return;
#else
va_start(ap, fmt);
n = _vscprintf(fmt, ap);
va_end(ap);
#endif
}
if ((size_t)n >= buf->asize - buf->size) {
hoedown_buffer_grow(buf, buf->size + n + 1);
va_start(ap, fmt);
n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap);
va_end(ap);
}
if (n < 0)
return;
buf->size += n;
}
void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int c) {
unsigned char unichar[4];
assert(buf && buf->unit);
if (c < 0x80) {
hoedown_buffer_putc(buf, c);
}
else if (c < 0x800) {
unichar[0] = 192 + (c / 64);
unichar[1] = 128 + (c % 64);
hoedown_buffer_put(buf, unichar, 2);
}
else if (c - 0xd800u < 0x800) {
HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd");
}
else if (c < 0x10000) {
unichar[0] = 224 + (c / 4096);
unichar[1] = 128 + (c / 64) % 64;
unichar[2] = 128 + (c % 64);
hoedown_buffer_put(buf, unichar, 3);
}
else if (c < 0x110000) {
unichar[0] = 240 + (c / 262144);
unichar[1] = 128 + (c / 4096) % 64;
unichar[2] = 128 + (c / 64) % 64;
unichar[3] = 128 + (c % 64);
hoedown_buffer_put(buf, unichar, 4);
}
else {
HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd");
}
}

134
include/hoedown/buffer.h Normal file
View File

@ -0,0 +1,134 @@
/* buffer.h - simple, fast buffers */
#ifndef HOEDOWN_BUFFER_H
#define HOEDOWN_BUFFER_H
#include <stdio.h>
#include <stddef.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdlib.h>
#ifdef __cplusplus
extern "C" {
#endif
#if defined(_MSC_VER)
#define __attribute__(x)
#define inline __inline
#define __builtin_expect(x,n) x
#endif
/*********
* TYPES *
*********/
typedef void *(*hoedown_realloc_callback)(void *, size_t);
typedef void (*hoedown_free_callback)(void *);
struct hoedown_buffer {
uint8_t *data; /* actual character data */
size_t size; /* size of the string */
size_t asize; /* allocated size (0 = volatile buffer) */
size_t unit; /* reallocation unit size (0 = read-only buffer) */
hoedown_realloc_callback data_realloc;
hoedown_free_callback data_free;
hoedown_free_callback buffer_free;
};
typedef struct hoedown_buffer hoedown_buffer;
/*************
* FUNCTIONS *
*************/
/* allocation wrappers */
void *hoedown_malloc(size_t size) __attribute__ ((malloc));
void *hoedown_calloc(size_t nmemb, size_t size) __attribute__ ((malloc));
void *hoedown_realloc(void *ptr, size_t size) __attribute__ ((malloc));
/* hoedown_buffer_init: initialize a buffer with custom allocators */
void hoedown_buffer_init(
hoedown_buffer *buffer,
size_t unit,
hoedown_realloc_callback data_realloc,
hoedown_free_callback data_free,
hoedown_free_callback buffer_free
);
/* hoedown_buffer_uninit: uninitialize an existing buffer */
void hoedown_buffer_uninit(hoedown_buffer *buf);
/* hoedown_buffer_new: allocate a new buffer */
hoedown_buffer *hoedown_buffer_new(size_t unit) __attribute__ ((malloc));
/* hoedown_buffer_reset: free internal data of the buffer */
void hoedown_buffer_reset(hoedown_buffer *buf);
/* hoedown_buffer_grow: increase the allocated size to the given value */
void hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz);
/* hoedown_buffer_put: append raw data to a buffer */
void hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size);
/* hoedown_buffer_puts: append a NUL-terminated string to a buffer */
void hoedown_buffer_puts(hoedown_buffer *buf, const char *str);
/* hoedown_buffer_putc: append a single char to a buffer */
void hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c);
/* hoedown_buffer_putf: read from a file and append to a buffer, until EOF or error */
int hoedown_buffer_putf(hoedown_buffer *buf, FILE* file);
/* hoedown_buffer_set: replace the buffer's contents with raw data */
void hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size);
/* hoedown_buffer_sets: replace the buffer's contents with a NUL-terminated string */
void hoedown_buffer_sets(hoedown_buffer *buf, const char *str);
/* hoedown_buffer_eq: compare a buffer's data with other data for equality */
int hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size);
/* hoedown_buffer_eq: compare a buffer's data with NUL-terminated string for equality */
int hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str);
/* hoedown_buffer_prefix: compare the beginning of a buffer with a string */
int hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix);
/* hoedown_buffer_slurp: remove a given number of bytes from the head of the buffer */
void hoedown_buffer_slurp(hoedown_buffer *buf, size_t size);
/* hoedown_buffer_cstr: NUL-termination of the string array (making a C-string) */
const char *hoedown_buffer_cstr(hoedown_buffer *buf);
/* hoedown_buffer_printf: formatted printing to a buffer */
void hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...) __attribute__ ((format (printf, 2, 3)));
/* hoedown_buffer_put_utf8: put a Unicode character encoded as UTF-8 */
void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int codepoint);
/* hoedown_buffer_free: free the buffer */
void hoedown_buffer_free(hoedown_buffer *buf);
/* HOEDOWN_BUFPUTSL: optimized hoedown_buffer_puts of a string literal */
#define HOEDOWN_BUFPUTSL(output, literal) \
hoedown_buffer_put(output, (const uint8_t *)literal, sizeof(literal) - 1)
/* HOEDOWN_BUFSETSL: optimized hoedown_buffer_sets of a string literal */
#define HOEDOWN_BUFSETSL(output, literal) \
hoedown_buffer_set(output, (const uint8_t *)literal, sizeof(literal) - 1)
/* HOEDOWN_BUFEQSL: optimized hoedown_buffer_eqs of a string literal */
#define HOEDOWN_BUFEQSL(output, literal) \
hoedown_buffer_eq(output, (const uint8_t *)literal, sizeof(literal) - 1)
#ifdef __cplusplus
}
#endif
#endif /** HOEDOWN_BUFFER_H **/

3012
include/hoedown/document.c Normal file

File diff suppressed because it is too large Load Diff

172
include/hoedown/document.h Normal file
View File

@ -0,0 +1,172 @@
/* document.h - generic markdown parser */
#ifndef HOEDOWN_DOCUMENT_H
#define HOEDOWN_DOCUMENT_H
#include "buffer.h"
#include "autolink.h"
#ifdef __cplusplus
extern "C" {
#endif
/*************
* CONSTANTS *
*************/
typedef enum hoedown_extensions {
/* block-level extensions */
HOEDOWN_EXT_TABLES = (1 << 0),
HOEDOWN_EXT_FENCED_CODE = (1 << 1),
HOEDOWN_EXT_FOOTNOTES = (1 << 2),
/* span-level extensions */
HOEDOWN_EXT_AUTOLINK = (1 << 3),
HOEDOWN_EXT_STRIKETHROUGH = (1 << 4),
HOEDOWN_EXT_UNDERLINE = (1 << 5),
HOEDOWN_EXT_HIGHLIGHT = (1 << 6),
HOEDOWN_EXT_QUOTE = (1 << 7),
HOEDOWN_EXT_SUPERSCRIPT = (1 << 8),
HOEDOWN_EXT_MATH = (1 << 9),
/* other flags */
HOEDOWN_EXT_NO_INTRA_EMPHASIS = (1 << 11),
HOEDOWN_EXT_SPACE_HEADERS = (1 << 12),
HOEDOWN_EXT_MATH_EXPLICIT = (1 << 13),
/* negative flags */
HOEDOWN_EXT_DISABLE_INDENTED_CODE = (1 << 14)
} hoedown_extensions;
#define HOEDOWN_EXT_BLOCK (\
HOEDOWN_EXT_TABLES |\
HOEDOWN_EXT_FENCED_CODE |\
HOEDOWN_EXT_FOOTNOTES )
#define HOEDOWN_EXT_SPAN (\
HOEDOWN_EXT_AUTOLINK |\
HOEDOWN_EXT_STRIKETHROUGH |\
HOEDOWN_EXT_UNDERLINE |\
HOEDOWN_EXT_HIGHLIGHT |\
HOEDOWN_EXT_QUOTE |\
HOEDOWN_EXT_SUPERSCRIPT |\
HOEDOWN_EXT_MATH )
#define HOEDOWN_EXT_FLAGS (\
HOEDOWN_EXT_NO_INTRA_EMPHASIS |\
HOEDOWN_EXT_SPACE_HEADERS |\
HOEDOWN_EXT_MATH_EXPLICIT )
#define HOEDOWN_EXT_NEGATIVE (\
HOEDOWN_EXT_DISABLE_INDENTED_CODE )
typedef enum hoedown_list_flags {
HOEDOWN_LIST_ORDERED = (1 << 0),
HOEDOWN_LI_BLOCK = (1 << 1) /* <li> containing block data */
} hoedown_list_flags;
typedef enum hoedown_table_flags {
HOEDOWN_TABLE_ALIGN_LEFT = 1,
HOEDOWN_TABLE_ALIGN_RIGHT = 2,
HOEDOWN_TABLE_ALIGN_CENTER = 3,
HOEDOWN_TABLE_ALIGNMASK = 3,
HOEDOWN_TABLE_HEADER = 4
} hoedown_table_flags;
typedef enum hoedown_autolink_type {
HOEDOWN_AUTOLINK_NONE, /* used internally when it is not an autolink*/
HOEDOWN_AUTOLINK_NORMAL, /* normal http/http/ftp/mailto/etc link */
HOEDOWN_AUTOLINK_EMAIL /* e-mail link without explit mailto: */
} hoedown_autolink_type;
/*********
* TYPES *
*********/
struct hoedown_document;
typedef struct hoedown_document hoedown_document;
struct hoedown_renderer_data {
void *opaque;
};
typedef struct hoedown_renderer_data hoedown_renderer_data;
/* hoedown_renderer - functions for rendering parsed data */
struct hoedown_renderer {
/* state object */
void *opaque;
/* block level callbacks - NULL skips the block */
void (*blockcode)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data);
void (*blockquote)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
void (*header)(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data);
void (*hrule)(hoedown_buffer *ob, const hoedown_renderer_data *data);
void (*list)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data);
void (*listitem)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data);
void (*paragraph)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
void (*table)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
void (*table_header)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
void (*table_body)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
void (*table_row)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
void (*table_cell)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data);
void (*footnotes)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
void (*footnote_def)(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data);
void (*blockhtml)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data);
/* span level callbacks - NULL or return 0 prints the span verbatim */
int (*autolink)(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data);
int (*codespan)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data);
int (*double_emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
int (*emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
int (*underline)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
int (*highlight)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
int (*quote)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
int (*image)(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data);
int (*linebreak)(hoedown_buffer *ob, const hoedown_renderer_data *data);
int (*link)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data);
int (*triple_emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
int (*strikethrough)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
int (*superscript)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
int (*footnote_ref)(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data);
int (*math)(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data);
int (*raw_html)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data);
/* low level callbacks - NULL copies input directly into the output */
void (*entity)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data);
void (*normal_text)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data);
/* miscellaneous callbacks */
void (*doc_header)(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data);
void (*doc_footer)(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data);
};
typedef struct hoedown_renderer hoedown_renderer;
/*************
* FUNCTIONS *
*************/
/* hoedown_document_new: allocate a new document processor instance */
hoedown_document *hoedown_document_new(
const hoedown_renderer *renderer,
hoedown_extensions extensions,
size_t max_nesting
) __attribute__ ((malloc));
/* hoedown_document_render: render regular Markdown using the document processor */
void hoedown_document_render(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size);
/* hoedown_document_render_inline: render inline Markdown using the document processor */
void hoedown_document_render_inline(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size);
/* hoedown_document_free: deallocate a document processor instance */
void hoedown_document_free(hoedown_document *doc);
#ifdef __cplusplus
}
#endif
#endif /** HOEDOWN_DOCUMENT_H **/

188
include/hoedown/escape.c Normal file
View File

@ -0,0 +1,188 @@
#include "escape.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
/*
* The following characters will not be escaped:
*
* -_.+!*'(),%#@?=;:/,+&$ alphanum
*
* Note that this character set is the addition of:
*
* - The characters which are safe to be in an URL
* - The characters which are *not* safe to be in
* an URL because they are RESERVED characters.
*
* We assume (lazily) that any RESERVED char that
* appears inside an URL is actually meant to
* have its native function (i.e. as an URL
* component/separator) and hence needs no escaping.
*
* There are two exceptions: the chacters & (amp)
* and ' (single quote) do not appear in the table.
* They are meant to appear in the URL as components,
* yet they require special HTML-entity escaping
* to generate valid HTML markup.
*
* All other characters will be escaped to %XX.
*
*/
static const uint8_t HREF_SAFE[UINT8_MAX+1] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
void
hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size)
{
static const char hex_chars[] = "0123456789ABCDEF";
size_t i = 0, mark;
char hex_str[3];
hex_str[0] = '%';
while (i < size) {
mark = i;
while (i < size && HREF_SAFE[data[i]]) i++;
/* Optimization for cases where there's nothing to escape */
if (mark == 0 && i >= size) {
hoedown_buffer_put(ob, data, size);
return;
}
if (likely(i > mark)) {
hoedown_buffer_put(ob, data + mark, i - mark);
}
/* escaping */
if (i >= size)
break;
switch (data[i]) {
/* amp appears all the time in URLs, but needs
* HTML-entity escaping to be inside an href */
case '&':
HOEDOWN_BUFPUTSL(ob, "&amp;");
break;
/* the single quote is a valid URL character
* according to the standard; it needs HTML
* entity escaping too */
case '\'':
HOEDOWN_BUFPUTSL(ob, "&#x27;");
break;
/* the space can be escaped to %20 or a plus
* sign. we're going with the generic escape
* for now. the plus thing is more commonly seen
* when building GET strings */
#if 0
case ' ':
hoedown_buffer_putc(ob, '+');
break;
#endif
/* every other character goes with a %XX escaping */
default:
hex_str[1] = hex_chars[(data[i] >> 4) & 0xF];
hex_str[2] = hex_chars[data[i] & 0xF];
hoedown_buffer_put(ob, (uint8_t *)hex_str, 3);
}
i++;
}
}
/**
* According to the OWASP rules:
*
* & --> &amp;
* < --> &lt;
* > --> &gt;
* " --> &quot;
* ' --> &#x27; &apos; is not recommended
* / --> &#x2F; forward slash is included as it helps end an HTML entity
*
*/
static const uint8_t HTML_ESCAPE_TABLE[UINT8_MAX+1] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 4,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
static const char *HTML_ESCAPES[] = {
"",
"&quot;",
"&amp;",
"&#39;",
"&#47;",
"&lt;",
"&gt;"
};
void
hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure)
{
size_t i = 0, mark;
while (1) {
mark = i;
while (i < size && HTML_ESCAPE_TABLE[data[i]] == 0) i++;
/* Optimization for cases where there's nothing to escape */
if (mark == 0 && i >= size) {
hoedown_buffer_put(ob, data, size);
return;
}
if (likely(i > mark))
hoedown_buffer_put(ob, data + mark, i - mark);
if (i >= size) break;
/* The forward slash is only escaped in secure mode */
if (!secure && data[i] == '/') {
hoedown_buffer_putc(ob, '/');
} else {
hoedown_buffer_puts(ob, HTML_ESCAPES[HTML_ESCAPE_TABLE[data[i]]]);
}
i++;
}
}

28
include/hoedown/escape.h Normal file
View File

@ -0,0 +1,28 @@
/* escape.h - escape utilities */
#ifndef HOEDOWN_ESCAPE_H
#define HOEDOWN_ESCAPE_H
#include "buffer.h"
#ifdef __cplusplus
extern "C" {
#endif
/*************
* FUNCTIONS *
*************/
/* hoedown_escape_href: escape (part of) a URL inside HTML */
void hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size);
/* hoedown_escape_html: escape HTML */
void hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure);
#ifdef __cplusplus
}
#endif
#endif /** HOEDOWN_ESCAPE_H **/

754
include/hoedown/html.c Normal file
View File

@ -0,0 +1,754 @@
#include "html.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include "escape.h"
#define USE_XHTML(opt) (opt->flags & HOEDOWN_HTML_USE_XHTML)
hoedown_html_tag
hoedown_html_is_tag(const uint8_t *data, size_t size, const char *tagname)
{
size_t i;
int closed = 0;
if (size < 3 || data[0] != '<')
return HOEDOWN_HTML_TAG_NONE;
i = 1;
if (data[i] == '/') {
closed = 1;
i++;
}
for (; i < size; ++i, ++tagname) {
if (*tagname == 0)
break;
if (data[i] != *tagname)
return HOEDOWN_HTML_TAG_NONE;
}
if (i == size)
return HOEDOWN_HTML_TAG_NONE;
if (isspace(data[i]) || data[i] == '>')
return closed ? HOEDOWN_HTML_TAG_CLOSE : HOEDOWN_HTML_TAG_OPEN;
return HOEDOWN_HTML_TAG_NONE;
}
static void escape_html(hoedown_buffer *ob, const uint8_t *source, size_t length)
{
hoedown_escape_html(ob, source, length, 0);
}
static void escape_href(hoedown_buffer *ob, const uint8_t *source, size_t length)
{
hoedown_escape_href(ob, source, length);
}
/********************
* GENERIC RENDERER *
********************/
static int
rndr_autolink(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state = data->opaque;
if (!link || !link->size)
return 0;
HOEDOWN_BUFPUTSL(ob, "<a href=\"");
if (type == HOEDOWN_AUTOLINK_EMAIL)
HOEDOWN_BUFPUTSL(ob, "mailto:");
escape_href(ob, link->data, link->size);
if (state->link_attributes) {
hoedown_buffer_putc(ob, '\"');
state->link_attributes(ob, link, data);
hoedown_buffer_putc(ob, '>');
} else {
HOEDOWN_BUFPUTSL(ob, "\">");
}
/*
* Pretty printing: if we get an email address as
* an actual URI, e.g. `mailto:foo@bar.com`, we don't
* want to print the `mailto:` prefix
*/
if (hoedown_buffer_prefix(link, "mailto:") == 0) {
escape_html(ob, link->data + 7, link->size - 7);
} else {
escape_html(ob, link->data, link->size);
}
HOEDOWN_BUFPUTSL(ob, "</a>");
return 1;
}
static void
rndr_blockcode(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data)
{
if (ob->size) hoedown_buffer_putc(ob, '\n');
if (lang) {
HOEDOWN_BUFPUTSL(ob, "<pre><code class=\"language-");
escape_html(ob, lang->data, lang->size);
HOEDOWN_BUFPUTSL(ob, "\">");
} else {
HOEDOWN_BUFPUTSL(ob, "<pre><code>");
}
if (text)
escape_html(ob, text->data, text->size);
HOEDOWN_BUFPUTSL(ob, "</code></pre>\n");
}
static void
rndr_blockquote(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (ob->size) hoedown_buffer_putc(ob, '\n');
HOEDOWN_BUFPUTSL(ob, "<blockquote>\n");
if (content) hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</blockquote>\n");
}
static int
rndr_codespan(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)
{
HOEDOWN_BUFPUTSL(ob, "<code>");
if (text) escape_html(ob, text->data, text->size);
HOEDOWN_BUFPUTSL(ob, "</code>");
return 1;
}
static int
rndr_strikethrough(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (!content || !content->size)
return 0;
HOEDOWN_BUFPUTSL(ob, "<del>");
hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</del>");
return 1;
}
static int
rndr_double_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (!content || !content->size)
return 0;
HOEDOWN_BUFPUTSL(ob, "<strong>");
hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</strong>");
return 1;
}
static int
rndr_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (!content || !content->size) return 0;
HOEDOWN_BUFPUTSL(ob, "<em>");
if (content) hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</em>");
return 1;
}
static int
rndr_underline(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (!content || !content->size)
return 0;
HOEDOWN_BUFPUTSL(ob, "<u>");
hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</u>");
return 1;
}
static int
rndr_highlight(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (!content || !content->size)
return 0;
HOEDOWN_BUFPUTSL(ob, "<mark>");
hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</mark>");
return 1;
}
static int
rndr_quote(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (!content || !content->size)
return 0;
HOEDOWN_BUFPUTSL(ob, "<q>");
hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</q>");
return 1;
}
static int
rndr_linebreak(hoedown_buffer *ob, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state = data->opaque;
hoedown_buffer_puts(ob, USE_XHTML(state) ? "<br/>\n" : "<br>\n");
return 1;
}
static void
rndr_header(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state = data->opaque;
if (ob->size)
hoedown_buffer_putc(ob, '\n');
if (level <= state->toc_data.nesting_level)
hoedown_buffer_printf(ob, "<h%d id=\"toc_%d\">", level, state->toc_data.header_count++);
else
hoedown_buffer_printf(ob, "<h%d>", level);
if (content) hoedown_buffer_put(ob, content->data, content->size);
hoedown_buffer_printf(ob, "</h%d>\n", level);
}
static int
rndr_link(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state = data->opaque;
HOEDOWN_BUFPUTSL(ob, "<a href=\"");
if (link && link->size)
escape_href(ob, link->data, link->size);
if (title && title->size) {
HOEDOWN_BUFPUTSL(ob, "\" title=\"");
escape_html(ob, title->data, title->size);
}
if (state->link_attributes) {
hoedown_buffer_putc(ob, '\"');
state->link_attributes(ob, link, data);
hoedown_buffer_putc(ob, '>');
} else {
HOEDOWN_BUFPUTSL(ob, "\">");
}
if (content && content->size) hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</a>");
return 1;
}
static void
rndr_list(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data)
{
if (ob->size) hoedown_buffer_putc(ob, '\n');
hoedown_buffer_put(ob, (const uint8_t *)(flags & HOEDOWN_LIST_ORDERED ? "<ol>\n" : "<ul>\n"), 5);
if (content) hoedown_buffer_put(ob, content->data, content->size);
hoedown_buffer_put(ob, (const uint8_t *)(flags & HOEDOWN_LIST_ORDERED ? "</ol>\n" : "</ul>\n"), 6);
}
static void
rndr_listitem(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data)
{
HOEDOWN_BUFPUTSL(ob, "<li>");
if (content) {
size_t size = content->size;
while (size && content->data[size - 1] == '\n')
size--;
hoedown_buffer_put(ob, content->data, size);
}
HOEDOWN_BUFPUTSL(ob, "</li>\n");
}
static void
rndr_paragraph(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state = data->opaque;
size_t i = 0;
if (ob->size) hoedown_buffer_putc(ob, '\n');
if (!content || !content->size)
return;
while (i < content->size && isspace(content->data[i])) i++;
if (i == content->size)
return;
HOEDOWN_BUFPUTSL(ob, "<p>");
if (state->flags & HOEDOWN_HTML_HARD_WRAP) {
size_t org;
while (i < content->size) {
org = i;
while (i < content->size && content->data[i] != '\n')
i++;
if (i > org)
hoedown_buffer_put(ob, content->data + org, i - org);
/*
* do not insert a line break if this newline
* is the last character on the paragraph
*/
if (i >= content->size - 1)
break;
rndr_linebreak(ob, data);
i++;
}
} else {
hoedown_buffer_put(ob, content->data + i, content->size - i);
}
HOEDOWN_BUFPUTSL(ob, "</p>\n");
}
static void
rndr_raw_block(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)
{
size_t org, sz;
if (!text)
return;
/* FIXME: Do we *really* need to trim the HTML? How does that make a difference? */
sz = text->size;
while (sz > 0 && text->data[sz - 1] == '\n')
sz--;
org = 0;
while (org < sz && text->data[org] == '\n')
org++;
if (org >= sz)
return;
if (ob->size)
hoedown_buffer_putc(ob, '\n');
hoedown_buffer_put(ob, text->data + org, sz - org);
hoedown_buffer_putc(ob, '\n');
}
static int
rndr_triple_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (!content || !content->size) return 0;
HOEDOWN_BUFPUTSL(ob, "<strong><em>");
hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</em></strong>");
return 1;
}
static void
rndr_hrule(hoedown_buffer *ob, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state = data->opaque;
if (ob->size) hoedown_buffer_putc(ob, '\n');
hoedown_buffer_puts(ob, USE_XHTML(state) ? "<hr/>\n" : "<hr>\n");
}
static int
rndr_image(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state = data->opaque;
if (!link || !link->size) return 0;
HOEDOWN_BUFPUTSL(ob, "<img src=\"");
escape_href(ob, link->data, link->size);
HOEDOWN_BUFPUTSL(ob, "\" alt=\"");
if (alt && alt->size)
escape_html(ob, alt->data, alt->size);
if (title && title->size) {
HOEDOWN_BUFPUTSL(ob, "\" title=\"");
escape_html(ob, title->data, title->size); }
hoedown_buffer_puts(ob, USE_XHTML(state) ? "\"/>" : "\">");
return 1;
}
static int
rndr_raw_html(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state = data->opaque;
/* ESCAPE overrides SKIP_HTML. It doesn't look to see if
* there are any valid tags, just escapes all of them. */
if((state->flags & HOEDOWN_HTML_ESCAPE) != 0) {
escape_html(ob, text->data, text->size);
return 1;
}
if ((state->flags & HOEDOWN_HTML_SKIP_HTML) != 0)
return 1;
hoedown_buffer_put(ob, text->data, text->size);
return 1;
}
static void
rndr_table(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (ob->size) hoedown_buffer_putc(ob, '\n');
HOEDOWN_BUFPUTSL(ob, "<table>\n");
hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</table>\n");
}
static void
rndr_table_header(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (ob->size) hoedown_buffer_putc(ob, '\n');
HOEDOWN_BUFPUTSL(ob, "<thead>\n");
hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</thead>\n");
}
static void
rndr_table_body(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (ob->size) hoedown_buffer_putc(ob, '\n');
HOEDOWN_BUFPUTSL(ob, "<tbody>\n");
hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</tbody>\n");
}
static void
rndr_tablerow(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
HOEDOWN_BUFPUTSL(ob, "<tr>\n");
if (content) hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</tr>\n");
}
static void
rndr_tablecell(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data)
{
if (flags & HOEDOWN_TABLE_HEADER) {
HOEDOWN_BUFPUTSL(ob, "<th");
} else {
HOEDOWN_BUFPUTSL(ob, "<td");
}
switch (flags & HOEDOWN_TABLE_ALIGNMASK) {
case HOEDOWN_TABLE_ALIGN_CENTER:
HOEDOWN_BUFPUTSL(ob, " style=\"text-align: center\">");
break;
case HOEDOWN_TABLE_ALIGN_LEFT:
HOEDOWN_BUFPUTSL(ob, " style=\"text-align: left\">");
break;
case HOEDOWN_TABLE_ALIGN_RIGHT:
HOEDOWN_BUFPUTSL(ob, " style=\"text-align: right\">");
break;
default:
HOEDOWN_BUFPUTSL(ob, ">");
}
if (content)
hoedown_buffer_put(ob, content->data, content->size);
if (flags & HOEDOWN_TABLE_HEADER) {
HOEDOWN_BUFPUTSL(ob, "</th>\n");
} else {
HOEDOWN_BUFPUTSL(ob, "</td>\n");
}
}
static int
rndr_superscript(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (!content || !content->size) return 0;
HOEDOWN_BUFPUTSL(ob, "<sup>");
hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</sup>");
return 1;
}
static void
rndr_normal_text(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
if (content)
escape_html(ob, content->data, content->size);
}
static void
rndr_footnotes(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state = data->opaque;
if (ob->size) hoedown_buffer_putc(ob, '\n');
HOEDOWN_BUFPUTSL(ob, "<div class=\"footnotes\">\n");
hoedown_buffer_puts(ob, USE_XHTML(state) ? "<hr/>\n" : "<hr>\n");
HOEDOWN_BUFPUTSL(ob, "<ol>\n");
if (content) hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "\n</ol>\n</div>\n");
}
static void
rndr_footnote_def(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data)
{
size_t i = 0;
int pfound = 0;
/* insert anchor at the end of first paragraph block */
if (content) {
while ((i+3) < content->size) {
if (content->data[i++] != '<') continue;
if (content->data[i++] != '/') continue;
if (content->data[i++] != 'p' && content->data[i] != 'P') continue;
if (content->data[i] != '>') continue;
i -= 3;
pfound = 1;
break;
}
}
hoedown_buffer_printf(ob, "\n<li id=\"fn%d\">\n", num);
if (pfound) {
hoedown_buffer_put(ob, content->data, i);
hoedown_buffer_printf(ob, "&nbsp;<a href=\"#fnref%d\" rev=\"footnote\">&#8617;</a>", num);
hoedown_buffer_put(ob, content->data + i, content->size - i);
} else if (content) {
hoedown_buffer_put(ob, content->data, content->size);
}
HOEDOWN_BUFPUTSL(ob, "</li>\n");
}
static int
rndr_footnote_ref(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data)
{
hoedown_buffer_printf(ob, "<sup id=\"fnref%d\"><a href=\"#fn%d\" rel=\"footnote\">%d</a></sup>", num, num, num);
return 1;
}
static int
rndr_math(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data)
{
hoedown_buffer_put(ob, (const uint8_t *)(displaymode ? "\\[" : "\\("), 2);
escape_html(ob, text->data, text->size);
hoedown_buffer_put(ob, (const uint8_t *)(displaymode ? "\\]" : "\\)"), 2);
return 1;
}
static void
toc_header(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state = data->opaque;
if (level <= state->toc_data.nesting_level) {
/* set the level offset if this is the first header
* we're parsing for the document */
if (state->toc_data.current_level == 0)
state->toc_data.level_offset = level - 1;
level -= state->toc_data.level_offset;
if (level > state->toc_data.current_level) {
while (level > state->toc_data.current_level) {
HOEDOWN_BUFPUTSL(ob, "<ul>\n<li>\n");
state->toc_data.current_level++;
}
} else if (level < state->toc_data.current_level) {
HOEDOWN_BUFPUTSL(ob, "</li>\n");
while (level < state->toc_data.current_level) {
HOEDOWN_BUFPUTSL(ob, "</ul>\n</li>\n");
state->toc_data.current_level--;
}
HOEDOWN_BUFPUTSL(ob,"<li>\n");
} else {
HOEDOWN_BUFPUTSL(ob,"</li>\n<li>\n");
}
hoedown_buffer_printf(ob, "<a href=\"#toc_%d\">", state->toc_data.header_count++);
if (content) hoedown_buffer_put(ob, content->data, content->size);
HOEDOWN_BUFPUTSL(ob, "</a>\n");
}
}
static int
toc_link(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data)
{
if (content && content->size) hoedown_buffer_put(ob, content->data, content->size);
return 1;
}
static void
toc_finalize(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data)
{
hoedown_html_renderer_state *state;
if (inline_render)
return;
state = data->opaque;
while (state->toc_data.current_level > 0) {
HOEDOWN_BUFPUTSL(ob, "</li>\n</ul>\n");
state->toc_data.current_level--;
}
state->toc_data.header_count = 0;
}
hoedown_renderer *
hoedown_html_toc_renderer_new(int nesting_level)
{
static const hoedown_renderer cb_default = {
NULL,
NULL,
NULL,
toc_header,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
rndr_codespan,
rndr_double_emphasis,
rndr_emphasis,
rndr_underline,
rndr_highlight,
rndr_quote,
NULL,
NULL,
toc_link,
rndr_triple_emphasis,
rndr_strikethrough,
rndr_superscript,
NULL,
NULL,
NULL,
NULL,
rndr_normal_text,
NULL,
toc_finalize
};
hoedown_html_renderer_state *state;
hoedown_renderer *renderer;
/* Prepare the state pointer */
state = hoedown_malloc(sizeof(hoedown_html_renderer_state));
memset(state, 0x0, sizeof(hoedown_html_renderer_state));
state->toc_data.nesting_level = nesting_level;
/* Prepare the renderer */
renderer = hoedown_malloc(sizeof(hoedown_renderer));
memcpy(renderer, &cb_default, sizeof(hoedown_renderer));
renderer->opaque = state;
return renderer;
}
hoedown_renderer *
hoedown_html_renderer_new(hoedown_html_flags render_flags, int nesting_level)
{
static const hoedown_renderer cb_default = {
NULL,
rndr_blockcode,
rndr_blockquote,
rndr_header,
rndr_hrule,
rndr_list,
rndr_listitem,
rndr_paragraph,
rndr_table,
rndr_table_header,
rndr_table_body,
rndr_tablerow,
rndr_tablecell,
rndr_footnotes,
rndr_footnote_def,
rndr_raw_block,
rndr_autolink,
rndr_codespan,
rndr_double_emphasis,
rndr_emphasis,
rndr_underline,
rndr_highlight,
rndr_quote,
rndr_image,
rndr_linebreak,
rndr_link,
rndr_triple_emphasis,
rndr_strikethrough,
rndr_superscript,
rndr_footnote_ref,
rndr_math,
rndr_raw_html,
NULL,
rndr_normal_text,
NULL,
NULL
};
hoedown_html_renderer_state *state;
hoedown_renderer *renderer;
/* Prepare the state pointer */
state = hoedown_malloc(sizeof(hoedown_html_renderer_state));
memset(state, 0x0, sizeof(hoedown_html_renderer_state));
state->flags = render_flags;
state->toc_data.nesting_level = nesting_level;
/* Prepare the renderer */
renderer = hoedown_malloc(sizeof(hoedown_renderer));
memcpy(renderer, &cb_default, sizeof(hoedown_renderer));
if (render_flags & HOEDOWN_HTML_SKIP_HTML || render_flags & HOEDOWN_HTML_ESCAPE)
renderer->blockhtml = NULL;
renderer->opaque = state;
return renderer;
}
void
hoedown_html_renderer_free(hoedown_renderer *renderer)
{
free(renderer->opaque);
free(renderer);
}

84
include/hoedown/html.h Normal file
View File

@ -0,0 +1,84 @@
/* html.h - HTML renderer and utilities */
#ifndef HOEDOWN_HTML_H
#define HOEDOWN_HTML_H
#include "document.h"
#include "buffer.h"
#ifdef __cplusplus
extern "C" {
#endif
/*************
* CONSTANTS *
*************/
typedef enum hoedown_html_flags {
HOEDOWN_HTML_SKIP_HTML = (1 << 0),
HOEDOWN_HTML_ESCAPE = (1 << 1),
HOEDOWN_HTML_HARD_WRAP = (1 << 2),
HOEDOWN_HTML_USE_XHTML = (1 << 3)
} hoedown_html_flags;
typedef enum hoedown_html_tag {
HOEDOWN_HTML_TAG_NONE = 0,
HOEDOWN_HTML_TAG_OPEN,
HOEDOWN_HTML_TAG_CLOSE
} hoedown_html_tag;
/*********
* TYPES *
*********/
struct hoedown_html_renderer_state {
void *opaque;
struct {
int header_count;
int current_level;
int level_offset;
int nesting_level;
} toc_data;
hoedown_html_flags flags;
/* extra callbacks */
void (*link_attributes)(hoedown_buffer *ob, const hoedown_buffer *url, const hoedown_renderer_data *data);
};
typedef struct hoedown_html_renderer_state hoedown_html_renderer_state;
/*************
* FUNCTIONS *
*************/
/* hoedown_html_smartypants: process an HTML snippet using SmartyPants for smart punctuation */
void hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *data, size_t size);
/* hoedown_html_is_tag: checks if data starts with a specific tag, returns the tag type or NONE */
hoedown_html_tag hoedown_html_is_tag(const uint8_t *data, size_t size, const char *tagname);
/* hoedown_html_renderer_new: allocates a regular HTML renderer */
hoedown_renderer *hoedown_html_renderer_new(
hoedown_html_flags render_flags,
int nesting_level
) __attribute__ ((malloc));
/* hoedown_html_toc_renderer_new: like hoedown_html_renderer_new, but the returned renderer produces the Table of Contents */
hoedown_renderer *hoedown_html_toc_renderer_new(
int nesting_level
) __attribute__ ((malloc));
/* hoedown_html_renderer_free: deallocate an HTML renderer */
void hoedown_html_renderer_free(hoedown_renderer *renderer);
#ifdef __cplusplus
}
#endif
#endif /** HOEDOWN_HTML_H **/

View File

@ -0,0 +1,240 @@
/* ANSI-C code produced by gperf version 3.0.3 */
/* Command-line: gperf -L ANSI-C -N hoedown_find_block_tag -c -C -E -S 1 --ignore-case -m100 html_block_names.gperf */
/* Computed positions: -k'1-2' */
#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \
&& ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \
&& (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \
&& ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \
&& ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \
&& ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \
&& ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \
&& ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \
&& ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \
&& ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \
&& ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \
&& ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \
&& ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \
&& ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \
&& ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \
&& ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \
&& ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \
&& ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \
&& ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \
&& ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \
&& ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \
&& ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \
&& ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126))
/* The character set is not based on ISO-646. */
#error "gperf generated tables don't work with this execution character set. Please report a bug to <bug-gnu-gperf@gnu.org>."
#endif
/* maximum key range = 24, duplicates = 0 */
#ifndef GPERF_DOWNCASE
#define GPERF_DOWNCASE 1
static unsigned char gperf_downcase[256] =
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106,
107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121,
122, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104,
105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134,
135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164,
165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194,
195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209,
210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224,
225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254,
255
};
#endif
#ifndef GPERF_CASE_STRNCMP
#define GPERF_CASE_STRNCMP 1
static int
gperf_case_strncmp (register const char *s1, register const char *s2, register unsigned int n)
{
for (; n > 0;)
{
unsigned char c1 = gperf_downcase[(unsigned char)*s1++];
unsigned char c2 = gperf_downcase[(unsigned char)*s2++];
if (c1 != 0 && c1 == c2)
{
n--;
continue;
}
return (int)c1 - (int)c2;
}
return 0;
}
#endif
#ifdef __GNUC__
__inline
#else
#ifdef __cplusplus
inline
#endif
#endif
static unsigned int
hash (register const char *str, register unsigned int len)
{
static const unsigned char asso_values[] =
{
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
22, 21, 19, 18, 16, 0, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 1, 25, 0, 25,
1, 0, 0, 13, 0, 25, 25, 11, 2, 1,
0, 25, 25, 5, 0, 2, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 1, 25,
0, 25, 1, 0, 0, 13, 0, 25, 25, 11,
2, 1, 0, 25, 25, 5, 0, 2, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25
};
register int hval = (int)len;
switch (hval)
{
default:
hval += asso_values[(unsigned char)str[1]+1];
/*FALLTHROUGH*/
case 1:
hval += asso_values[(unsigned char)str[0]];
break;
}
return hval;
}
#ifdef __GNUC__
__inline
#ifdef __GNUC_STDC_INLINE__
__attribute__ ((__gnu_inline__))
#endif
#endif
const char *
hoedown_find_block_tag (register const char *str, register unsigned int len)
{
enum
{
TOTAL_KEYWORDS = 24,
MIN_WORD_LENGTH = 1,
MAX_WORD_LENGTH = 10,
MIN_HASH_VALUE = 1,
MAX_HASH_VALUE = 24
};
if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH)
{
register int key = hash (str, len);
if (key <= MAX_HASH_VALUE && key >= MIN_HASH_VALUE)
{
register const char *resword;
switch (key - 1)
{
case 0:
resword = "p";
goto compare;
case 1:
resword = "h6";
goto compare;
case 2:
resword = "div";
goto compare;
case 3:
resword = "del";
goto compare;
case 4:
resword = "form";
goto compare;
case 5:
resword = "table";
goto compare;
case 6:
resword = "figure";
goto compare;
case 7:
resword = "pre";
goto compare;
case 8:
resword = "fieldset";
goto compare;
case 9:
resword = "noscript";
goto compare;
case 10:
resword = "script";
goto compare;
case 11:
resword = "style";
goto compare;
case 12:
resword = "dl";
goto compare;
case 13:
resword = "ol";
goto compare;
case 14:
resword = "ul";
goto compare;
case 15:
resword = "math";
goto compare;
case 16:
resword = "ins";
goto compare;
case 17:
resword = "h5";
goto compare;
case 18:
resword = "iframe";
goto compare;
case 19:
resword = "h4";
goto compare;
case 20:
resword = "h3";
goto compare;
case 21:
resword = "blockquote";
goto compare;
case 22:
resword = "h2";
goto compare;
case 23:
resword = "h1";
goto compare;
}
return 0;
compare:
if ((((unsigned char)*str ^ (unsigned char)*resword) & ~32) == 0 && !gperf_case_strncmp (str, resword, len) && resword[len] == '\0')
return resword;
}
}
return 0;
}

View File

@ -0,0 +1,435 @@
#include "html.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#ifdef _MSC_VER
#define snprintf _snprintf
#endif
struct smartypants_data {
int in_squote;
int in_dquote;
};
static size_t smartypants_cb__ltag(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
static size_t smartypants_cb__dquote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
static size_t smartypants_cb__amp(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
static size_t smartypants_cb__period(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
static size_t smartypants_cb__number(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
static size_t smartypants_cb__dash(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
static size_t smartypants_cb__parens(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
static size_t smartypants_cb__squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
static size_t smartypants_cb__backtick(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
static size_t smartypants_cb__escape(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
static size_t (*smartypants_cb_ptrs[])
(hoedown_buffer *, struct smartypants_data *, uint8_t, const uint8_t *, size_t) =
{
NULL, /* 0 */
smartypants_cb__dash, /* 1 */
smartypants_cb__parens, /* 2 */
smartypants_cb__squote, /* 3 */
smartypants_cb__dquote, /* 4 */
smartypants_cb__amp, /* 5 */
smartypants_cb__period, /* 6 */
smartypants_cb__number, /* 7 */
smartypants_cb__ltag, /* 8 */
smartypants_cb__backtick, /* 9 */
smartypants_cb__escape, /* 10 */
};
static const uint8_t smartypants_cb_chars[UINT8_MAX+1] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 4, 0, 0, 0, 5, 3, 2, 0, 0, 0, 0, 1, 6, 0,
0, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0,
9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
static int
word_boundary(uint8_t c)
{
return c == 0 || isspace(c) || ispunct(c);
}
/*
If 'text' begins with any kind of single quote (e.g. "'" or "&apos;" etc.),
returns the length of the sequence of characters that makes up the single-
quote. Otherwise, returns zero.
*/
static size_t
squote_len(const uint8_t *text, size_t size)
{
static char* single_quote_list[] = { "'", "&#39;", "&#x27;", "&apos;", NULL };
char** p;
for (p = single_quote_list; *p; ++p) {
size_t len = strlen(*p);
if (size >= len && memcmp(text, *p, len) == 0) {
return len;
}
}
return 0;
}
/* Converts " or ' at very beginning or end of a word to left or right quote */
static int
smartypants_quotes(hoedown_buffer *ob, uint8_t previous_char, uint8_t next_char, uint8_t quote, int *is_open)
{
char ent[8];
if (*is_open && !word_boundary(next_char))
return 0;
if (!(*is_open) && !word_boundary(previous_char))
return 0;
snprintf(ent, sizeof(ent), "&%c%cquo;", (*is_open) ? 'r' : 'l', quote);
*is_open = !(*is_open);
hoedown_buffer_puts(ob, ent);
return 1;
}
/*
Converts ' to left or right single quote; but the initial ' might be in
different forms, e.g. &apos; or &#39; or &#x27;.
'squote_text' points to the original single quote, and 'squote_size' is its length.
'text' points at the last character of the single-quote, e.g. ' or ;
*/
static size_t
smartypants_squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size,
const uint8_t *squote_text, size_t squote_size)
{
if (size >= 2) {
uint8_t t1 = tolower(text[1]);
size_t next_squote_len = squote_len(text+1, size-1);
/* convert '' to &ldquo; or &rdquo; */
if (next_squote_len > 0) {
uint8_t next_char = (size > 1+next_squote_len) ? text[1+next_squote_len] : 0;
if (smartypants_quotes(ob, previous_char, next_char, 'd', &smrt->in_dquote))
return next_squote_len;
}
/* Tom's, isn't, I'm, I'd */
if ((t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') &&
(size == 3 || word_boundary(text[2]))) {
HOEDOWN_BUFPUTSL(ob, "&rsquo;");
return 0;
}
/* you're, you'll, you've */
if (size >= 3) {
uint8_t t2 = tolower(text[2]);
if (((t1 == 'r' && t2 == 'e') ||
(t1 == 'l' && t2 == 'l') ||
(t1 == 'v' && t2 == 'e')) &&
(size == 4 || word_boundary(text[3]))) {
HOEDOWN_BUFPUTSL(ob, "&rsquo;");
return 0;
}
}
}
if (smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 's', &smrt->in_squote))
return 0;
hoedown_buffer_put(ob, squote_text, squote_size);
return 0;
}
/* Converts ' to left or right single quote. */
static size_t
smartypants_cb__squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
{
return smartypants_squote(ob, smrt, previous_char, text, size, text, 1);
}
/* Converts (c), (r), (tm) */
static size_t
smartypants_cb__parens(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
{
if (size >= 3) {
uint8_t t1 = tolower(text[1]);
uint8_t t2 = tolower(text[2]);
if (t1 == 'c' && t2 == ')') {
HOEDOWN_BUFPUTSL(ob, "&copy;");
return 2;
}
if (t1 == 'r' && t2 == ')') {
HOEDOWN_BUFPUTSL(ob, "&reg;");
return 2;
}
if (size >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')') {
HOEDOWN_BUFPUTSL(ob, "&trade;");
return 3;
}
}
hoedown_buffer_putc(ob, text[0]);
return 0;
}
/* Converts "--" to em-dash, etc. */
static size_t
smartypants_cb__dash(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
{
if (size >= 3 && text[1] == '-' && text[2] == '-') {
HOEDOWN_BUFPUTSL(ob, "&mdash;");
return 2;
}
if (size >= 2 && text[1] == '-') {
HOEDOWN_BUFPUTSL(ob, "&ndash;");
return 1;
}
hoedown_buffer_putc(ob, text[0]);
return 0;
}
/* Converts &quot; etc. */
static size_t
smartypants_cb__amp(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
{
size_t len;
if (size >= 6 && memcmp(text, "&quot;", 6) == 0) {
if (smartypants_quotes(ob, previous_char, size >= 7 ? text[6] : 0, 'd', &smrt->in_dquote))
return 5;
}
len = squote_len(text, size);
if (len > 0) {
return (len-1) + smartypants_squote(ob, smrt, previous_char, text+(len-1), size-(len-1), text, len);
}
if (size >= 4 && memcmp(text, "&#0;", 4) == 0)
return 3;
hoedown_buffer_putc(ob, '&');
return 0;
}
/* Converts "..." to ellipsis */
static size_t
smartypants_cb__period(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
{
if (size >= 3 && text[1] == '.' && text[2] == '.') {
HOEDOWN_BUFPUTSL(ob, "&hellip;");
return 2;
}
if (size >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.') {
HOEDOWN_BUFPUTSL(ob, "&hellip;");
return 4;
}
hoedown_buffer_putc(ob, text[0]);
return 0;
}
/* Converts `` to opening double quote */
static size_t
smartypants_cb__backtick(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
{
if (size >= 2 && text[1] == '`') {
if (smartypants_quotes(ob, previous_char, size >= 3 ? text[2] : 0, 'd', &smrt->in_dquote))
return 1;
}
hoedown_buffer_putc(ob, text[0]);
return 0;
}
/* Converts 1/2, 1/4, 3/4 */
static size_t
smartypants_cb__number(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
{
if (word_boundary(previous_char) && size >= 3) {
if (text[0] == '1' && text[1] == '/' && text[2] == '2') {
if (size == 3 || word_boundary(text[3])) {
HOEDOWN_BUFPUTSL(ob, "&frac12;");
return 2;
}
}
if (text[0] == '1' && text[1] == '/' && text[2] == '4') {
if (size == 3 || word_boundary(text[3]) ||
(size >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h')) {
HOEDOWN_BUFPUTSL(ob, "&frac14;");
return 2;
}
}
if (text[0] == '3' && text[1] == '/' && text[2] == '4') {
if (size == 3 || word_boundary(text[3]) ||
(size >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's')) {
HOEDOWN_BUFPUTSL(ob, "&frac34;");
return 2;
}
}
}
hoedown_buffer_putc(ob, text[0]);
return 0;
}
/* Converts " to left or right double quote */
static size_t
smartypants_cb__dquote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
{
if (!smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 'd', &smrt->in_dquote))
HOEDOWN_BUFPUTSL(ob, "&quot;");
return 0;
}
static size_t
smartypants_cb__ltag(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
{
static const char *skip_tags[] = {
"pre", "code", "var", "samp", "kbd", "math", "script", "style"
};
static const size_t skip_tags_count = 8;
size_t tag, i = 0;
/* This is a comment. Copy everything verbatim until --> or EOF is seen. */
if (i + 4 < size && memcmp(text + i, "<!--", 4) == 0) {
i += 4;
while (i + 3 < size && memcmp(text + i, "-->", 3) != 0)
i++;
i += 3;
hoedown_buffer_put(ob, text, i + 1);
return i;
}
while (i < size && text[i] != '>')
i++;
for (tag = 0; tag < skip_tags_count; ++tag) {
if (hoedown_html_is_tag(text, size, skip_tags[tag]) == HOEDOWN_HTML_TAG_OPEN)
break;
}
if (tag < skip_tags_count) {
for (;;) {
while (i < size && text[i] != '<')
i++;
if (i == size)
break;
if (hoedown_html_is_tag(text + i, size - i, skip_tags[tag]) == HOEDOWN_HTML_TAG_CLOSE)
break;
i++;
}
while (i < size && text[i] != '>')
i++;
}
hoedown_buffer_put(ob, text, i + 1);
return i;
}
static size_t
smartypants_cb__escape(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
{
if (size < 2)
return 0;
switch (text[1]) {
case '\\':
case '"':
case '\'':
case '.':
case '-':
case '`':
hoedown_buffer_putc(ob, text[1]);
return 1;
default:
hoedown_buffer_putc(ob, '\\');
return 0;
}
}
#if 0
static struct {
uint8_t c0;
const uint8_t *pattern;
const uint8_t *entity;
int skip;
} smartypants_subs[] = {
{ '\'', "'s>", "&rsquo;", 0 },
{ '\'', "'t>", "&rsquo;", 0 },
{ '\'', "'re>", "&rsquo;", 0 },
{ '\'', "'ll>", "&rsquo;", 0 },
{ '\'', "'ve>", "&rsquo;", 0 },
{ '\'', "'m>", "&rsquo;", 0 },
{ '\'', "'d>", "&rsquo;", 0 },
{ '-', "--", "&mdash;", 1 },
{ '-', "<->", "&ndash;", 0 },
{ '.', "...", "&hellip;", 2 },
{ '.', ". . .", "&hellip;", 4 },
{ '(', "(c)", "&copy;", 2 },
{ '(', "(r)", "&reg;", 2 },
{ '(', "(tm)", "&trade;", 3 },
{ '3', "<3/4>", "&frac34;", 2 },
{ '3', "<3/4ths>", "&frac34;", 2 },
{ '1', "<1/2>", "&frac12;", 2 },
{ '1', "<1/4>", "&frac14;", 2 },
{ '1', "<1/4th>", "&frac14;", 2 },
{ '&', "&#0;", 0, 3 },
};
#endif
void
hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *text, size_t size)
{
size_t i;
struct smartypants_data smrt = {0, 0};
if (!text)
return;
hoedown_buffer_grow(ob, size);
for (i = 0; i < size; ++i) {
size_t org;
uint8_t action = 0;
org = i;
while (i < size && (action = smartypants_cb_chars[text[i]]) == 0)
i++;
if (i > org)
hoedown_buffer_put(ob, text + org, i - org);
if (i < size) {
i += smartypants_cb_ptrs[(int)action]
(ob, &smrt, i ? text[i - 1] : 0, text + i, size - i);
}
}
}

79
include/hoedown/stack.c Normal file
View File

@ -0,0 +1,79 @@
#include "stack.h"
#include "buffer.h"
#include <stdlib.h>
#include <string.h>
#include <assert.h>
void
hoedown_stack_init(hoedown_stack *st, size_t initial_size)
{
assert(st);
st->item = NULL;
st->size = st->asize = 0;
if (!initial_size)
initial_size = 8;
hoedown_stack_grow(st, initial_size);
}
void
hoedown_stack_uninit(hoedown_stack *st)
{
assert(st);
free(st->item);
}
void
hoedown_stack_grow(hoedown_stack *st, size_t neosz)
{
assert(st);
if (st->asize >= neosz)
return;
st->item = hoedown_realloc(st->item, neosz * sizeof(void *));
memset(st->item + st->asize, 0x0, (neosz - st->asize) * sizeof(void *));
st->asize = neosz;
if (st->size > neosz)
st->size = neosz;
}
void
hoedown_stack_push(hoedown_stack *st, void *item)
{
assert(st);
if (st->size >= st->asize)
hoedown_stack_grow(st, st->size * 2);
st->item[st->size++] = item;
}
void *
hoedown_stack_pop(hoedown_stack *st)
{
assert(st);
if (!st->size)
return NULL;
return st->item[--st->size];
}
void *
hoedown_stack_top(const hoedown_stack *st)
{
assert(st);
if (!st->size)
return NULL;
return st->item[st->size - 1];
}

52
include/hoedown/stack.h Normal file
View File

@ -0,0 +1,52 @@
/* stack.h - simple stacking */
#ifndef HOEDOWN_STACK_H
#define HOEDOWN_STACK_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/*********
* TYPES *
*********/
struct hoedown_stack {
void **item;
size_t size;
size_t asize;
};
typedef struct hoedown_stack hoedown_stack;
/*************
* FUNCTIONS *
*************/
/* hoedown_stack_init: initialize a stack */
void hoedown_stack_init(hoedown_stack *st, size_t initial_size);
/* hoedown_stack_uninit: free internal data of the stack */
void hoedown_stack_uninit(hoedown_stack *st);
/* hoedown_stack_grow: increase the allocated size to the given value */
void hoedown_stack_grow(hoedown_stack *st, size_t neosz);
/* hoedown_stack_push: push an item to the top of the stack */
void hoedown_stack_push(hoedown_stack *st, void *item);
/* hoedown_stack_pop: retrieve and remove the item at the top of the stack */
void *hoedown_stack_pop(hoedown_stack *st);
/* hoedown_stack_top: retrieve the item at the top of the stack */
void *hoedown_stack_top(const hoedown_stack *st);
#ifdef __cplusplus
}
#endif
#endif /** HOEDOWN_STACK_H **/

View File

@ -0,0 +1,9 @@
#include "version.h"
void
hoedown_version(int *major, int *minor, int *revision)
{
*major = HOEDOWN_VERSION_MAJOR;
*minor = HOEDOWN_VERSION_MINOR;
*revision = HOEDOWN_VERSION_REVISION;
}

33
include/hoedown/version.h Normal file
View File

@ -0,0 +1,33 @@
/* version.h - holds Hoedown's version */
#ifndef HOEDOWN_VERSION_H
#define HOEDOWN_VERSION_H
#ifdef __cplusplus
extern "C" {
#endif
/*************
* CONSTANTS *
*************/
#define HOEDOWN_VERSION "3.0.7"
#define HOEDOWN_VERSION_MAJOR 3
#define HOEDOWN_VERSION_MINOR 0
#define HOEDOWN_VERSION_REVISION 7
/*************
* FUNCTIONS *
*************/
/* hoedown_version: retrieve Hoedown's version numbers */
void hoedown_version(int *major, int *minor, int *revision);
#ifdef __cplusplus
}
#endif
#endif /** HOEDOWN_VERSION_H **/

@ -1 +1 @@
Subproject commit be7d25ed22abd07a254bfb8ff6c30de4fcc79e6a Subproject commit b467b0816f5f6816778f90b55a9d0b5437310fd5

View File

@ -1,90 +0,0 @@
.pragma library
var preg_replace=function(a,b,c,d){void 0===d&&(d=-1);var e=a.substr(a.lastIndexOf(a[0])+1),f=a.substr(1,a.lastIndexOf(a[0])-1),g=RegExp(f,e),i=[],j=0,k=0,l=c,m=[];if(-1===d){do m=g.exec(c),null!==m&&i.push(m);while(null!==m&&-1!==e.indexOf("g"))}else i.push(g.exec(c));for(j=i.length-1;j>-1;j--){for(m=b,k=i[j].length;k>-1;k--)m=m.replace("${"+k+"}",i[j][k]).replace("$"+k,i[j][k]).replace("\\"+k,i[j][k]);l=l.replace(i[j][0],m)}return l};
var markdown_parser = function(str){
var rules = [
// headers
['/(#+)(.*)/g', function(chars, header){
var level = chars.length;
return '<h'+level+'>'+header.trim()+'</h'+level+'>';
}],
// images
// ['/\\!\\[([^\\[]+)\\]\\(([^\\(]+)\\)/g', '<img src=\"\\2\" alt=\"\\1\" />'],
// link
['/\\[([^\\[]+)\\]\\(([^\\(]+)\\)/g', '<a href=\"\\2\">\\1</a>'],
// bold
['/(\\*\\*|__)(.*?)\\1/g', '<strong>\\2</strong>'],
// emphasis
['/(\\*|_)(.*?)\\1/g', '<i>\\2</i>'],
// strike
['/(\\~\\~)(.*?)\\1/g', '<del>\\2</del>'],
// quote
['/\\:\\"(.*?)\\"\\:/g', '<q>\\1</q>'],
// unordered list
// ['/\\n\\*(.*)/g', function(item){
// return '<ul>\n<li>'+item.trim()+'</li>\n</ul>';
// }],
// ordered list
// ['/\\n[0-9]+\\.(.*)/g', function(item){
// return '<ol>\n<li>'+item.trim()+'</li>\n</ol>';
// }],
// blockquote
['/\\n\\>(.*)/g', function(str){
return '<blockquote>'+str.trim()+'</blockquote>';
}]
// paragraphs
// ['/\\n[^\\n]+\\n/g', function(line){
// line = line.trim();
// if(line[0] === '<'){
// return line;
// }
// return '\n<p>'+line+'</p>\n';
// }]
], fixes = [
['/<\\/ul>\n<ul>/g', '\n'],
['/<\\/ol>\n<ol>/g', '\n'],
['/<\\/blockquote>\n<blockquote>/g', "\n"]
];
var parse_line = function(str){
str = "\n" + str.trim() + "\n";
for(var i = 0, j = rules.length; i < j; i++){
if(typeof rules[i][1] == 'function') {
var _flag = rules[i][0].substr(rules[i][0].lastIndexOf(rules[i][0][0])+1),
_pattern = rules[i][0].substr(1, rules[i][0].lastIndexOf(rules[i][0][0])-1),
reg = new RegExp(_pattern, _flag);
var matches = reg.exec(str);
if(matches !== null){
if(matches.length > 1){
str = preg_replace(rules[i][0], rules[i][1](matches[1], matches[2]), str);
}
else
{
str = preg_replace(rules[i][0], rules[i][1](matches[0]), str);
}
}
}
else {
str = preg_replace(rules[i][0], rules[i][1], str);
}
}
return str.trim();
};
str = str.split('\n');
var rtn = [];
for(var i = 0, j = str.length; i < j; i++){
rtn.push(parse_line(str[i]));
}
rtn = rtn.join('\n');
for(i = 0, j = fixes.length; i < j; i++){
rtn = preg_replace(fixes[i][0], fixes[i][1], rtn);
}
return rtn;
};

View File

@ -1,12 +0,0 @@
.pragma library
function pushToStack(stack, page) {
if(page && stack.currentItem !== page) {
if(stack.depth === 1) {
stack.replace(page)
} else {
stack.pop(null)
stack.replace(page)
}
}
}

View File

@ -1,20 +1,21 @@
import QtQuick 2.9 import QtQuick 2.12
import QtQuick.Controls 2.2 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.12
import Qt.labs.settings 1.0 import Qt.labs.settings 1.0
import Qt.labs.platform 1.0 as Platform import Qt.labs.platform 1.0 as Platform
import Spectral.Panel 2.0
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Page 2.0 import Spectral.Page 2.0
import Spectral.Effect 2.0
import Spectral 0.1 import Spectral 0.1
import Spectral.Setting 0.1 import Spectral.Setting 0.1
import "qrc:/js/util.js" as Util
ApplicationWindow { ApplicationWindow {
readonly property var currentConnection: accountListView.currentConnection ? accountListView.currentConnection : null Material.theme: MPalette.theme
Material.background: MPalette.background
width: 960 width: 960
height: 640 height: 640
@ -26,9 +27,9 @@ ApplicationWindow {
visible: true visible: true
title: qsTr("Spectral") title: qsTr("Spectral")
Material.theme: MSettings.darkTheme ? Material.Dark : Material.Light background: Rectangle {
color: MSettings.darkTheme ? "#303030" : "#FFFFFF"
Material.accent: spectralController.color(currentConnection ? currentConnection.localUserId : "") }
Platform.SystemTrayIcon { Platform.SystemTrayIcon {
visible: MSettings.showTray visible: MSettings.showTray
@ -54,353 +55,147 @@ ApplicationWindow {
quitOnLastWindowClosed: !MSettings.showTray quitOnLastWindowClosed: !MSettings.showTray
onNotificationClicked: { onNotificationClicked: {
roomPage.enteredRoom = currentConnection.room(roomId) roomListForm.enteredRoom = spectralController.connection.room(roomId)
roomPage.goToEvent(eventId) roomForm.goToEvent(eventId)
showWindow() showWindow()
} }
onErrorOccured: { onErrorOccured: {
errorDialog.error = error roomListForm.errorControl.error = error
errorDialog.detail = detail roomListForm.errorControl.detail = detail
errorDialog.open() roomListForm.errorControl.visible = true
}
onSyncDone: roomListForm.errorControl.visible = false
}
Shortcut {
sequence: StandardKey.Quit
onActivated: Qt.quit()
}
Dialog {
property bool busy: false
width: 360
x: (window.width - width) / 2
y: (window.height - height) / 2
id: loginDialog
parent: ApplicationWindow.overlay
title: "Login"
contentItem: Column {
AutoTextField {
width: parent.width
id: serverField
placeholderText: "Server Address"
text: "https://matrix.org"
}
AutoTextField {
width: parent.width
id: usernameField
placeholderText: "Username"
}
AutoTextField {
width: parent.width
id: passwordField
placeholderText: "Password"
echoMode: TextInput.Password
} }
} }
AccountListModel { footer: DialogButtonBox {
id: accountListModel Button {
text: "OK"
flat: true
enabled: !loginDialog.busy
onClicked: loginDialog.doLogin()
}
Button {
text: "Cancel"
flat: true
enabled: !loginDialog.busy
onClicked: loginDialog.close()
}
ToolTip {
id: loginButtonTooltip
}
}
onVisibleChanged: {
if (visible) spectralController.onErrorOccured.connect(showError)
else spectralController.onErrorOccured.disconnect(showError)
}
function showError(error, detail) {
loginDialog.busy = false
loginButtonTooltip.text = error + ": " + detail
loginButtonTooltip.open()
}
function doLogin() {
if (!(serverField.text.startsWith("http") && serverField.text.includes("://"))) {
loginButtonTooltip.text = "Server address should start with http(s)://"
loginButtonTooltip.open()
return
}
loginDialog.busy = true
spectralController.loginWithCredentials(serverField.text, usernameField.text, passwordField.text)
spectralController.connectionAdded.connect(function(conn) {
busy = false
loginDialog.close()
})
}
}
SplitView {
anchors.fill: parent
RoomListPanel {
width: window.width * 0.35
Layout.minimumWidth: 180
id: roomListForm
clip: true
controller: spectralController controller: spectralController
onLeaveRoom: roomForm.saveReadMarker(room)
} }
Dialog { RoomPanel {
property string error
property string detail
x: (window.width - width) / 2
y: (window.height - height) / 2
id: errorDialog
title: error + " Error"
contentItem: Label { text: errorDialog.detail }
}
Component {
id: loginPage
Login { controller: spectralController }
}
Room {
id: roomPage
parent: null
connection: currentConnection
}
Setting {
id: settingPage
parent: null
listModel: accountListModel
}
RowLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.preferredWidth: 64
Layout.fillHeight: true
id: sideNav
color: Material.primary
ColumnLayout {
anchors.fill: parent
spacing: 0
AutoListView {
property var currentConnection: null
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.minimumWidth: 480
id: accountListView id: roomForm
model: accountListModel
spacing: 0
clip: true clip: true
delegate: Column { currentRoom: roomListForm.enteredRoom
property bool expanded: accountListView.currentConnection === connection
width: parent.width
spacing: 0
SideNavButton {
width: parent.width
height: width
selected: stackView.currentItem === page && currentConnection === connection
ImageItem {
anchors.fill: parent
anchors.margins: 12
hint: user.displayName
source: user.paintable
}
highlightColor: spectralController.color(user.id)
page: roomPage
onClicked: {
accountListView.currentConnection = connection
roomPage.filter = 0
}
}
Column {
width: parent.width
height: expanded ? implicitHeight : 0
spacing: 0
clip: true
SideNavButton {
width: parent.width
height: width
MaterialIcon {
anchors.fill: parent
icon: "\ue7f7"
color: "white"
}
onClicked: roomPage.filter = 1
}
SideNavButton {
width: parent.width
height: width
MaterialIcon {
anchors.fill: parent
icon: "\ue7fd"
color: "white"
}
onClicked: roomPage.filter = 2
}
SideNavButton {
width: parent.width
height: width
MaterialIcon {
anchors.fill: parent
icon: "\ue7fb"
color: "white"
}
onClicked: roomPage.filter = 3
}
Behavior on height {
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
}
}
}
}
SideNavButton {
Layout.fillWidth: true
Layout.preferredHeight: width
MaterialIcon {
anchors.fill: parent
icon: "\ue145"
color: "white"
}
enabled: !addRoomMenu.opened
onClicked: addRoomMenu.popup()
Menu {
id: addRoomMenu
MenuItem {
text:"New Room"
onTriggered: addRoomDialog.open()
Dialog {
id: addRoomDialog
parent: ApplicationWindow.overlay
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 360
title: "New Room"
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: Column {
AutoTextField {
width: parent.width
id: addRoomDialogNameTextField
placeholderText: "Name"
}
AutoTextField {
width: parent.width
id: addRoomDialogTopicTextField
placeholderText: "Topic"
}
}
onAccepted: spectralController.createRoom(currentConnection, addRoomDialogNameTextField.text, addRoomDialogTopicTextField.text)
}
}
MenuItem {
text: "Join Room"
onTriggered: joinRoomDialog.open()
Dialog {
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 360
id: joinRoomDialog
parent: ApplicationWindow.overlay
title: "Input Room Alias or ID"
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: AutoTextField {
id: joinRoomDialogTextField
placeholderText: "#matrix:matrix.org"
}
onAccepted: spectralController.joinRoom(currentConnection, joinRoomDialogTextField.text)
}
}
MenuItem {
text: "Direct Chat"
onTriggered: directChatDialog.open()
Dialog {
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 360
id: directChatDialog
parent: ApplicationWindow.overlay
title: "Input User ID"
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: AutoTextField {
id: directChatDialogTextField
placeholderText: "@bot:matrix.org"
}
onAccepted: spectralController.createDirectChat(currentConnection, directChatDialogTextField.text)
}
}
}
}
SideNavButton {
Layout.fillWidth: true
Layout.preferredHeight: width
MaterialIcon {
anchors.fill: parent
icon: "\ue8b8"
color: "white"
}
page: settingPage
}
SideNavButton {
Layout.fillWidth: true
Layout.preferredHeight: width
MaterialIcon {
anchors.fill: parent
icon: "\ue8ac"
color: "white"
}
onClicked: MSettings.confirmOnExit ? confirmExitDialog.open() : Qt.quit()
Dialog {
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 360
id: confirmExitDialog
parent: ApplicationWindow.overlay
title: "Exit"
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: Column {
Label { text: "Exit?" }
CheckBox {
text: "Do not ask next time"
checked: !MSettings.confirmOnExit
onCheckedChanged: MSettings.confirmOnExit = !checked
}
}
onAccepted: Qt.quit()
}
}
}
}
StackView {
Layout.fillWidth: true
Layout.fillHeight: true
id: stackView
initialItem: roomPage
} }
} }
Binding { Binding {
target: imageProvider target: imageProvider
property: "connection" property: "connection"
value: currentConnection value: spectralController.connection
} }
function showWindow() { function showWindow() {
@ -415,7 +210,7 @@ ApplicationWindow {
Component.onCompleted: { Component.onCompleted: {
spectralController.initiated.connect(function() { spectralController.initiated.connect(function() {
if (spectralController.accountCount == 0) stackView.push(loginPage) if (spectralController.accountCount == 0) loginDialog.open()
}) })
} }
} }

View File

@ -7,7 +7,7 @@ Style=Material
[Material] [Material]
Theme=Light Theme=Light
Variant=Dense
Primary=#344955 Primary=#344955
Accent=#498882 Accent=#673AB7
;Foreground=Black Font/Family="Roboto,Noto Sans,Noto Color Emoji"
;Background=#161616

32
res.qrc
View File

@ -2,30 +2,19 @@
<qresource prefix="/"> <qresource prefix="/">
<file>qtquickcontrols2.conf</file> <file>qtquickcontrols2.conf</file>
<file>qml/main.qml</file> <file>qml/main.qml</file>
<file>js/md.js</file>
<file>js/util.js</file>
<file>imports/Spectral/Component/Emoji/EmojiPicker.qml</file> <file>imports/Spectral/Component/Emoji/EmojiPicker.qml</file>
<file>imports/Spectral/Component/Emoji/qmldir</file> <file>imports/Spectral/Component/Emoji/qmldir</file>
<file>imports/Spectral/Component/Timeline/DownloadableContent.qml</file> <file>imports/Spectral/Component/Timeline/DownloadableContent.qml</file>
<file>imports/Spectral/Component/Timeline/GenericBubble.qml</file>
<file>imports/Spectral/Component/Timeline/MessageDelegate.qml</file> <file>imports/Spectral/Component/Timeline/MessageDelegate.qml</file>
<file>imports/Spectral/Component/Timeline/qmldir</file> <file>imports/Spectral/Component/Timeline/qmldir</file>
<file>imports/Spectral/Component/Timeline/StateDelegate.qml</file> <file>imports/Spectral/Component/Timeline/StateDelegate.qml</file>
<file>imports/Spectral/Component/AutoMouseArea.qml</file> <file>imports/Spectral/Component/AutoMouseArea.qml</file>
<file>imports/Spectral/Component/MaterialIcon.qml</file> <file>imports/Spectral/Component/MaterialIcon.qml</file>
<file>imports/Spectral/Component/qmldir</file> <file>imports/Spectral/Component/qmldir</file>
<file>imports/Spectral/Component/SideNavButton.qml</file>
<file>imports/Spectral/Effect/ElevationEffect.qml</file> <file>imports/Spectral/Effect/ElevationEffect.qml</file>
<file>imports/Spectral/Effect/qmldir</file> <file>imports/Spectral/Effect/qmldir</file>
<file>imports/Spectral/Menu/MessageContextMenu.qml</file>
<file>imports/Spectral/Menu/qmldir</file>
<file>imports/Spectral/Menu/RoomContextMenu.qml</file>
<file>imports/Spectral/Page/Login.qml</file>
<file>imports/Spectral/Page/qmldir</file> <file>imports/Spectral/Page/qmldir</file>
<file>imports/Spectral/Page/Room.qml</file>
<file>assets/font/material.ttf</file> <file>assets/font/material.ttf</file>
<file>assets/img/avatar.png</file>
<file>assets/img/background.jpg</file>
<file>assets/img/icon.icns</file> <file>assets/img/icon.icns</file>
<file>assets/img/icon.ico</file> <file>assets/img/icon.ico</file>
<file>assets/img/icon.png</file> <file>assets/img/icon.png</file>
@ -33,25 +22,26 @@
<file>imports/Spectral/Font/MaterialFont.qml</file> <file>imports/Spectral/Font/MaterialFont.qml</file>
<file>imports/Spectral/Font/qmldir</file> <file>imports/Spectral/Font/qmldir</file>
<file>imports/Spectral/Setting/qmldir</file> <file>imports/Spectral/Setting/qmldir</file>
<file>imports/Spectral/Page/Setting.qml</file>
<file>imports/Spectral/Page/SettingForm.ui.qml</file>
<file>imports/Spectral/Page/SettingCategoryDelegate.qml</file>
<file>imports/Spectral/Page/SettingAccountDelegate.qml</file>
<file>imports/Spectral/Page/LoginForm.ui.qml</file>
<file>imports/Spectral/Panel/qmldir</file> <file>imports/Spectral/Panel/qmldir</file>
<file>imports/Spectral/Panel/RoomDrawer.qml</file> <file>imports/Spectral/Panel/RoomDrawer.qml</file>
<file>imports/Spectral/Panel/RoomListPanel.qml</file> <file>imports/Spectral/Panel/RoomListPanel.qml</file>
<file>imports/Spectral/Panel/RoomListPanelForm.ui.qml</file>
<file>imports/Spectral/Panel/RoomPanel.qml</file> <file>imports/Spectral/Panel/RoomPanel.qml</file>
<file>imports/Spectral/Panel/RoomPanelForm.ui.qml</file>
<file>imports/Spectral/Panel/RoomHeader.qml</file> <file>imports/Spectral/Panel/RoomHeader.qml</file>
<file>imports/Spectral/Panel/RoomListDelegate.qml</file>
<file>imports/Spectral/Component/ScrollHelper.qml</file> <file>imports/Spectral/Component/ScrollHelper.qml</file>
<file>imports/Spectral/Component/AutoListView.qml</file> <file>imports/Spectral/Component/AutoListView.qml</file>
<file>imports/Spectral/Component/Timeline/TimelineImage.qml</file>
<file>imports/Spectral/Component/Timeline/TimelineLabel.qml</file>
<file>imports/Spectral/Component/AutoTextField.qml</file> <file>imports/Spectral/Component/AutoTextField.qml</file>
<file>imports/Spectral/Panel/RoomPanelInput.qml</file> <file>imports/Spectral/Panel/RoomPanelInput.qml</file>
<file>imports/Spectral/Component/SplitView.qml</file> <file>imports/Spectral/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>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -20,6 +20,9 @@ isEmpty(USE_SYSTEM_SORTFILTERPROXYMODEL) {
isEmpty(USE_SYSTEM_QMATRIXCLIENT) { isEmpty(USE_SYSTEM_QMATRIXCLIENT) {
USE_SYSTEM_QMATRIXCLIENT = false USE_SYSTEM_QMATRIXCLIENT = false
} }
isEmpty(BUNDLE_FONT) {
BUNDLE_FONT = false
}
$$USE_SYSTEM_QMATRIXCLIENT { $$USE_SYSTEM_QMATRIXCLIENT {
PKGCONFIG += QMatrixClient PKGCONFIG += QMatrixClient
@ -34,6 +37,27 @@ $$USE_SYSTEM_SORTFILTERPROXYMODEL {
include(include/SortFilterProxyModel/SortFilterProxyModel.pri) include(include/SortFilterProxyModel/SortFilterProxyModel.pri)
} }
INCLUDEPATH += include/hoedown
HEADERS += \
include/hoedown/autolink.h \
include/hoedown/buffer.h \
include/hoedown/document.h \
include/hoedown/escape.h \
include/hoedown/html.h \
include/hoedown/stack.h \
include/hoedown/version.h
SOURCES += \
include/hoedown/autolink.c \
include/hoedown/buffer.c \
include/hoedown/document.c \
include/hoedown/escape.c \
include/hoedown/html.c \
include/hoedown/html_blocks.c \
include/hoedown/html_smartypants.c \
include/hoedown/stack.c \
include/hoedown/version.c
# The following define makes your compiler emit warnings if you use # The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings # any feature of Qt which as been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the # depend on your compiler). Please consult the documentation of the
@ -45,8 +69,14 @@ DEFINES += QT_DEPRECATED_WARNINGS
# You can also select to disable deprecated APIs only up to a certain version of Qt. # You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
RESOURCES += \ RESOURCES += res.qrc
res.qrc $$BUNDLE_FONT {
message("Bundling fonts.")
DEFINES += BUNDLE_FONT
RESOURCES += font.qrc
} else {
message("Using fonts from operating system.")
}
# Additional import path used to resolve QML modules in Qt Creator's code model # Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH += imports/ QML_IMPORT_PATH += imports/
@ -102,12 +132,10 @@ HEADERS += \
src/emojimodel.h \ src/emojimodel.h \
src/spectralroom.h \ src/spectralroom.h \
src/userlistmodel.h \ src/userlistmodel.h \
src/imageitem.h \
src/accountlistmodel.h \ src/accountlistmodel.h \
src/spectraluser.h \ src/spectraluser.h \
src/notifications/manager.h \ src/notifications/manager.h \
src/utils.h \ src/utils.h
src/paintable.h
SOURCES += src/main.cpp \ SOURCES += src/main.cpp \
src/controller.cpp \ src/controller.cpp \
@ -117,11 +145,9 @@ SOURCES += src/main.cpp \
src/emojimodel.cpp \ src/emojimodel.cpp \
src/spectralroom.cpp \ src/spectralroom.cpp \
src/userlistmodel.cpp \ src/userlistmodel.cpp \
src/imageitem.cpp \
src/accountlistmodel.cpp \ src/accountlistmodel.cpp \
src/spectraluser.cpp \ src/spectraluser.cpp \
src/utils.cpp \ src/utils.cpp
src/paintable.cpp
unix:!mac { unix:!mac {
SOURCES += src/notifications/managerlinux.cpp SOURCES += src/notifications/managerlinux.cpp

View File

@ -10,6 +10,8 @@
#include "csapi/joining.h" #include "csapi/joining.h"
#include "csapi/logout.h" #include "csapi/logout.h"
#include "utils.h"
#include <QClipboard> #include <QClipboard>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
@ -42,8 +44,8 @@ Controller::Controller(QObject* parent)
Controller::~Controller() { Controller::~Controller() {
for (Connection* c : m_connections) { for (Connection* c : m_connections) {
c->saveState();
c->stopSync(); c->stopSync();
c->saveState();
} }
} }
@ -58,27 +60,27 @@ inline QString accessTokenFileName(const AccountSettings& account) {
void Controller::loginWithCredentials(QString serverAddr, QString user, void Controller::loginWithCredentials(QString serverAddr, QString user,
QString pass) { QString pass) {
if (!user.isEmpty() && !pass.isEmpty()) { if (!user.isEmpty() && !pass.isEmpty()) {
Connection* m_connection = new Connection(this); Connection* conn = new Connection(this);
m_connection->setHomeserver(QUrl(serverAddr)); conn->setHomeserver(QUrl(serverAddr));
m_connection->connectToServer(user, pass, ""); conn->connectToServer(user, pass, "");
connect(m_connection, &Connection::connected, [=] { connect(conn, &Connection::connected, [=] {
AccountSettings account(m_connection->userId()); AccountSettings account(conn->userId());
account.setKeepLoggedIn(true); account.setKeepLoggedIn(true);
account.clearAccessToken(); // Drop the legacy - just in case account.clearAccessToken(); // Drop the legacy - just in case
account.setHomeserver(m_connection->homeserver()); account.setHomeserver(conn->homeserver());
account.setDeviceId(m_connection->deviceId()); account.setDeviceId(conn->deviceId());
account.setDeviceName("Spectral"); account.setDeviceName("Spectral");
if (!saveAccessToken(account, m_connection->accessToken())) if (!saveAccessToken(account, conn->accessToken()))
qWarning() << "Couldn't save access token"; qWarning() << "Couldn't save access token";
account.sync(); account.sync();
addConnection(m_connection); addConnection(conn);
setConnection(conn);
}); });
connect(m_connection, &Connection::networkError, connect(conn, &Connection::networkError,
[=](QString error, QByteArray detail) { [=](QString error, QString, int, int) {
emit errorOccured("Network", error); emit errorOccured("Network Error", error);
}); });
connect(m_connection, &Connection::loginError, connect(conn, &Connection::loginError, [=](QString error, QString) {
[=](QString error, QByteArray detail) {
emit errorOccured("Login Failed", error); emit errorOccured("Login Failed", error);
}); });
} }
@ -98,6 +100,7 @@ void Controller::logout(Connection* conn) {
conn->stopSync(); conn->stopSync();
emit conn->stateChanged(); emit conn->stateChanged();
emit conn->loggedOut(); emit conn->loggedOut();
if (!m_connections.isEmpty()) setConnection(m_connections[0]);
}); });
connect(job, &LogoutJob::failure, this, [=] { connect(job, &LogoutJob::failure, this, [=] {
emit errorOccured("Server-side Logout Failed", job->errorString()); emit errorOccured("Server-side Logout Failed", job->errorString());
@ -109,11 +112,12 @@ void Controller::addConnection(Connection* c) {
m_connections.push_back(c); m_connections.push_back(c);
connect(c, &Connection::syncDone, this, [=] { c->setLazyLoading(true);
c->sync(30000);
static int counter = 0; connect(c, &Connection::syncDone, this, [=] {
if (++counter % 17 == 2) c->saveState(); emit syncDone();
c->sync(30000);
c->saveState();
}); });
connect(c, &Connection::loggedOut, this, [=] { dropConnection(c); }); connect(c, &Connection::loggedOut, this, [=] { dropConnection(c); });
@ -146,17 +150,17 @@ void Controller::invokeLogin() {
c->loadState(); c->loadState();
addConnection(c); addConnection(c);
}); });
connect(c, &Connection::loginError, connect(c, &Connection::loginError, [=](QString error, QString) {
[=](QString error, QByteArray detail) {
emit errorOccured("Login Failed", error); emit errorOccured("Login Failed", error);
}); });
connect(c, &Connection::networkError, connect(c, &Connection::networkError,
[=](QString error, QByteArray detail) { [=](QString error, QString, int, int) {
emit errorOccured("Network", error); emit errorOccured("Network Error", error);
}); });
c->connectWithToken(account.userId(), accessToken, account.deviceId()); c->connectWithToken(account.userId(), accessToken, account.deviceId());
} }
} }
if (!m_connections.isEmpty()) setConnection(m_connections[0]);
emit initiated(); emit initiated();
} }
@ -184,7 +188,7 @@ bool Controller::saveAccessToken(const AccountSettings& account,
auto fileDir = QFileInfo(accountTokenFile).dir(); auto fileDir = QFileInfo(accountTokenFile).dir();
if (!((fileDir.exists() || fileDir.mkpath(".")) && if (!((fileDir.exists() || fileDir.mkpath(".")) &&
accountTokenFile.open(QFile::WriteOnly))) { accountTokenFile.open(QFile::WriteOnly))) {
emit errorOccured("Token", "Cannot save access token."); emit errorOccured("I/O Denied", "Cannot save access token.");
} else { } else {
accountTokenFile.write(accessToken); accountTokenFile.write(accessToken);
return true; return true;
@ -227,19 +231,22 @@ void Controller::playAudio(QUrl localFile) {
connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); }); connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); });
} }
QColor Controller::color(QString userId) {
return QColor(SettingsGroup("UI/Color").value(userId, "#498882").toString());
}
void Controller::setColor(QString userId, QColor newColor) {
SettingsGroup("UI/Color").setValue(userId, newColor.name());
}
void Controller::postNotification(const QString& roomId, const QString& eventId, void Controller::postNotification(const QString& roomId, const QString& eventId,
const QString& roomName, const QString& roomName,
const QString& senderName, const QString& senderName,
const QString& text, const QImage& icon, const QString& text, const QImage& icon) {
const QUrl& iconPath) {
notificationsManager.postNotification(roomId, eventId, roomName, senderName, notificationsManager.postNotification(roomId, eventId, roomName, senderName,
text, icon, iconPath); text, icon);
}
int Controller::dpi() {
return SettingsGroup("Interface").value("dpi", 100).toInt();
}
void Controller::setDpi(int dpi) {
SettingsGroup("Interface").setValue("dpi", dpi);
}
QString Controller::removeReply(const QString& text) {
return utils::removeReply(text);
} }

View File

@ -20,6 +20,8 @@ class Controller : public QObject {
connectionDropped) connectionDropped)
Q_PROPERTY(bool quitOnLastWindowClosed READ quitOnLastWindowClosed WRITE Q_PROPERTY(bool quitOnLastWindowClosed READ quitOnLastWindowClosed WRITE
setQuitOnLastWindowClosed NOTIFY quitOnLastWindowClosedChanged) setQuitOnLastWindowClosed NOTIFY quitOnLastWindowClosedChanged)
Q_PROPERTY(Connection* connection READ connection WRITE setConnection NOTIFY
connectionChanged)
public: public:
explicit Controller(QObject* parent = nullptr); explicit Controller(QObject* parent = nullptr);
@ -30,6 +32,9 @@ class Controller : public QObject {
QVector<Connection*> connections() { return m_connections; } QVector<Connection*> connections() { return m_connections; }
Q_INVOKABLE int dpi();
Q_INVOKABLE void setDpi(int dpi);
// All the non-Q_INVOKABLE functions. // All the non-Q_INVOKABLE functions.
void addConnection(Connection* c); void addConnection(Connection* c);
void dropConnection(Connection* c); void dropConnection(Connection* c);
@ -47,13 +52,23 @@ class Controller : public QObject {
} }
} }
Q_INVOKABLE QColor color(QString userId); Connection* connection() {
Q_INVOKABLE void setColor(QString userId, QColor newColor); if (m_connection.isNull()) return nullptr;
return m_connection;
}
void setConnection(Connection* conn) {
if (!conn) return;
if (conn == m_connection) return;
m_connection = conn;
emit connectionChanged();
}
private: private:
QClipboard* m_clipboard = QApplication::clipboard(); QClipboard* m_clipboard = QApplication::clipboard();
NotificationsManager notificationsManager; NotificationsManager notificationsManager;
QVector<Connection*> m_connections; QVector<Connection*> m_connections;
QPointer<Connection> m_connection;
QByteArray loadAccessToken(const AccountSettings& account); QByteArray loadAccessToken(const AccountSettings& account);
bool saveAccessToken(const AccountSettings& account, bool saveAccessToken(const AccountSettings& account,
@ -61,17 +76,21 @@ class Controller : public QObject {
void loadSettings(); void loadSettings();
void saveSettings() const; void saveSettings() const;
Q_INVOKABLE QString removeReply(const QString& text);
private slots: private slots:
void invokeLogin(); void invokeLogin();
signals: signals:
void busyChanged(); void busyChanged();
void errorOccured(QString error, QString detail); void errorOccured(QString error, QString detail);
void syncDone();
void connectionAdded(Connection* conn); void connectionAdded(Connection* conn);
void connectionDropped(Connection* conn); void connectionDropped(Connection* conn);
void initiated(); void initiated();
void notificationClicked(const QString roomId, const QString eventId); void notificationClicked(const QString roomId, const QString eventId);
void quitOnLastWindowClosedChanged(); void quitOnLastWindowClosedChanged();
void connectionChanged();
public slots: public slots:
void logout(Connection* conn); void logout(Connection* conn);
@ -82,8 +101,7 @@ class Controller : public QObject {
void playAudio(QUrl localFile); void playAudio(QUrl localFile);
void postNotification(const QString& roomId, const QString& eventId, void postNotification(const QString& roomId, const QString& eventId,
const QString& roomName, const QString& senderName, const QString& roomName, const QString& senderName,
const QString& text, const QImage& icon, const QString& text, const QImage& icon);
const QUrl& iconPath);
}; };
#endif // CONTROLLER_H #endif // CONTROLLER_H

View File

@ -1,106 +0,0 @@
#include "imageitem.h"
#include <QBitmap>
#include <QGraphicsOpacityEffect>
#include <QRect>
ImageItem::ImageItem(QQuickItem *parent) : QQuickPaintedItem(parent) {}
inline static QString stringtoColor(QString string) {
int hash = 0;
for (int i = 0; i < string.length(); i++)
hash = string.at(i).unicode() + ((hash << 5) - hash);
QString colour = "#";
for (int j = 0; j < 3; j++)
colour += ("00" + QString::number((hash >> (j * 8)) & 0xFF, 16)).right(2);
return colour;
}
inline static QImage getImageFromPaintable(QPointer<Paintable> p, QRectF b) {
if (p.isNull()) return {};
QImage image(p->image(int(b.width()), int(b.height())));
if (image.isNull()) return {};
return image;
}
void ImageItem::paint(QPainter *painter) {
QRectF bounding_rect = boundingRect();
painter->setRenderHint(QPainter::Antialiasing, true);
QImage image(getImageFromPaintable(m_paintable, bounding_rect));
if (image.isNull()) {
painter->setPen(Qt::NoPen);
if (m_color.isEmpty())
painter->setBrush(QColor(stringtoColor(m_hint)));
else
painter->setBrush(QColor(m_color));
if (m_round)
painter->drawEllipse(0, 0, int(bounding_rect.width()),
int(bounding_rect.height()));
else
painter->drawRect(0, 0, int(bounding_rect.width()),
int(bounding_rect.height()));
painter->setPen(QPen(Qt::white, 2));
QFont font;
font.setStyleHint(QFont::SansSerif);
font.setPixelSize(int(bounding_rect.width() / 2));
font.setBold(true);
painter->setFont(font);
painter->drawText(
QRect(0, 0, int(bounding_rect.width()), int(bounding_rect.height())),
Qt::AlignCenter, m_hint.at(0).toUpper());
} else {
QImage scaled = image.scaled(
int(bounding_rect.width()) + 1, int(bounding_rect.height()) + 1,
Qt::KeepAspectRatioByExpanding, Qt::FastTransformation);
QPointF center = bounding_rect.center() - scaled.rect().center();
if (m_round) {
QPainterPath clip;
clip.addEllipse(
0, 0, bounding_rect.width(),
bounding_rect.height()); // this is the shape we want to clip to
painter->setClipPath(clip);
}
painter->drawImage(center, scaled);
}
}
void ImageItem::setPaintable(Paintable *paintable) {
if (!paintable) return;
if (!m_paintable.isNull()) m_paintable->disconnect(this);
m_paintable = paintable;
connect(m_paintable, &Paintable::paintableChanged, this,
[=] { this->update(); });
emit paintableChanged();
update();
}
void ImageItem::setHint(QString newHint) {
if (m_hint != newHint) {
m_hint = newHint;
emit hintChanged();
update();
}
}
void ImageItem::setDefaultColor(QString color) {
if (color != m_color) {
m_color = color;
emit defaultColorChanged();
update();
}
}
void ImageItem::setRound(bool value) {
if (m_round != value) {
m_round = value;
emit roundChanged();
update();
}
}

View File

@ -1,52 +0,0 @@
#ifndef IMAGEITEM_H
#define IMAGEITEM_H
#include <QPointer>
#include <QImage>
#include <QObject>
#include <QPainter>
#include <QQuickItem>
#include <QQuickPaintedItem>
#include "paintable.h"
class ImageItem : public QQuickPaintedItem {
Q_OBJECT
Q_PROPERTY(Paintable* source READ paintable WRITE setPaintable NOTIFY
paintableChanged)
Q_PROPERTY(QString hint READ hint WRITE setHint NOTIFY hintChanged)
Q_PROPERTY(QString defaultColor READ defaultColor WRITE setDefaultColor NOTIFY
defaultColorChanged)
Q_PROPERTY(bool round READ round WRITE setRound NOTIFY roundChanged)
public:
ImageItem(QQuickItem* parent = nullptr);
void paint(QPainter* painter);
Paintable* paintable() { return m_paintable; }
void setPaintable(Paintable* paintable);
QString hint() { return m_hint; }
void setHint(QString hint);
QString defaultColor() { return m_color; }
void setDefaultColor(QString color);
bool round() { return m_round; }
void setRound(bool value);
signals:
void paintableChanged();
void hintChanged();
void defaultColorChanged();
void roundChanged();
private:
QPointer<Paintable> m_paintable;
QString m_hint = "H";
QString m_color;
bool m_round = true;
};
#endif // IMAGEITEM_H

View File

@ -1,79 +1,89 @@
#include "imageprovider.h" #include "imageprovider.h"
#include <QFile>
#include <QMetaObject>
#include <QStandardPaths>
#include <QtCore/QDebug> #include <QtCore/QDebug>
#include <QtCore/QWaitCondition> #include <QtCore/QThread>
#include "jobs/mediathumbnailjob.h" using QMatrixClient::BaseJob;
#include "connection.h" ThumbnailResponse::ThumbnailResponse(QMatrixClient::Connection* c,
QString id, const QSize& size)
using QMatrixClient::MediaThumbnailJob; : c(c),
mediaId(std::move(id)),
ImageProvider::ImageProvider(QObject* parent) requestedSize(size),
: QObject(parent), errorStr("Image request hasn't started") {
QQuickImageProvider( if (requestedSize.isEmpty()) {
QQmlImageProviderBase::Image, errorStr.clear();
QQmlImageProviderBase::ForceAsynchronousImageLoading) { emit finished();
#if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0)) return;
qRegisterMetaType<MediaThumbnailJob*>(); }
#endif if (mediaId.count('/') != 1) {
errorStr =
tr("Media id '%1' doesn't follow server/mediaId pattern")
.arg(mediaId);
emit finished();
return;
}
// Execute a request on the main thread asynchronously
moveToThread(c->thread());
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest,
Qt::QueuedConnection);
} }
QImage ImageProvider::requestImage(const QString& id, QSize* pSize, void ThumbnailResponse::startRequest() {
const QSize& requestedSize) { // Runs in the main thread, not QML thread
if (!id.startsWith("mxc://")) { Q_ASSERT(QThread::currentThread() == c->thread());
qWarning() << "ImageProvider: won't fetch an invalid id:" << id job = c->getThumbnail(mediaId, requestedSize);
<< "doesn't follow server/mediaId pattern"; // Connect to any possible outcome including abandonment
return {}; // to make sure the QML thread is not left stuck forever.
connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult);
} }
QUrl mxcUri{id}; void ThumbnailResponse::prepareResult() {
Q_ASSERT(QThread::currentThread() == job->thread());
QUrl tempfilePath = QUrl::fromLocalFile( Q_ASSERT(job->error() != BaseJob::Pending);
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" +
mxcUri.fileName() + "-" + QString::number(requestedSize.width()) + "x" +
QString::number(requestedSize.height()) + ".png");
QImage cachedImage;
if (cachedImage.load(tempfilePath.toLocalFile())) {
if (pSize != nullptr) *pSize = cachedImage.size();
return cachedImage;
}
MediaThumbnailJob* job = nullptr;
QReadLocker locker(&m_lock);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
QMetaObject::invokeMethod(
m_connection,
[=] { return m_connection->getThumbnail(mxcUri, requestedSize); },
Qt::BlockingQueuedConnection, &job);
#else
QMetaObject::invokeMethod(m_connection, "getThumbnail",
Qt::BlockingQueuedConnection,
Q_RETURN_ARG(MediaThumbnailJob*, job),
Q_ARG(QUrl, mxcUri), Q_ARG(QSize, requestedSize));
#endif
if (!job) {
qDebug() << "ImageProvider: failed to send a request";
return {};
}
QImage result;
{ {
QWaitCondition condition; // The most compact way to block on a signal QWriteLocker _(&lock);
job->connect(job, &MediaThumbnailJob::finished, job, [&] { if (job->error() == BaseJob::Success) {
result = job->thumbnail(); image = job->thumbnail();
condition.wakeAll(); errorStr.clear();
}); } else if (job->error() == BaseJob::Abandoned) {
condition.wait(&m_lock); errorStr = tr("Image request has been cancelled");
qDebug() << "ThumbnailResponse: cancelled for" << mediaId;
} else {
errorStr = job->errorString();
qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-"
<< errorStr;
}
job = nullptr;
}
emit finished();
} }
if (pSize != nullptr) *pSize = result.size(); void ThumbnailResponse::doCancel() {
// Runs in the main thread, not QML thread
result.save(tempfilePath.toLocalFile()); if (job) {
Q_ASSERT(QThread::currentThread() == job->thread());
return result; job->abandon();
}
}
QQuickTextureFactory* ThumbnailResponse::textureFactory() const {
QReadLocker _(&lock);
return QQuickTextureFactory::textureFactoryForImage(image);
}
QString ThumbnailResponse::errorString() const {
QReadLocker _(&lock);
return errorStr;
}
void ThumbnailResponse::cancel() {
QMetaObject::invokeMethod(this, &ThumbnailResponse::doCancel,
Qt::QueuedConnection);
}
QQuickImageResponse* ImageProvider::requestImageResponse(
const QString& id, const QSize& requestedSize) {
qDebug() << "ImageProvider: requesting " << id << "of size" << requestedSize;
return new ThumbnailResponse(m_connection.load(), id, requestedSize);
} }

View File

@ -1,38 +1,67 @@
#ifndef IMAGEPROVIDER_H #ifndef IMAGEPROVIDER_H
#define IMAGEPROVIDER_H #define IMAGEPROVIDER_H
#pragma once
#include <QtQuick/QQuickAsyncImageProvider>
#include <connection.h>
#include <jobs/mediathumbnailjob.h>
#include <QObject>
#include <QtCore/QReadWriteLock> #include <QtCore/QReadWriteLock>
#include <QtQuick/QQuickImageProvider> #include <QtCore/QAtomicPointer>
#include "connection.h" namespace QMatrixClient {
class Connection;
}
class ImageProvider : public QObject, public QQuickImageProvider { class ThumbnailResponse : public QQuickImageResponse {
Q_OBJECT
public:
ThumbnailResponse(QMatrixClient::Connection* c, QString mediaId,
const QSize& requestedSize);
~ThumbnailResponse() override = default;
private slots:
void startRequest();
void prepareResult();
void doCancel();
private:
QMatrixClient::Connection* c;
const QString mediaId;
const QSize requestedSize;
QMatrixClient::MediaThumbnailJob* job = nullptr;
QImage image;
QString errorStr;
mutable QReadWriteLock lock; // Guards ONLY these two members above
QQuickTextureFactory* textureFactory() const override;
QString errorString() const override;
void cancel() override;
};
class ImageProvider : public QObject, public QQuickAsyncImageProvider {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QMatrixClient::Connection* connection READ connection WRITE Q_PROPERTY(QMatrixClient::Connection* connection READ connection WRITE
setConnection NOTIFY connectionChanged) setConnection NOTIFY connectionChanged)
public: public:
explicit ImageProvider(QObject* parent = nullptr); explicit ImageProvider() = default;
QImage requestImage(const QString& id, QSize* pSize, QQuickImageResponse* requestImageResponse(
const QSize& requestedSize) override; const QString& id, const QSize& requestedSize) override;
void initializeEngine(QQmlEngine* engine, const char* uri);
QMatrixClient::Connection* connection() { return m_connection; } QMatrixClient::Connection* connection() { return m_connection; }
void setConnection(QMatrixClient::Connection* newConnection) { void setConnection(QMatrixClient::Connection* connection) {
if (m_connection != newConnection) { m_connection.store(connection);
m_connection = newConnection;
emit connectionChanged(); emit connectionChanged();
} }
}
signals: signals:
void connectionChanged(); void connectionChanged();
private: private:
QReadWriteLock m_lock; QAtomicPointer<QMatrixClient::Connection> m_connection;
QMatrixClient::Connection* m_connection = nullptr;
}; };
#endif // IMAGEPROVIDER_H #endif // IMAGEPROVIDER_H

View File

@ -1,12 +1,13 @@
#include <QFontDatabase>
#include <QGuiApplication> #include <QGuiApplication>
#include <QNetworkProxy> #include <QNetworkProxy>
#include <QNetworkProxyFactory>
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
#include "accountlistmodel.h" #include "accountlistmodel.h"
#include "controller.h" #include "controller.h"
#include "emojimodel.h" #include "emojimodel.h"
#include "imageitem.h"
#include "imageprovider.h" #include "imageprovider.h"
#include "messageeventmodel.h" #include "messageeventmodel.h"
#include "room.h" #include "room.h"
@ -23,7 +24,19 @@
using namespace QMatrixClient; using namespace QMatrixClient;
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #if defined(Q_OS_LINUX) || defined(Q_OS_WIN) || defined(Q_OS_FREEBSD)
if (qgetenv("QT_SCALE_FACTOR").size() == 0) {
QSettings settings("ENCOM", "Spectral");
float factor = settings.value("Interface/dpi", 100).toFloat() / 100;
qDebug() << "DPI:" << factor;
if (factor != -1)
qputenv("QT_SCALE_FACTOR", QString::number(factor).toUtf8());
}
#endif
QNetworkProxyFactory::setUseSystemConfiguration(true);
QApplication app(argc, argv); QApplication app(argc, argv);
@ -34,7 +47,6 @@ int main(int argc, char *argv[]) {
qmlRegisterType<qqsfpm::QQmlSortFilterProxyModel>("SortFilterProxyModel", 0, qmlRegisterType<qqsfpm::QQmlSortFilterProxyModel>("SortFilterProxyModel", 0,
2, "SortFilterProxyModel"); 2, "SortFilterProxyModel");
qmlRegisterType<ImageItem>("Spectral", 0, 1, "ImageItem");
qmlRegisterType<Controller>("Spectral", 0, 1, "Controller"); qmlRegisterType<Controller>("Spectral", 0, 1, "Controller");
qmlRegisterType<AccountListModel>("Spectral", 0, 1, "AccountListModel"); qmlRegisterType<AccountListModel>("Spectral", 0, 1, "AccountListModel");
qmlRegisterType<RoomListModel>("Spectral", 0, 1, "RoomListModel"); qmlRegisterType<RoomListModel>("Spectral", 0, 1, "RoomListModel");
@ -51,6 +63,11 @@ int main(int argc, char *argv[]) {
qRegisterMetaType<SpectralRoom *>("SpectralRoom*"); qRegisterMetaType<SpectralRoom *>("SpectralRoom*");
qRegisterMetaType<SpectralUser *>("SpectralUser*"); qRegisterMetaType<SpectralUser *>("SpectralUser*");
#if defined(BUNDLE_FONT)
QFontDatabase::addApplicationFont(":/assets/font/roboto.ttf");
QFontDatabase::addApplicationFont(":/assets/font/twemoji.ttf");
#endif
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
engine.addImportPath("qrc:/imports"); engine.addImportPath("qrc:/imports");

View File

@ -9,16 +9,11 @@
#include <events/roommemberevent.h> #include <events/roommemberevent.h>
#include <events/simplestateevents.h> #include <events/simplestateevents.h>
#include <QRegExp>
#include <QtCore/QDebug> #include <QtCore/QDebug>
#include <QtQml> // for qmlRegisterType() #include <QtQml> // for qmlRegisterType()
#include "utils.h" #include "utils.h"
static QString parseAvatarUrl(QUrl url) {
return url.host() + "/" + url.path();
}
QHash<int, QByteArray> MessageEventModel::roleNames() const { QHash<int, QByteArray> MessageEventModel::roleNames() const {
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[EventTypeRole] = "eventType"; roles[EventTypeRole] = "eventType";
@ -39,6 +34,9 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const {
roles[LongOperationRole] = "progressInfo"; roles[LongOperationRole] = "progressInfo";
roles[AnnotationRole] = "annotation"; roles[AnnotationRole] = "annotation";
roles[EventResolvedTypeRole] = "eventResolvedType"; roles[EventResolvedTypeRole] = "eventResolvedType";
roles[ReplyEventIdRole] = "replyEventId";
roles[ReplyAuthorRole] = "replyAuthor";
roles[ReplyDisplayRole] = "replyDisplay";
roles[UserMarkerRole] = "userMarker"; roles[UserMarkerRole] = "userMarker";
return roles; return roles;
} }
@ -89,6 +87,9 @@ void MessageEventModel::setRoom(SpectralRoom *room) {
{AboveEventTypeRole, AboveAuthorRole, {AboveEventTypeRole, AboveAuthorRole,
AboveSectionRole, AboveTimeRole}); AboveSectionRole, AboveTimeRole});
} }
for (auto i = m_currentRoom->maxTimelineIndex() - biggest;
i <= m_currentRoom->maxTimelineIndex() - lowest; ++i)
refreshLastUserEvents(i);
}); });
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, connect(m_currentRoom, &Room::pendingEventAboutToAdd, this,
[this] { beginInsertRows({}, 0, 0); }); [this] { beginInsertRows({}, 0, 0); });
@ -109,8 +110,9 @@ void MessageEventModel::setRoom(SpectralRoom *room) {
movingEvent = false; movingEvent = false;
} }
refreshRow(timelineBaseIndex()); // Refresh the looks refreshRow(timelineBaseIndex()); // Refresh the looks
refreshLastUserEvents(0);
if (m_currentRoom->timelineSize() > 1) // Refresh above if (m_currentRoom->timelineSize() > 1) // Refresh above
refreshEventRoles(timelineBaseIndex() + 1); refreshEventRoles(timelineBaseIndex() + 1, {ReadMarkerRole});
if (timelineBaseIndex() > 0) // Refresh below, see #312 if (timelineBaseIndex() > 0) // Refresh below, see #312
refreshEventRoles(timelineBaseIndex() - 1, refreshEventRoles(timelineBaseIndex() - 1,
{AboveEventTypeRole, AboveAuthorRole, {AboveEventTypeRole, AboveAuthorRole,
@ -128,9 +130,11 @@ void MessageEventModel::setRoom(SpectralRoom *room) {
{ReadMarkerRole}); {ReadMarkerRole});
refreshEventRoles(lastReadEventId, {ReadMarkerRole}); refreshEventRoles(lastReadEventId, {ReadMarkerRole});
}); });
connect( connect(m_currentRoom, &Room::replacedEvent, this,
m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
[this](const RoomEvent *newEvent) { refreshEvent(newEvent->id()); }); refreshLastUserEvents(refreshEvent(newEvent->id()) -
timelineBaseIndex());
});
connect(m_currentRoom, &Room::fileTransferProgress, this, connect(m_currentRoom, &Room::fileTransferProgress, this,
&MessageEventModel::refreshEvent); &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::fileTransferCompleted, this, connect(m_currentRoom, &Room::fileTransferCompleted, this,
@ -140,7 +144,7 @@ void MessageEventModel::setRoom(SpectralRoom *room) {
connect(m_currentRoom, &Room::fileTransferCancelled, this, connect(m_currentRoom, &Room::fileTransferCancelled, this,
&MessageEventModel::refreshEvent); &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::readMarkerForUserMoved, this, connect(m_currentRoom, &Room::readMarkerForUserMoved, this,
[=](User *user, QString fromEventId, QString toEventId) { [=](User *, QString fromEventId, QString toEventId) {
refreshEventRoles(fromEventId, {UserMarkerRole}); refreshEventRoles(fromEventId, {UserMarkerRole});
refreshEventRoles(toEventId, {UserMarkerRole}); refreshEventRoles(toEventId, {UserMarkerRole});
}); });
@ -214,6 +218,23 @@ QString MessageEventModel::renderDate(QDateTime timestamp) const {
return date.toString(Qt::DefaultLocaleShortDate); return date.toString(Qt::DefaultLocaleShortDate);
} }
void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) {
if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow)
return;
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin();
const auto &lastSender = (*(timelineBottom + baseTimelineRow))->senderId();
const auto limit = timelineBottom + std::min(baseTimelineRow + 10,
m_currentRoom->timelineSize());
for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0);
it != limit; ++it) {
if ((*it)->senderId() == lastSender) {
auto idx = index(it - timelineBottom);
emit dataChanged(idx, idx);
}
}
}
int MessageEventModel::rowCount(const QModelIndex &parent) const { int MessageEventModel::rowCount(const QModelIndex &parent) const {
if (!m_currentRoom || parent.isValid()) return 0; if (!m_currentRoom || parent.isValid()) return 0;
return m_currentRoom->timelineSize(); return m_currentRoom->timelineSize();
@ -235,13 +256,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
const auto &evt = isPending ? **pendingIt : **timelineIt; const auto &evt = isPending ? **pendingIt : **timelineIt;
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
return utils::eventToString(evt, m_currentRoom, Qt::RichText); return utils::removeReply(utils::eventToString(evt, m_currentRoom, Qt::RichText));
} }
if (role == MessageRole) { if (role == MessageRole) {
static const QRegExp rmReplyRegExp("^> <@.*:.*> .*\n\n(.*)"); return utils::removeReply(utils::eventToString(evt, m_currentRoom));
return utils::eventToString(evt, m_currentRoom)
.replace(rmReplyRegExp, "\\1");
} }
if (role == Qt::ToolTipRole) { if (role == Qt::ToolTipRole) {
@ -306,13 +325,14 @@ 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 == ReadMarkerRole)
return evt.id() == lastReadEventId && row > timelineBaseIndex();
if (role == SpecialMarksRole) { if (role == SpecialMarksRole) {
if (isPending) return pendingIt->deliveryStatus(); if (isPending) return pendingIt->deliveryStatus();
if (is<RedactionEvent>(evt)) return EventStatus::Hidden; if (is<RedactionEvent>(evt)) return EventStatus::Hidden;
if (evt.isRedacted()) return EventStatus::Redacted; if (evt.isRedacted()) return EventStatus::Hidden;
if (evt.isStateEvent() && if (evt.isStateEvent() &&
static_cast<const StateEventBase &>(evt).repeatsState()) static_cast<const StateEventBase &>(evt).repeatsState())
@ -348,6 +368,28 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
return variantList; return variantList;
} }
if (role == ReplyEventIdRole || role == ReplyDisplayRole ||
role == ReplyAuthorRole) {
const QString &replyEventId = evt.contentJson()["m.relates_to"]
.toObject()["m.in_reply_to"]
.toObject()["event_id"]
.toString();
if (replyEventId.isEmpty()) return {};
const auto replyIt = m_currentRoom->findInTimeline(replyEventId);
if (replyIt == m_currentRoom->timelineEdge()) return {};
const auto& replyEvt = **replyIt;
switch (role) {
case ReplyEventIdRole:
return replyEventId;
case ReplyDisplayRole:
return utils::removeReply(utils::eventToString(replyEvt, m_currentRoom, Qt::RichText));
case ReplyAuthorRole:
return QVariant::fromValue(
m_currentRoom->user(replyEvt.senderId()));
}
return {};
}
if (role == AboveEventTypeRole || role == AboveSectionRole || if (role == AboveEventTypeRole || role == AboveSectionRole ||
role == AboveAuthorRole || role == AboveTimeRole) role == AboveAuthorRole || role == AboveTimeRole)
for (auto r = row + 1; r < rowCount(); ++r) { for (auto r = row + 1; r < rowCount(); ++r) {

View File

@ -1,8 +1,8 @@
#ifndef MESSAGEEVENTMODEL_H #ifndef MESSAGEEVENTMODEL_H
#define MESSAGEEVENTMODEL_H #define MESSAGEEVENTMODEL_H
#include "spectralroom.h"
#include "room.h" #include "room.h"
#include "spectralroom.h"
#include <QtCore/QAbstractListModel> #include <QtCore/QAbstractListModel>
@ -30,6 +30,9 @@ class MessageEventModel : public QAbstractListModel {
LongOperationRole, LongOperationRole,
AnnotationRole, AnnotationRole,
UserMarkerRole, UserMarkerRole,
ReplyEventIdRole,
ReplyAuthorRole,
ReplyDisplayRole,
// For debugging // For debugging
EventResolvedTypeRole, EventResolvedTypeRole,
}; };
@ -62,6 +65,7 @@ class MessageEventModel : public QAbstractListModel {
const QMatrixClient::Room::rev_iter_t& baseIt) const; const QMatrixClient::Room::rev_iter_t& baseIt) const;
QString renderDate(QDateTime timestamp) const; QString renderDate(QDateTime timestamp) const;
void refreshLastUserEvents(int baseRow);
void refreshEventRoles(int row, const QVector<int>& roles = {}); void refreshEventRoles(int row, const QVector<int>& roles = {});
int refreshEventRoles(const QString& eventId, const QVector<int>& roles = {}); int refreshEventRoles(const QString& eventId, const QVector<int>& roles = {});

View File

@ -23,7 +23,7 @@ class NotificationsManager : public QObject {
void postNotification(const QString &roomId, const QString &eventId, void postNotification(const QString &roomId, const QString &eventId,
const QString &roomName, const QString &senderName, const QString &roomName, const QString &senderName,
const QString &text, const QImage &icon, const QUrl &iconPath); const QString &text, const QImage &icon);
signals: signals:
void notificationClicked(const QString roomId, const QString eventId); void notificationClicked(const QString roomId, const QString eventId);

View File

@ -25,8 +25,7 @@ NotificationsManager::NotificationsManager(QObject *parent)
void NotificationsManager::postNotification( void NotificationsManager::postNotification(
const QString &roomid, const QString &eventid, const QString &roomname, const QString &roomid, const QString &eventid, const QString &roomname,
const QString &sender, const QString &text, const QImage &icon, const QString &sender, const QString &text, const QImage &icon) {
const QUrl &iconPath) {
uint id = showNotification(roomname, sender + ": " + text, icon); uint id = showNotification(roomname, sender + ": " + text, icon);
notificationIds[id] = roomEventId{roomid, eventid}; notificationIds[id] = roomEventId{roomid, eventid};
} }

View File

@ -19,13 +19,11 @@ NotificationsManager::postNotification(
const QString &roomName, const QString &roomName,
const QString &senderName, const QString &senderName,
const QString &text, const QString &text,
const QImage &icon, const QImage &icon)
const QUrl &iconPath)
{ {
Q_UNUSED(roomId); Q_UNUSED(roomId);
Q_UNUSED(eventId); Q_UNUSED(eventId);
Q_UNUSED(icon); Q_UNUSED(icon);
Q_UNUSED(iconPath);
NSUserNotification * notif = [[NSUserNotification alloc] init]; NSUserNotification * notif = [[NSUserNotification alloc] init];

View File

@ -46,8 +46,7 @@ NotificationsManager::NotificationsManager(QObject *parent) : QObject(parent) {}
void NotificationsManager::postNotification( void NotificationsManager::postNotification(
const QString &room_id, const QString &event_id, const QString &room_name, const QString &room_id, const QString &event_id, const QString &room_name,
const QString &sender, const QString &text, const QImage &icon, const QString &sender, const QString &text, const QImage &icon) {
const QUrl &iconPath) {
Q_UNUSED(room_id) Q_UNUSED(room_id)
Q_UNUSED(event_id) Q_UNUSED(event_id)
Q_UNUSED(icon) Q_UNUSED(icon)
@ -65,9 +64,6 @@ void NotificationsManager::postNotification(
templ.setTextField(QString("%1").arg(text).toStdWString(), templ.setTextField(QString("%1").arg(text).toStdWString(),
WinToastTemplate::SecondLine); WinToastTemplate::SecondLine);
templ.setImagePath(
reinterpret_cast<const wchar_t *>(QDir::toNativeSeparators(iconPath.toLocalFile()).utf16()));
count++; count++;
CustomHandler *customHandler = new CustomHandler(count, this); CustomHandler *customHandler = new CustomHandler(count, this);
notificationIds[count] = roomEventId{room_id, event_id}; notificationIds[count] = roomEventId{room_id, event_id};

View File

@ -1,3 +0,0 @@
#include "paintable.h"
Paintable::Paintable(QObject *parent) : QObject(parent) {}

View File

@ -1,20 +0,0 @@
#ifndef PAINTABLE_H
#define PAINTABLE_H
#include <QImage>
#include <QObject>
class Paintable : public QObject {
Q_OBJECT
public:
Paintable(QObject* parent = nullptr);
virtual ~Paintable() = default;
virtual QImage image(int) = 0;
virtual QImage image(int, int) = 0;
signals:
void paintableChanged();
};
#endif // PAINTABLE_H

View File

@ -70,24 +70,23 @@ void RoomListModel::connectRoomSignals(SpectralRoom* room) {
[=] { unreadMessagesChanged(room); }); [=] { unreadMessagesChanged(room); });
connect(room, &Room::notificationCountChanged, this, connect(room, &Room::notificationCountChanged, this,
[=] { unreadMessagesChanged(room); }); [=] { unreadMessagesChanged(room); });
connect(room, &Room::avatarChanged, this,
[this, room] { refresh(room, {AvatarRole}); });
connect(room, &Room::tagsChanged, this, [=] { refresh(room); }); connect(room, &Room::tagsChanged, this, [=] { refresh(room); });
connect(room, &Room::joinStateChanged, this, [=] { refresh(room); }); connect(room, &Room::joinStateChanged, this, [=] { refresh(room); });
connect(room, &Room::addedMessages, this, connect(room, &Room::addedMessages, this,
[=] { refresh(room, {LastEventRole}); }); [=] { refresh(room, {LastEventRole}); });
connect(room, &Room::aboutToAddNewMessages, this, connect(room, &Room::notificationCountChanged, this, [=] {
[=](QMatrixClient::RoomEventsRange eventsRange) { if (room->notificationCount() == 0) return;
RoomEvent* event = (eventsRange.end() - 1)->get(); if (room->timelineSize() == 0) return;
User* sender = room->user(event->senderId()); const RoomEvent* lastEvent = room->messageEvents().rbegin()->get();
if (lastEvent->isStateEvent()) return;
User* sender = room->user(lastEvent->senderId());
if (sender == room->localUser()) return; if (sender == room->localUser()) return;
QUrl _url = room->avatarUrl();
emit newMessage( emit newMessage(
room->id(), event->id(), room->displayName(), room->id(), lastEvent->id(), room->displayName(),
sender->displayname(), utils::eventToString(*event), sender->displayname(), utils::eventToString(*lastEvent),
room->avatar(128), room->avatar(128));
QUrl::fromLocalFile(QStandardPaths::writableLocation(
QStandardPaths::CacheLocation) +
"/avatar/" + _url.authority() + '_' +
_url.fileName() + ".png"));
}); });
} }
@ -152,7 +151,7 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const {
} }
SpectralRoom* room = m_rooms.at(index.row()); SpectralRoom* room = m_rooms.at(index.row());
if (role == NameRole) return room->displayName(); if (role == NameRole) return room->displayName();
if (role == PaintableRole) return QVariant::fromValue(room->paintable()); if (role == AvatarRole) return room->avatarMediaId();
if (role == TopicRole) return room->topic(); if (role == TopicRole) return room->topic();
if (role == CategoryRole) { if (role == CategoryRole) {
if (room->joinState() == JoinState::Invite) return RoomType::Invited; if (room->joinState() == JoinState::Invite) return RoomType::Invited;
@ -162,6 +161,7 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const {
return RoomType::Normal; return RoomType::Normal;
} }
if (role == UnreadCountRole) return room->unreadCount(); if (role == UnreadCountRole) return room->unreadCount();
if (role == NotificationCountRole) return room->notificationCount();
if (role == HighlightCountRole) return room->highlightCount(); if (role == HighlightCountRole) return room->highlightCount();
if (role == LastEventRole) return room->lastEvent(); if (role == LastEventRole) return room->lastEvent();
if (role == LastActiveTimeRole) return room->lastActiveTime(); if (role == LastActiveTimeRole) return room->lastActiveTime();
@ -192,10 +192,11 @@ void RoomListModel::unreadMessagesChanged(SpectralRoom* room) {
QHash<int, QByteArray> RoomListModel::roleNames() const { QHash<int, QByteArray> RoomListModel::roleNames() const {
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
roles[NameRole] = "name"; roles[NameRole] = "name";
roles[PaintableRole] = "paintable"; roles[AvatarRole] = "avatar";
roles[TopicRole] = "topic"; roles[TopicRole] = "topic";
roles[CategoryRole] = "category"; roles[CategoryRole] = "category";
roles[UnreadCountRole] = "unreadCount"; roles[UnreadCountRole] = "unreadCount";
roles[NotificationCountRole] = "notificationCount";
roles[HighlightCountRole] = "highlightCount"; roles[HighlightCountRole] = "highlightCount";
roles[LastEventRole] = "lastEvent"; roles[LastEventRole] = "lastEvent";
roles[LastActiveTimeRole] = "lastActiveTime"; roles[LastActiveTimeRole] = "lastActiveTime";

View File

@ -31,10 +31,11 @@ class RoomListModel : public QAbstractListModel {
public: public:
enum EventRoles { enum EventRoles {
NameRole = Qt::UserRole + 1, NameRole = Qt::UserRole + 1,
PaintableRole, AvatarRole,
TopicRole, TopicRole,
CategoryRole, CategoryRole,
UnreadCountRole, UnreadCountRole,
NotificationCountRole,
HighlightCountRole, HighlightCountRole,
LastEventRole, LastEventRole,
LastActiveTimeRole, LastActiveTimeRole,
@ -76,7 +77,7 @@ class RoomListModel : public QAbstractListModel {
void roomAdded(SpectralRoom* room); void roomAdded(SpectralRoom* room);
void newMessage(const QString& roomId, const QString& eventId, void newMessage(const QString& roomId, const QString& eventId,
const QString& roomName, const QString& senderName, const QString& roomName, const QString& senderName,
const QString& text, const QImage& icon, const QUrl& iconPath); const QString& text, const QImage& icon);
}; };
#endif // ROOMLISTMODEL_H #endif // ROOMLISTMODEL_H

View File

@ -9,62 +9,65 @@
#include "events/typingevent.h" #include "events/typingevent.h"
#include <QFileDialog> #include <QFileDialog>
#include <QFileInfo>
#include <QImageReader>
#include <QMetaObject> #include <QMetaObject>
#include <QMimeDatabase> #include <QMimeDatabase>
#include "html.h"
#include "utils.h" #include "utils.h"
SpectralRoom::SpectralRoom(Connection* connection, QString roomId, SpectralRoom::SpectralRoom(Connection* connection, QString roomId,
JoinState joinState) JoinState joinState)
: Room(connection, std::move(roomId), joinState), m_paintable(this) { : Room(connection, std::move(roomId), joinState) {
connect(this, &SpectralRoom::notificationCountChanged, this, connect(this, &SpectralRoom::notificationCountChanged, this,
&SpectralRoom::countChanged); &SpectralRoom::countChanged);
connect(this, &SpectralRoom::highlightCountChanged, this, connect(this, &SpectralRoom::highlightCountChanged, this,
&SpectralRoom::countChanged); &SpectralRoom::countChanged);
connect(this, &Room::addedMessages, this, [=] { setBusy(false); }); connect(this, &Room::addedMessages, this, [=] { setBusy(false); });
connect(this, &Room::fileTransferCompleted, this, [=] {
setFileUploadingProgress(0);
setHasFileUploading(false);
});
}
inline QString getMIME(const QUrl& fileUrl) {
return QMimeDatabase().mimeTypeForFile(fileUrl.toLocalFile()).name();
}
inline QSize getImageSize(const QUrl& imageUrl) {
QImageReader reader(imageUrl.toLocalFile());
return reader.size();
} }
void SpectralRoom::chooseAndUploadFile() { void SpectralRoom::chooseAndUploadFile() {
auto localFile = QFileDialog::getOpenFileUrl(Q_NULLPTR, tr("Save File as")); auto localFile = QFileDialog::getOpenFileUrl(Q_NULLPTR, tr("Save File as"));
if (!localFile.isEmpty()) { if (!localFile.isEmpty()) {
UploadContentJob* job = QString txnID = postFile(localFile.fileName(), localFile, false);
connection()->uploadFile(localFile.toLocalFile(), getMIME(localFile));
if (isJobRunning(job)) {
setHasFileUploading(true); setHasFileUploading(true);
connect(job, &BaseJob::uploadProgress, this, connect(this, &Room::fileTransferCompleted,
[=](qint64 bytesSent, qint64 bytesTotal) { [=](QString id, QUrl localFile, QUrl mxcUrl) {
if (bytesTotal != 0) { if (id == txnID) {
setFileUploadingProgress(bytesSent * 100 / bytesTotal);
}
});
connect(job, &BaseJob::success, this,
[=] { postFile(localFile, job->contentUri()); });
connect(job, &BaseJob::finished, this, [=] {
setHasFileUploading(false);
setFileUploadingProgress(0); setFileUploadingProgress(0);
setHasFileUploading(false);
}
}); });
} else { connect(this, &Room::fileTransferFailed, [=](QString id, QString error) {
qDebug() << "Failed transfer."; if (id == txnID) {
setFileUploadingProgress(0);
setHasFileUploading(false);
} }
});
connect(
this, &Room::fileTransferProgress,
[=](QString id, qint64 progress, qint64 total) {
if (id == txnID) {
qDebug() << "Progress:" << progress << total;
setFileUploadingProgress(int(float(progress) / float(total) * 100));
} }
});
} }
void SpectralRoom::postFile(const QUrl& localFile, const QUrl& mxcUrl) {
const QString mime = getMIME(localFile);
const QString fileName = localFile.fileName();
QString msgType = "m.file";
if (mime.startsWith("image")) msgType = "m.image";
if (mime.startsWith("video")) msgType = "m.video";
if (mime.startsWith("audio")) msgType = "m.audio";
QJsonObject json{QJsonObject{{"msgtype", msgType},
{"body", fileName},
{"filename", fileName},
{"url", mxcUrl.url()}}};
postJson("m.room.message", json);
}
QString SpectralRoom::getMIME(const QUrl& fileUrl) const {
return QMimeDatabase().mimeTypeForFile(fileUrl.toLocalFile()).name();
} }
void SpectralRoom::saveFileAs(QString eventId) { void SpectralRoom::saveFileAs(QString eventId) {
@ -85,16 +88,14 @@ bool SpectralRoom::hasUsersTyping() {
return count != 0; return count != 0;
} }
QString SpectralRoom::getUsersTyping() { QVariantList SpectralRoom::getUsersTyping() {
QString usersTypingStr;
QList<User*> users = usersTyping(); QList<User*> users = usersTyping();
users.removeOne(localUser()); users.removeOne(localUser());
QVariantList out;
for (User* user : users) { for (User* user : users) {
usersTypingStr += user->displayname() + " "; out.append(QVariant::fromValue(user));
} }
usersTypingStr += users.count() < 2 ? "is" : "are"; return out;
usersTypingStr += " typing.";
return usersTypingStr;
} }
void SpectralRoom::sendTypingNotification(bool isTyping) { void SpectralRoom::sendTypingNotification(bool isTyping) {
@ -162,8 +163,6 @@ QDateTime SpectralRoom::lastActiveTime() {
return messageEvents().rbegin()->get()->timestamp(); return messageEvents().rbegin()->get()->timestamp();
} }
float SpectralRoom::orderForTag(QString name) { return tag(name).order; }
int SpectralRoom::savedTopVisibleIndex() const { int SpectralRoom::savedTopVisibleIndex() const {
return firstDisplayedMarker() == timelineEdge() return firstDisplayedMarker() == timelineEdge()
? 0 ? 0
@ -204,3 +203,22 @@ QVariantList SpectralRoom::getUsers(const QString& prefix) {
return matchedList; return matchedList;
} }
QString SpectralRoom::postMarkdownText(const QString& markdown) {
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_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);
free(sequence);
hoedown_buffer_free(html);
hoedown_document_free(document);
hoedown_html_renderer_free(renderer);
return postHtmlText(markdown, result);
}

Some files were not shown because too many files have changed in this diff Show More