diff --git a/ionicons/LICENSE b/ionicons/LICENSE new file mode 100644 index 0000000..015269f --- /dev/null +++ b/ionicons/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Drifty (http://drifty.com/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/ionicons/alert-circled.svg b/ionicons/alert-circled.svg new file mode 100644 index 0000000..89d1143 --- /dev/null +++ b/ionicons/alert-circled.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<!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="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> +<g> + <path d="M476.7,422.2L270.1,72.7c-2.9-5-8.3-8.7-14.1-8.7c-5.9,0-11.3,3.7-14.1,8.7L35.3,422.2c-2.8,5-4.8,13-1.9,17.9 + c2.9,4.9,8.2,7.9,14,7.9h417.1c5.8,0,11.1-3,14-7.9C481.5,435.2,479.5,427.1,476.7,422.2z M288,400h-64v-48h64V400z M288,320h-64 + V176h64V320z"/> +</g> +</svg> diff --git a/ionicons/load-a.svg b/ionicons/load-a.svg new file mode 100644 index 0000000..469054b --- /dev/null +++ b/ionicons/load-a.svg @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<!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="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> +<g> + <circle cx="256" cy="96" r="64"/> + <circle cx="96" cy="256" r="48"/> + <circle cx="368" cy="144" r="8"/> + <path d="M180.1,107.6c-19.9-20.1-52.2-20.1-72.1,0c-19.9,20.1-19.9,52.7,0,72.8c19.9,20.1,52.2,20.1,72.1,0 + C200,160.3,200,127.7,180.1,107.6z"/> + <circle cx="416" cy="256" r="16"/> + <circle cx="369" cy="369" r="24"/> + <circle cx="256" cy="416" r="32"/> + <circle cx="144" cy="368" r="40"/> +</g> +</svg> diff --git a/ionicons/person-add.svg b/ionicons/person-add.svg new file mode 100644 index 0000000..74a4b84 --- /dev/null +++ b/ionicons/person-add.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<!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="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> +<g> + <polygon points="429,328 429,277 480,277 480,235 429,235 429,184 387,184 387,235 336,235 336,277 387,277 387,328 "/> + <path d="M416,448c0,0,0-26.4-2.2-40.2c-1.8-10.9-16.9-25.3-81.1-48.9c-63.2-23.2-59.3-11.9-59.3-54.6c0-27.7,14.1-11.6,23.1-64.2 + c3.5-20.7,6.3-6.9,13.9-40.1c4-17.4-2.7-18.7-1.9-27c0.8-8.3,1.6-15.7,3.1-32.7C313.4,119.3,293.9,64,224,64 + c-69.9,0-89.4,55.3-87.5,76.4c1.5,16.9,2.3,24.4,3.1,32.7c0.8,8.3-5.9,9.6-1.9,27c7.6,33.1,10.4,19.3,13.9,40.1 + c9,52.6,23.1,36.5,23.1,64.2c0,42.8,3.9,31.5-59.3,54.6c-64.2,23.5-79.4,38-81.1,48.9C32,421.6,32,448,32,448h192H416z"/> +</g> +</svg> diff --git a/saori.pro b/saori.pro index a16a457..64e08b5 100644 --- a/saori.pro +++ b/saori.pro @@ -4,7 +4,7 @@ # #------------------------------------------------- -QT += core gui network +QT += core gui network sql greaterThan(QT_MAJOR_VERSION, 4): QT += widgets @@ -29,7 +29,9 @@ saoriview.cpp \ saoriaddaccountdialog.cpp \ saoriaccount.cpp \ - saoriapplication.cpp + saoriapplication.cpp \ + saoricache.cpp \ + saoriviewentry.cpp HEADERS += \ saoriwindow.h \ @@ -38,7 +40,9 @@ saoriaddaccountdialog.h \ saoridef.h \ saoriaccount.h \ - saoriapplication.h + saoriapplication.h \ + saoricache.h \ + saoriviewentry.h FORMS += \ saoriwindow.ui \ diff --git a/saori.qrc b/saori.qrc index fb65ba0..7626b56 100644 --- a/saori.qrc +++ b/saori.qrc @@ -1,5 +1,7 @@ <RCC> - <qresource prefix="/lang"/> + <qresource prefix="/lang" lang="ja"> + <file alias="saori.qm">saori_ja.qm</file> + </qresource> <qresource prefix="/icons"> <file>ionicons/heart.svg</file> <file>ionicons/trash-a.svg</file> @@ -26,5 +28,12 @@ <file>ionicons/chatbubble.svg</file> <file>ionicons/share.svg</file> <file>ionicons/reply.svg</file> + <file>saori.svg</file> + <file>ionicons/load-a.svg</file> + <file>ionicons/alert-circled.svg</file> + <file>ionicons/person-add.svg</file> + </qresource> + <qresource prefix="/css"> + <file>saoristyle.css</file> </qresource> </RCC> diff --git a/saori.svg b/saori.svg new file mode 100644 index 0000000..3a6af8d --- /dev/null +++ b/saori.svg @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> + +<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" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="512px" + height="512px" + viewBox="0 0 512 512" + enable-background="new 0 0 512 512" + xml:space="preserve" + inkscape:version="0.91 r13725" + sodipodi:docname="tshirt-outline.svg"><metadata + id="metadata9"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs7" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1543" + inkscape:window-height="983" + id="namedview5" + showgrid="false" + inkscape:zoom="0.91895868" + inkscape:cx="256" + inkscape:cy="256" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1" /><path + d="M178.249,85.534c18.761,22.976,43.909,34.009,77.445,34.572l0.269-0.106h0.269c22.638,0,41.169-4.861,56.653-15.091 c7.801-5.154,14.721-11.47,21.029-19.382l104.461,31.367L426.59,149.41l-38.743-4.685l-37.912-4.589l2.114,38.133L366.176,433 H145.824l14.127-254.729l2.106-37.981l-37.782,4.428l-38.854,4.553l-11.767-32.358L178.249,85.534 M320,48 c-13.988,27.227-30.771,40.223-63.769,40.223C223.723,87.676,205.722,75,192,48L32,96l32,88l64-7.5L112,465h288l-16-288.5l64,7.75 L480,96L320,48L320,48z" + id="path3" /><g + transform="matrix(5.3976179,0,0,5.5513808,-953.6893,-709.35688)" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:125%;font-family:'源ノ角ゴシック JP';-inkscape-font-specification:'源ノ角ゴシック JP, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="flowRoot3337"><path + d="m 241.17234,168.23562 q -3.60007,-0.60001 -8.24015,-0.72001 l 0,14.72027 q 3.84007,2.04004 8.56016,5.92011 l -1.84004,2.16005 q -3.84007,-3.08006 -6.72012,-4.9201 -0.16001,4.12008 -2.20005,5.88011 -2.00003,1.76004 -6.48012,1.76004 -4.00007,0 -6.28012,-1.80004 -2.24004,-1.84003 -2.24004,-5.00009 0,-3.08006 2.20004,-4.80009 2.24005,-1.72003 6.32012,-1.72003 2.80005,0 5.76011,1.20002 l 0,-16.1203 q 6.48012,0 11.52022,0.80001 l -0.36001,2.64005 z m -34.32064,-0.80001 0,-2.64005 7.40013,0 q 0.72002,-2.32005 1.76004,-6.08012 l 2.76005,0.40001 q -0.68001,2.76005 -1.56003,5.68011 l 7.52014,0 0,2.64005 -8.32015,0 q -3.24007,10.20019 -7.72015,20.32038 l -2.68005,-0.96002 q 4.36008,-10.00019 7.40014,-19.36036 l -6.56012,0 z m 23.16043,16.3203 q -3.04006,-1.44002 -5.76011,-1.44002 -5.6001,0 -5.6001,3.92007 0,2.04004 1.44002,3.16006 1.48003,1.12002 4.16008,1.12002 3.32006,0 4.52009,-1.20002 1.24002,-1.24003 1.24002,-4.60009 l 0,-0.96002 z" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40.0007515px;line-height:125%;font-family:'源ノ角ゴシック JP';-inkscape-font-specification:'源ノ角ゴシック JP, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path3346" /></g></svg> \ No newline at end of file diff --git a/saori_ja.qm b/saori_ja.qm new file mode 100644 index 0000000..beedf00 --- /dev/null +++ b/saori_ja.qm Binary files differ diff --git a/saori_ja.ts b/saori_ja.ts index 7e676a5..461f047 100644 --- a/saori_ja.ts +++ b/saori_ja.ts @@ -6,47 +6,47 @@ <message> <location filename="saoriaddaccountdialog.ui" line="14"/> <source>Add new Account</source> - <translation type="unfinished"></translation> + <translation>アカウントを追加</translation> </message> <message> <location filename="saoriaddaccountdialog.ui" line="22"/> <source>Instance URL:</source> - <translation type="unfinished"></translation> + <translation>インスタンスURL:</translation> </message> <message> <location filename="saoriaddaccountdialog.ui" line="29"/> <source>Get Access token:</source> - <translation type="unfinished"></translation> + <translation>アクセストークンを取得:</translation> </message> <message> <location filename="saoriaddaccountdialog.ui" line="36"/> <source>Open Authorize page:</source> - <translation type="unfinished"></translation> + <translation>認証ページを開く:</translation> </message> <message> <location filename="saoriaddaccountdialog.ui" line="71"/> <source>Check</source> - <translation type="unfinished"></translation> + <translation>確認</translation> </message> <message> <location filename="saoriaddaccountdialog.ui" line="88"/> <source>Get!</source> - <translation type="unfinished"></translation> + <translation>取得!</translation> </message> <message> <location filename="saoriaddaccountdialog.ui" line="43"/> <source>Authorize Code:</source> - <translation type="unfinished"></translation> + <translation>認証コード:</translation> </message> <message> <location filename="saoriaddaccountdialog.ui" line="78"/> <source>Open WebBrowser</source> - <translation type="unfinished"></translation> + <translation>ブラウザを開く</translation> </message> <message> <location filename="saoriaddaccountdialog.ui" line="57"/> <source>Account:</source> - <translation type="unfinished"></translation> + <translation>アカウント:</translation> </message> </context> <context> @@ -57,33 +57,84 @@ <translation type="unfinished"></translation> </message> <message> - <location filename="saoriview.ui" line="43"/> + <location filename="saoriview.ui" line="67"/> <source>Pin</source> - <translation type="unfinished"></translation> + <translation>ピン</translation> </message> <message> - <location filename="saoriview.ui" line="57"/> + <location filename="saoriview.ui" line="81"/> <source>Notify</source> - <translation type="unfinished"></translation> + <translation>通知</translation> </message> <message> - <location filename="saoriview.ui" line="71"/> + <location filename="saoriview.ui" line="95"/> <source>Auto reload</source> - <translation type="unfinished"></translation> + <translation>自動更新</translation> </message> <message> - <location filename="saoriview.ui" line="85"/> + <location filename="saoriview.ui" line="109"/> <source>Newest</source> - <translation type="unfinished"></translation> + <translation>最新</translation> </message> <message> - <location filename="saoriview.ui" line="96"/> + <location filename="saoriview.ui" line="120"/> <source>Reload</source> + <translation>更新</translation> + </message> + <message> + <location filename="saoriview.ui" line="150"/> + <source>Toot</source> + <translation>トゥート</translation> + </message> + <message> + <location filename="saoriview.cpp" line="107"/> + <source><div>%1</div><div>%2</div><div>%3</div><div>email : %4</div><div>%5</div><div>version : %6</div></source> <translation type="unfinished"></translation> </message> <message> - <location filename="saoriview.ui" line="126"/> - <source>Toot</source> + <location filename="saoriview.cpp" line="138"/> + <source>boosted by: %1</source> + <translation>%1 さんがブースト</translation> + </message> + <message> + <location filename="saoriview.cpp" line="142"/> + <location filename="saoriview.cpp" line="266"/> + <source>created at :</source> + <translation>投稿日時 :</translation> + </message> + <message> + <location filename="saoriview.cpp" line="177"/> + <source> following: </source> + <translation> フォロー: </translation> + </message> + <message> + <location filename="saoriview.cpp" line="179"/> + <source> followers: </source> + <translation> フォロワー: </translation> + </message> + <message> + <location filename="saoriview.cpp" line="247"/> + <source>%1 mentioned your status.</source> + <translation>%1 さんが言及</translation> + </message> + <message> + <location filename="saoriview.cpp" line="251"/> + <source>%1 boosted your status.</source> + <translation>%1 さんがブースト</translation> + </message> + <message> + <location filename="saoriview.cpp" line="255"/> + <source>%1 favourited your status.</source> + <translation>%1 さんがお気に入りに追加</translation> + </message> + <message> + <location filename="saoriview.cpp" line="259"/> + <source>%1 followed you.</source> + <translation>%1 さんにフォローされました</translation> + </message> + <message> + <location filename="saoriview.cpp" line="278"/> + <source>%1</source> <translation type="unfinished"></translation> </message> </context> @@ -91,113 +142,128 @@ <name>SaoriWindow</name> <message> <location filename="saoriwindow.ui" line="14"/> - <source>SaoriWindow</source> + <source>Saori</source> + <oldsource>SaoriWindow</oldsource> <translation type="unfinished"></translation> </message> <message> - <location filename="saoriwindow.ui" line="59"/> + <location filename="saoriwindow.ui" line="63"/> <source>&File</source> <translation type="unfinished">ファイル(&F)</translation> </message> <message> - <location filename="saoriwindow.ui" line="65"/> + <location filename="saoriwindow.ui" line="69"/> <source>&View</source> <translation type="unfinished">表示(&V)</translation> </message> <message> - <location filename="saoriwindow.ui" line="71"/> + <location filename="saoriwindow.ui" line="77"/> <source>&Edit</source> <translation type="unfinished">編集(&E)</translation> </message> <message> - <location filename="saoriwindow.ui" line="79"/> + <location filename="saoriwindow.ui" line="85"/> <source>&Help</source> <translation type="unfinished">ヘルプ(&H)</translation> </message> <message> - <location filename="saoriwindow.ui" line="113"/> + <location filename="saoriwindow.ui" line="119"/> <source>Timeline &list</source> <translation type="unfinished">タイムラインリスト(&l)</translation> </message> <message> - <location filename="saoriwindow.ui" line="132"/> + <location filename="saoriwindow.ui" line="138"/> <source>Auto reload Interval (minutes)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="saoriwindow.ui" line="146"/> + <location filename="saoriwindow.ui" line="152"/> <source>List</source> <translation type="unfinished">リスト</translation> </message> <message> - <location filename="saoriwindow.ui" line="151"/> + <location filename="saoriwindow.ui" line="157"/> <source>Account</source> <translation type="unfinished"></translation> </message> <message> - <location filename="saoriwindow.ui" line="156"/> + <location filename="saoriwindow.ui" line="162"/> <source>Instance</source> <translation type="unfinished"></translation> </message> <message> - <location filename="saoriwindow.ui" line="166"/> + <location filename="saoriwindow.ui" line="172"/> <source>&Quit</source> <translation type="unfinished"></translation> </message> <message> - <location filename="saoriwindow.ui" line="177"/> + <location filename="saoriwindow.ui" line="183"/> <source>Show/Hide Timeline &List</source> <translation type="unfinished"></translation> </message> <message> - <location filename="saoriwindow.ui" line="182"/> + <location filename="saoriwindow.ui" line="188"/> <source>&Open Timeline</source> <translation type="unfinished"></translation> </message> <message> - <location filename="saoriwindow.ui" line="187"/> + <location filename="saoriwindow.ui" line="193"/> <source>&Add Account</source> <translation type="unfinished"></translation> </message> <message> - <location filename="saoriwindow.ui" line="192"/> + <location filename="saoriwindow.ui" line="198"/> <source>&Remove Account</source> <translation type="unfinished"></translation> </message> <message> - <location filename="saoriwindow.cpp" line="59"/> + <location filename="saoriwindow.ui" line="206"/> + <source>&TabbedView mode</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="saoriwindow.ui" line="211"/> + <source>T&iled</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="saoriwindow.cpp" line="136"/> <source>Accounts</source> <translation type="unfinished">アカウント</translation> </message> <message> - <location filename="saoriwindow.cpp" line="72"/> + <location filename="saoriwindow.cpp" line="57"/> <source>home</source> <translation type="unfinished">ホーム</translation> </message> <message> - <location filename="saoriwindow.cpp" line="76"/> + <location filename="saoriwindow.cpp" line="61"/> <source>local</source> <translation type="unfinished">ローカルタイムライン</translation> </message> <message> - <location filename="saoriwindow.cpp" line="80"/> + <location filename="saoriwindow.cpp" line="65"/> <source>public</source> <translation type="unfinished">連合タイムライン</translation> </message> <message> - <location filename="saoriwindow.cpp" line="84"/> + <location filename="saoriwindow.cpp" line="69"/> <source>notifications</source> <translation type="unfinished">通知</translation> </message> <message> - <location filename="saoriwindow.cpp" line="98"/> + <location filename="saoriwindow.cpp" line="73"/> + <source>instance</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="saoriwindow.cpp" line="151"/> <source>Instances</source> <translation type="unfinished">インスタンス</translation> </message> <message> - <location filename="saoriwindow.cpp" line="102"/> <source>Infomation</source> - <translation type="unfinished">情報</translation> + <translation type="obsolete">情報</translation> </message> </context> </TS> diff --git a/saoriaccount.cpp b/saoriaccount.cpp index 201ba0f..a9009bd 100644 --- a/saoriaccount.cpp +++ b/saoriaccount.cpp @@ -25,6 +25,8 @@ ***/ #include "saoriaccount.h" +#include "saoriapplication.h" +#include "saoriview.h" #include "saoridef.h" SaoriAccount::SaoriAccount(const QString accountName, Saoridon *instance, const QString accsessToken, QObject *parent) : QObject(parent) @@ -64,21 +66,16 @@ void SaoriAccount::getAccountInfomation() { - auto manager = new QNetworkAccessManager(); QNetworkRequest request = createHearder(); request.setUrl(QUrl(m_instance->instance().url() + SAORI_MASTODON_APIPATH_ACCOUNTS + "/verify_credentials")); - auto *reply = manager->get(request); - connect(manager,&QNetworkAccessManager::finished,[=](){ - if (reply->NoError == QNetworkReply::NoError) { + auto *reply = SaoriApplication::saori()->manager->get(request); + connect(reply,&QNetworkReply::finished,[=](){ + if (reply->error() == QNetworkReply::NoError) { QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); - QMap<QString,QString> info; - for(auto it = json.begin();it != json.end();it ++) { - info[it.key()] = it.value().toString(); - } - m_accountInfo = info; + m_accountInfo = json; emit accountInfomationChanged(); } - manager->deleteLater(); + reply->deleteLater(); }); return; } @@ -90,7 +87,7 @@ return request; } -const QString SaoriAccount::accountInfo(const QString key) +const QJsonValue SaoriAccount::accountInfo(const QString key) { return m_accountInfo[key]; } @@ -106,3 +103,23 @@ return tl; } + +void SaoriAccount::getTimelineData(const QString timeline, const QUrlQuery query) +{ + // TODO max_id、since_id、limitの処理を追加すべし。 + QUrl url = instance()->timelineUrl(timeline); + if (url.isEmpty()) return; + QNetworkRequest request = createHearder(); + QUrlQuery q = query; + q.addQueryItem("limit","10"); + request.setUrl(instance()->addQuery(url,q)); + auto *reply = SaoriApplication::saori()->manager->get(request); + connect(reply,&QNetworkReply::finished,[=](){ + if (reply->error() == QNetworkReply::NoError) { + QByteArray data = reply->readAll(); + emit apiData(timeline,data); + } + reply->deleteLater(); + }); + return; +} diff --git a/saoriaccount.h b/saoriaccount.h index 6adbb17..61faf51 100644 --- a/saoriaccount.h +++ b/saoriaccount.h @@ -32,8 +32,8 @@ #include <saoridon.h> #include <QJsonDocument> #include <QJsonObject> -#include <QNetworkAccessManager> #include <QNetworkReply> +#include <QUrlQuery> class SaoriAccount : public QObject { @@ -48,23 +48,26 @@ const QString name(); void setName(const QString name); Saoridon *instance(); - const QString accountInfo(const QString key); + const QJsonValue accountInfo(const QString key); const QStringList timelineList(); +public slots: + void getTimelineData(const QString timeline,const QUrlQuery query); + protected: void getAccountInfomation(); QNetworkRequest createHearder(); - void requestGET(QUrl url,QNetworkAccessManager *manager); protected: QString m_name; QString m_accessToken; Saoridon *m_instance; - QMap<QString,QString> m_accountInfo; + QJsonObject m_accountInfo; signals: void accessTokenChanged(QString); void accountInfomationChanged(); + void apiData(const QString timeline,const QByteArray data); public slots: }; diff --git a/saoriaddaccountdialog.cpp b/saoriaddaccountdialog.cpp index 828f04a..a26ea72 100644 --- a/saoriaddaccountdialog.cpp +++ b/saoriaddaccountdialog.cpp @@ -29,8 +29,8 @@ #include "saoridef.h" #include "ui_saoriaddaccountdialog.h" #include <QDesktopServices> -#include <QtNetwork/QNetworkAccessManager> -#include <QtNetwork/QNetworkReply> +#include <QNetworkAccessManager> +#include <QNetworkReply> #include <QJsonDocument> #include <QJsonObject> #include <QEventLoop> @@ -49,7 +49,7 @@ ui->getAccessToken_pushButton->setEnabled(false); ui->authorizeCode_lineEdit->setEnabled(false); connect(ui->instanceUrl_comboBox,&QComboBox::editTextChanged,[this](QString) { - m_instance = SaoriApplication::findInstance(ui->instanceUrl_comboBox->currentText()); + m_instance = SaoriApplication::findInstance(QUrl(ui->instanceUrl_comboBox->currentText())); if (m_instance != nullptr) ui->openWebBrowser_pushButton->setEnabled(true); else ui->openWebBrowser_pushButton->setEnabled(false); }); @@ -65,7 +65,8 @@ m_instance = SaoriApplication::findInstance(ui->instanceUrl_comboBox->currentText()); if (m_instance == nullptr) return; if (m_instance->clientId().isEmpty() || m_instance->clientSecret().isEmpty()) { - qDebug() << "regist"; + // FIXED インスタンスは記録されているがclientが登録されていない状態。 + return; } QDesktopServices::openUrl(m_instance->getAuthorizedUrl()); QString text = ui->instanceUrl_comboBox->currentText(); @@ -85,24 +86,23 @@ if (token.isEmpty()) return; m_account = new SaoriAccount("check",m_instance,token); - auto manager = new QNetworkAccessManager(); QEventLoop event; - connect(manager,&QNetworkAccessManager::finished,&event,&QEventLoop::quit); QUrlQuery params; QNetworkRequest request; - QString instance = m_instance->instanceInfo("uri"); + QString instance = m_instance->instanceInfo("uri").toString(); request.setRawHeader("Authorization","Bearer " + token.toLatin1()); request.setUrl(QUrl(m_instance->instance().url() + SAORI_MASTODON_APIPATH_ACCOUNTS + "/verify_credentials")); - auto reply = manager->get(request); + auto reply = SaoriApplication::saori()->manager->get(request); + connect(reply,&QNetworkReply::finished,&event,&QEventLoop::quit); event.exec(); if (reply->error() != QNetworkReply::NoError) { - manager->deleteLater(); + reply->deleteLater(); return; } QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); QString account = json["acct"].toString(); - manager->deleteLater(); + reply->deleteLater(); m_account->setName(account + "@" + instance); ui->account_lineEdit->setText(m_account->name()); @@ -110,3 +110,22 @@ return; } + +void SaoriAddAccountDialog::on_check_pushButton_clicked() +{ + if (SaoriApplication::findInstance(QUrl(ui->instanceUrl_comboBox->currentText())) == nullptr) { + auto instance = new Saoridon(QUrl(ui->instanceUrl_comboBox->currentText()),this); + connect(instance,&Saoridon::instanceInfomationChanged,this,[=](){ + if (!instance->instanceInfo("uri").toString().isEmpty()) { + instance->clientRedistration(); + if (!instance->clientSecret().isEmpty()) { + instance->setParent(SaoriApplication::saori()); + SaoriApplication::getInstanceList()->append(instance); + ui->instanceUrl_comboBox->addItem(instance->instance().toString()); + ui->instanceUrl_comboBox->setEditText(instance->instance().toString()); + ui->openWebBrowser_pushButton->setEnabled(true); + } + } + }); + } +} diff --git a/saoriaddaccountdialog.h b/saoriaddaccountdialog.h index ad5c13c..a94021e 100644 --- a/saoriaddaccountdialog.h +++ b/saoriaddaccountdialog.h @@ -52,6 +52,8 @@ void on_getAccessToken_pushButton_clicked(); + void on_check_pushButton_clicked(); + private: Ui::SaoriAddAccountDialog *ui; }; diff --git a/saoriaddaccountdialog.ui b/saoriaddaccountdialog.ui index 11d3d82..6647133 100644 --- a/saoriaddaccountdialog.ui +++ b/saoriaddaccountdialog.ui @@ -66,7 +66,7 @@ </widget> </item> <item row="2" column="3"> - <widget class="QPushButton" name="pushButton"> + <widget class="QPushButton" name="check_pushButton"> <property name="text"> <string>Check</string> </property> diff --git a/saoriapplication.cpp b/saoriapplication.cpp index 2fdd9d7..6def0c8 100644 --- a/saoriapplication.cpp +++ b/saoriapplication.cpp @@ -41,27 +41,41 @@ setOrganizationDomain(SAORI_QSETTINGS_DOMAIN); setApplicationName(SAORI_QSETTINGS_APPLICATION); + manager = new QNetworkAccessManager; + m_config = new QSettings(SAORI_QSETTINGS_DOMAIN,SAORI_QSETTINGS_APPLICATION,this); loadSettings(); QDir dir; - if (!dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))) { - // CacheLocation is not writable. + if (dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))) { + m_cacheDirectory = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + } else { + m_cacheDirectory = dir; } + if (dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation))) { + m_dataDirectory = QDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); + } else { + m_dataDirectory = dir; + } + m_db = QSqlDatabase::addDatabase("QSQLITE"); + m_db.setDatabaseName(m_dataDirectory.absolutePath() + "/" + SAORI_SQLFILE); + m_db.open(); + + m_cache = new SaoriCache(this); } SaoriApplication::~SaoriApplication() { saveInstancesSettings(); saveAccountsSettings(); + m_db.close(); } Saoridon *SaoriApplication::findInstance(QUrl instance) { for(int i = 0;i < getInstanceList()->count();i ++) { if (getInstanceList()->at(i)->instance() == instance) return getInstanceList()->at(i); - break; } return nullptr; } @@ -70,7 +84,6 @@ { for(int i = 0;i < getAccountList()->count();i ++) { if (getAccountList()->at(i)->name() == account) return getAccountList()->at(i); - break; } return nullptr; } @@ -90,6 +103,31 @@ return m_self; } +QDir SaoriApplication::cacheDirectory() +{ + return m_cacheDirectory; +} + +QDir SaoriApplication::dataDirectory() +{ + return m_dataDirectory; +} + +QSqlDatabase *SaoriApplication::database() +{ + return &m_db; +} + +SaoriCache *SaoriApplication::cache() +{ + return m_cache; +} + +QSettings *SaoriApplication::setting() +{ + return m_config; +} + void SaoriApplication::loadSettings() { m_config->beginGroup("instances"); @@ -124,7 +162,7 @@ m_config->remove("instances"); m_config->beginGroup("instances"); for(int i = 0;i < m_instanceList.count();i ++) { - m_config->beginGroup(QCryptographicHash::hash(m_instanceList[i]->instance().toString().toLatin1(),QCryptographicHash::Md5)); + m_config->beginGroup(QCryptographicHash::hash(m_instanceList[i]->instance().toString().toLatin1(),QCryptographicHash::Md5).toHex()); m_config->setValue("url",m_instanceList[i]->instance()); m_config->setValue("clientId",m_instanceList[i]->clientId()); m_config->setValue("clientSecret",m_instanceList[i]->clientSecret()); diff --git a/saoriapplication.h b/saoriapplication.h index 9d73de0..62ac0bd 100644 --- a/saoriapplication.h +++ b/saoriapplication.h @@ -30,8 +30,12 @@ #include <QApplication> #include <saoridon.h> #include <saoriaccount.h> +#include <saoricache.h> #include <QList> #include <QSettings> +#include <QDir> +#include <QSqlDatabase> +#include <QNetworkAccessManager> class SaoriApplication : public QApplication { @@ -44,12 +48,24 @@ static QList<Saoridon *> * getInstanceList(); static QList<SaoriAccount *> * getAccountList(); static SaoriApplication * saori(); + QDir cacheDirectory(); + QDir dataDirectory(); + QSqlDatabase * database(); + SaoriCache * cache(); + QSettings * setting(); + +public: + QNetworkAccessManager * manager; protected: QSettings *m_config; QList<Saoridon *> m_instanceList; QList<SaoriAccount *> m_accountList; static SaoriApplication *m_self; + QDir m_cacheDirectory; + QDir m_dataDirectory; + QSqlDatabase m_db; + SaoriCache *m_cache; protected: void loadSettings(); diff --git a/saoricache.cpp b/saoricache.cpp new file mode 100644 index 0000000..5bfd93b --- /dev/null +++ b/saoricache.cpp @@ -0,0 +1,145 @@ +/*** + +The MIT License + +Copyright (c) 2018 Teppei Tamra (TAM) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +***/ + +#include "saoricache.h" +#include "saoriapplication.h" +#include <QCryptographicHash> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QFile> +#include <QDir> +#include <QSqlDatabase> +#include <QSqlQuery> +#include <QSqlResult> +#include <QDateTime> + +/* + * table file_cache + * url(text unique) + * filename(text) + * size(integer) + * timestamp(integer) + * + */ + +QList<QUrl> SaoriCache::m_nowloading; + +SaoriCache::SaoriCache(QObject *parent) : QObject(parent) +{ + migration(); +} + +void SaoriCache::removeFileCache(const QUrl url) +{ + QSqlQuery query(*SaoriApplication::saori()->database()); + query.prepare("DELETE from file_cache WHERE url=?;"); + query.addBindValue(url.toString()); + query.exec(); + QFile localfile(urlToFilename(url)); + if (localfile.exists()) localfile.remove(); +} + +void SaoriCache::reloadFileCache(const QUrl url) +{ + removeFileCache(url); + download(url); +} + +const QString SaoriCache::fileCache(const QUrl url) +{ + if (isCached(url)) return urlToFilename(url); + else download(url); + return QString(":/icons/ionicons/load-a.svg"); +} + +bool SaoriCache::migration() +{ + if (SaoriApplication::saori()->database()->tables().contains("file_cache")) return true; + QSqlQuery query(*SaoriApplication::saori()->database()); + return query.exec("CREATE TABLE file_cache(url TEXT UNIQUE,filename TEXT,size INTEGER,timestamp INTEGER);"); +} + +void SaoriCache::download(const QUrl url) +{ + if (m_nowloading.contains(url)) return; + if (isCached(url)) return; + m_nowloading.append(url); + QNetworkRequest request; + request.setUrl(url); + auto *reply = SaoriApplication::saori()->manager->get(request); + connect(reply,&QNetworkReply::finished,this,[=](){ + if (reply->error() == QNetworkReply::NoError) { + QFile localfile(urlToFilename(url)); + if (localfile.exists()) localfile.remove(); + localfile.open(QIODevice::WriteOnly); + localfile.write(reply->readAll()); + // TODO 100MBのファイルに100MBのメモリを使うのは如何なものか。 + qint64 size = localfile.size(); + localfile.close(); + QSqlQuery query(*SaoriApplication::saori()->database()); + query.prepare("INSERT INTO file_cache(url,filename,size,timestamp) values(?,?,?,?);"); + query.addBindValue(url.toString()); + query.addBindValue(urlToFilename(url)); + query.addBindValue(size); + query.addBindValue(QDateTime::currentSecsSinceEpoch()); + query.exec(); + emit downloaded(url); + } else { + QSqlQuery query(*SaoriApplication::saori()->database()); + query.prepare("INSERT INTO file_cache(url,filename,size,timestamp) values(?,?,?,?);"); + query.addBindValue(url.toString()); + query.addBindValue(":/icons/ionicons/alert-circled.svg"); + query.addBindValue("-1"); + query.addBindValue(QDateTime::currentSecsSinceEpoch()); + query.exec(); + emit downloaded(url); + } + m_nowloading.removeAll(url); + reply->deleteLater(); + }); +} + +bool SaoriCache::isCached(const QUrl url) +{ + QSqlQuery query(*SaoriApplication::saori()->database()); + query.prepare("SELECT size from file_cache WHERE url=?;"); + query.addBindValue(url.toString()); + query.exec(); + if (query.next()) { + if (query.value(0).toInt() >= 0) return true; + } + return false; +} + +const QString SaoriCache::urlToFilename(QUrl url) +{ + QString dir = QString::fromUtf8(QCryptographicHash::hash(url.adjusted(QUrl::RemovePath).toString().toLatin1(),QCryptographicHash::Md5).toHex()); + QString file = QString::fromUtf8(QCryptographicHash::hash(url.path().toLatin1(),QCryptographicHash::Md5).toHex()); + QDir d; + d.mkpath(SaoriApplication::saori()->cacheDirectory().absolutePath() + "/" + dir); + return QString(SaoriApplication::saori()->cacheDirectory().absolutePath() + "/" + dir + "/" + file); +} diff --git a/saoricache.h b/saoricache.h new file mode 100644 index 0000000..834e139 --- /dev/null +++ b/saoricache.h @@ -0,0 +1,59 @@ +/*** + +The MIT License + +Copyright (c) 2018 Teppei Tamra (TAM) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +***/ + +#ifndef SAORICACHE_H +#define SAORICACHE_H + +#include <QObject> +#include <QUrl> +#include <QList> + +class SaoriCache : public QObject +{ + Q_OBJECT +public: + explicit SaoriCache(QObject *parent = nullptr); + void removeFileCache(const QUrl url); + void reloadFileCache(const QUrl url); + const QString fileCache(const QUrl url); + +protected: + static QList<QUrl> m_nowloading; + +protected: + bool migration(); + void download(const QUrl url); + bool isCached(const QUrl url); + static const QString urlToFilename(QUrl url); + const QString localfileName(const QUrl url); + +signals: + void downloaded(const QUrl); + +public slots: +}; + +#endif // SAORICACHE_H diff --git a/saoridef.h b/saoridef.h index 234bd60..9873a59 100644 --- a/saoridef.h +++ b/saoridef.h @@ -35,12 +35,15 @@ #define SAORI_QSETTINGS_DOMAIN "net-p.org" #define SAORI_QSETTINGS_APPLICATION "Saori" +#define SAORI_SQLFILE "saoridatastore.sqlite" + #define SAORI_MASTODON_APIPATH_APPS "/api/v1/apps" #define SAORI_MASTODON_APIPATH_AUTHORIZE "/oauth/authorize" #define SAORI_MASTODON_APIPATH_TOKEN "/oauth/token" #define SAORI_MASTODON_APIPATH_INSTANCE "/api/v1/instance" #define SAORI_MASTODON_APIPATH_ACCOUNTS "/api/v1/accounts" -#define SAORI_MASTODON_APIPATH_TIMELINE "/api/v1/timeline" +#define SAORI_MASTODON_APIPATH_TIMELINE "/api/v1/timelines" +#define SAORI_MASTODON_APIPATH_NOTIFICATION "/api/v1/notifications" #endif // SAORIDEF_H diff --git a/saoridon.cpp b/saoridon.cpp index 7a9f86b..dd99481 100644 --- a/saoridon.cpp +++ b/saoridon.cpp @@ -25,11 +25,13 @@ ***/ #include "saoridon.h" +#include "saoriapplication.h" #include "saoridef.h" -#include <QtNetwork/QNetworkAccessManager> -#include <QtNetwork/QNetworkReply> +#include <QNetworkAccessManager> +#include <QNetworkReply> #include <QJsonDocument> -#include <QJsonObject> + +#include <QJsonArray> #include <QEventLoop> #include <QUrlQuery> #include <QDebug> @@ -74,9 +76,7 @@ bool Saoridon::clientRedistration() { - auto manager = new QNetworkAccessManager(); QEventLoop event; - connect(manager,SIGNAL(finished(QNetworkReply*)),&event,SLOT(quit())); QUrlQuery params; params.addQueryItem("client_name",SAORI_CLIENT_NAME); params.addQueryItem("redirect_uris",SAORI_CLIENT_REDIRECT_URI); @@ -85,10 +85,11 @@ QNetworkRequest request; request.setHeader(QNetworkRequest::ContentTypeHeader,QVariant("application/x-www-form-urlencoded")); request.setUrl(QUrl(instance().url() + SAORI_MASTODON_APIPATH_APPS)); - auto *reply = manager->post(request,params.toString().toLatin1()); + auto *reply = SaoriApplication::saori()->manager->post(request,params.toString().toLatin1()); + connect(reply,&QNetworkReply::finished,&event,&QEventLoop::quit); event.exec(); if (reply->error() != QNetworkReply::NoError) { - manager->deleteLater(); + reply->deleteLater(); return false; } QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); @@ -97,7 +98,7 @@ if (id.isEmpty() || secret.isEmpty()) return false; setClientId(id); setClientSecret(secret); - manager->deleteLater(); + reply->deleteLater(); return true; } @@ -119,9 +120,7 @@ const QString Saoridon::getAccessToken(const QString code) { - auto manager = new QNetworkAccessManager(); QEventLoop event; - connect(manager,&QNetworkAccessManager::finished,&event,&QEventLoop::quit); QUrlQuery params; params.addQueryItem("grant_type","authorization_code"); params.addQueryItem("redirect_uri","urn:ietf:wg:oauth:2.0:oob"); @@ -131,46 +130,41 @@ QNetworkRequest request; request.setHeader(QNetworkRequest::ContentTypeHeader,QVariant("application/x-www-form-urlencoded")); request.setUrl(QUrl(instance().url() + SAORI_MASTODON_APIPATH_TOKEN)); - auto *reply = manager->post(request,params.toString().toLatin1()); + auto *reply = SaoriApplication::saori()->manager->post(request,params.toString().toLatin1()); + connect(reply,&QNetworkReply::finished,&event,&QEventLoop::quit); event.exec(); if (reply->error() != QNetworkReply::NoError) { - manager->deleteLater(); + reply->deleteLater(); return QString(); } QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); QString token = json["access_token"].toString(); - manager->deleteLater(); + reply->deleteLater(); return token; } void Saoridon::getInstanceInfomation() { - auto manager = new QNetworkAccessManager(); QNetworkRequest request; request.setUrl(QUrl(instance().url() + SAORI_MASTODON_APIPATH_INSTANCE)); - auto *reply = manager->get(request); - connect(manager,&QNetworkAccessManager::finished,[=](){ - if (reply->NoError == QNetworkReply::NoError) { - QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); - QMap<QString,QString> info; - for (auto it = json.begin();it != json.end();it ++) { - info[it.key()] = it.value().toString(); - } - m_instanceInfo = info; + auto *reply = SaoriApplication::saori()->manager->get(request); + connect(reply,&QNetworkReply::finished,[=](){ + if (reply->error() == QNetworkReply::NoError) { + m_instanceInfo = QJsonDocument::fromJson(reply->readAll()).object(); emit instanceInfomationChanged(); } - manager->deleteLater(); + reply->deleteLater(); }); m_timelineMap.clear(); m_timelineMap["home"] = QUrl(instance().url() + SAORI_MASTODON_APIPATH_TIMELINE + "/home"); - m_timelineMap["local"] = QUrl(instance().url() + SAORI_MASTODON_APIPATH_TIMELINE + "/local"); + m_timelineMap["local"] = QUrl(instance().url() + SAORI_MASTODON_APIPATH_TIMELINE + "/public?local=1"); m_timelineMap["public"] = QUrl(instance().url() + SAORI_MASTODON_APIPATH_TIMELINE + "/public"); - m_timelineMap["notifications"] = QUrl(instance().url() + SAORI_MASTODON_APIPATH_TIMELINE + "/notifications"); + m_timelineMap["notifications"] = QUrl(instance().url() + SAORI_MASTODON_APIPATH_NOTIFICATION + "/"); return; } -const QString Saoridon::instanceInfo(const QString key) +const QJsonValue Saoridon::instanceInfo(const QString key) { return m_instanceInfo[key]; } @@ -180,3 +174,24 @@ return m_timelineMap[timeline]; } +const QUrl Saoridon::addQuery(const QUrl url, const QString key, const QString value) +{ + QUrl newurl = url; + QUrlQuery q(url.query()); + q.addQueryItem(key,value); + newurl.setQuery(q); + return newurl; +} + +const QUrl Saoridon::addQuery(const QUrl url, const QUrlQuery query) +{ + QUrl newurl = url; + QUrlQuery q(url.query()); + auto list = query.queryItems(); + list += q.queryItems(); + q.setQueryItems(list); + newurl.setQuery(q); + return newurl; + +} + diff --git a/saoridon.h b/saoridon.h index 6b23229..428da85 100644 --- a/saoridon.h +++ b/saoridon.h @@ -30,6 +30,7 @@ #include <QObject> #include <QUrl> #include <QList> +#include <QJsonObject> class Saoridon : public QObject { @@ -49,8 +50,10 @@ bool clientRedistration(); const QUrl getAuthorizedUrl(); const QString getAccessToken(const QString code); - const QString instanceInfo(const QString key); + const QJsonValue instanceInfo(const QString key); const QUrl timelineUrl(const QString timeline); + static const QUrl addQuery(const QUrl url,const QString key,const QString value); + static const QUrl addQuery(const QUrl url,const QUrlQuery query); protected: void getInstanceInfomation(); @@ -59,7 +62,7 @@ QUrl m_instance; QString m_clientId; QString m_clientSecret; - QMap<QString,QString> m_instanceInfo; + QJsonObject m_instanceInfo; QMap<QString,QUrl> m_timelineMap; signals: diff --git a/saoristyle.css b/saoristyle.css new file mode 100644 index 0000000..2daa4c0 --- /dev/null +++ b/saoristyle.css @@ -0,0 +1,51 @@ +div.user_info { + font-size:small; +} + +div.created_at { + font-size:small; + text-align:right; +} + +div.content { + float:none; + margin-left:68px; +} + +div.media { + text-align:center; +} + +div.reblogger { + font-size:small; + text-align:right; +} + +div.reblogger img { + float:none; +} + +span.media_preview { + float:left; +} + +span.display_name { + font-size:large; +} + +span.acct { + color:gray; +} + +div.account img { + float:left; +} + +div.notification_type { + font-size:large; + float:none; +} + +div.notification_type img { + float:left; +} diff --git a/saoriview.cpp b/saoriview.cpp index 9aa6d06..f09164b 100644 --- a/saoriview.cpp +++ b/saoriview.cpp @@ -26,7 +26,16 @@ #include "saoriview.h" #include "ui_saoriview.h" +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QUrlQuery> +#include <QLabel> +#include <QScrollBar> #include <QDebug> +#include <QDateTime> +#include <QBoxLayout> +#include <saoriapplication.h> QList<SaoriView *> SaoriView::m_viewList; @@ -38,6 +47,29 @@ m_viewname = view; m_account = account; m_viewList.append(this); + m_maxid = 0; + + if (m_viewname == "instance") { + m_entries.append(new SaoriViewEntry(0,ui->scrollAreaWidgetContents)); + m_entries.at(0)->setContent(instanceInfoParser(account)); + ui->scrollAreaWidgetContents->layout()->addWidget(m_entries.at(0)); + return; + } + + auto saoriaccount = SaoriApplication::saori()->findAccount(m_account); + if (saoriaccount) { + connect(saoriaccount,&SaoriAccount::apiData,this,&SaoriView::recived); + saoriaccount->getTimelineData(view,QUrlQuery()); + connect(ui->scrollArea->verticalScrollBar(),&QScrollBar::valueChanged,this,[=](int p){ + if (ui->scrollArea->verticalScrollBar()->maximum() == p) { + if (m_entries.count()) { + QUrlQuery q; + q.addQueryItem("max_id",QString::number(m_entries.last()->id())); + saoriaccount->getTimelineData(view,q); + } + } + }); + } } SaoriView::~SaoriView() @@ -54,7 +86,240 @@ return nullptr; } -void SaoriView::recived(QMap<QString, QString> result) +void SaoriView::reload() { - qDebug() << result; + auto saoriaccount = SaoriApplication::saori()->findAccount(m_account); + if (saoriaccount == nullptr) return; + if (m_entries.count() == 0) m_maxid = 0; + else { + m_maxid = m_entries.first()->id(); + } + QUrlQuery query; + query.addQueryItem("since_id",QString::number(m_maxid)); + saoriaccount->getTimelineData(m_viewname,query); +} + +const QString SaoriView::instanceInfoParser(const QString instance) +{ + auto i = SaoriApplication::findInstance(QUrl(instance)); + if (!i) return QString(); + QString result; + result = QString(htmlDiv("instance_title","%1") + + htmlDiv("instance_uri","%2") + + htmlDiv("instance_description","%3") + + htmlDiv("instance_email","email : %4") + + htmlDiv("instance_contact_account","%5") + + htmlDiv("instance_version","Version : %6") + ).arg(i->instanceInfo("title").toString(), + i->instanceInfo("uri").toString(), + i->instanceInfo("description").toString(), + i->instanceInfo("email").toString(), + accountParser(i->instanceInfo("contact_account").toObject()), + i->instanceInfo("version").toString()); + return result; +} + +const QString SaoriView::statusParser(const QJsonObject json) +{ + QString result; + if (json.isEmpty()) return QString(); + + QDateTime dt = QDateTime::fromString(json["created_at"].toString(),"yyyy-MM-ddTHH:mm:ss.zzzZ"); + dt.setTimeSpec(Qt::UTC); + + result = (!json["reblog"].isNull()) ? + (statusParser(json["reblog"].toObject()) + + htmlDiv("reblogger",htmlHr() + + htmlImg("mavatar",json["account"].toObject()["avatar"].toString()) + + tr("boosted by: %1").arg(json["account"].toObject()["display_name"].toString())) + ) : + (htmlDiv("status",accountParser(json["account"].toObject()) + + htmlDiv("created_at",tr("created at :") + + dt.toLocalTime().toString()) + + htmlDiv("content",contentParser(json["content"].toString()))) + ); + if (json["reblog"].isNull()) { + QString media; + for (auto j:json["media_attachments"].toArray()) { + media += mediaParser(j.toObject()); + } + result += htmlDiv("media",media); + } + return result; +} + +const QString SaoriView::accountParser(const QJsonObject json) +{ + QString result; + if (json.isEmpty()) return QString(); + result = htmlDiv("account", + htmlSpan("avatar", + htmlHr() + + htmlImg("avatar",json["avatar"].toString())) + + htmlSpan("display_name", + json["display_name"].toString()) + + " " + + htmlSpan("acct","@" + json["acct"].toString()) + + htmlDiv("user_info", + tr(" following: ") + + QString::number(json["following_count"].toInt()) + + tr(" following: ") + + QString::number(json["followers_count"].toInt())) + ); + return result; +} + +const QString SaoriView::mediaParser(const QJsonObject json) +{ + QString result; + result = htmlSpan("media_preview", + json["type"].toString() == "image" ? + htmlAnc("media:" + json["url"].toString(),htmlImg("media",json["preview_url"].toString())) : + htmlImg("media",json["preview_url"].toString())); + return result; +} + +const QString SaoriView::contentParser(const QString content) +{ + QString result; + auto l = content.split("<br />"); + for (auto s:l) { + result += "<br />"; + int i = 0; + for (auto c:s) { + if (c != ' ') break; + result += " "; + } + result += s.right(s.count() - i); + } + result = result.right(result.count() - 6); + + l = result.split("<p>"); + result = ""; + for (auto s:l) { + result += "<p>"; + int i = 0; + for (auto c:s) { + if (c != ' ') break; + result += " "; + } + result += s.right(s.count() - i); + } + result = result.right(result.count() - 3); + + return result; +} + +const QString SaoriView::notificationParser(const QJsonObject json) +{ + QString result; + if (json.isEmpty()) return QString(); + + QDateTime dt = QDateTime::fromString(json["created_at"].toString(),"yyyy-MM-ddTHH:mm:ss.zzzZ"); + dt.setTimeSpec(Qt::UTC); + + QStringList type; + type << "mention" << "reblog" << "favourite" << "follow"; + QString title; + switch (type.indexOf(json["type"].toString())) { + case 0: + title += htmlImg("mavatar",":/icons/ionicons/chatbubbles.svg"); + title += tr("%1 mentioned your status.").arg(json["account"].toObject()["display_name"].toString()); + break; + case 1: + title += htmlImg("mavatar",":/icons/ionicons/share.svg"); + title += tr("%1 boosted your status.").arg(json["account"].toObject()["display_name"].toString()); + break; + case 2: + title += htmlImg("mavatar",":/icons/ionicons/heart.svg"); + title += tr("%1 favourited your status.").arg(json["account"].toObject()["display_name"].toString()); + break; + case 3: + title += htmlImg("mavatar",":/icons/ionicons/person-add.svg"); + title += tr("%1 followed you.").arg(json["account"].toObject()["display_name"].toString()); + break; + default: + break; + } + + result = htmlDiv("notification", + htmlDiv("notification_type",title + htmlHr()) + + htmlDiv("created_at",tr("created at :") + dt.toLocalTime().toString()) + + statusParser(json["status"].toObject()) + + htmlDiv("reblogger",htmlHr() + + htmlImg("mavatar",json["account"].toObject()["avatar"].toString()) + + tr("%1 (@%2)").arg(json["account"].toObject()["display_name"].toString(), + json["account"].toObject()["acct"].toString()))); + return result; +} + +const QString SaoriView::htmlDiv(const QString divclass, const QString text) +{ + return QString("<div class=\"%1\">\n%2\n</div>\n").arg(divclass,text); +} + +const QString SaoriView::htmlSpan(const QString spanclass, const QString text) +{ + return QString("<span class=\"%1\">%2</span>").arg(spanclass,text); +} + +const QString SaoriView::htmlImg(const QString type, const QString url) +{ + return QString("<img(%1:%2) />").arg(type,url); +} + +const QString SaoriView::htmlAnc(const QString link, const QString text) +{ + return QString("<a href=\"%1\">%2</a>").arg(link,text); +} + +const QString SaoriView::htmlHr() +{ + return QString("<hr />"); +} + +void SaoriView::recived(const QString timeline,const QByteArray data) +{ + if (timeline != m_viewname) return; + QJsonArray json = QJsonDocument::fromJson(data).array(); + for (auto j:json) { + int i = 0; + SaoriViewEntry *entry = nullptr; + for(;m_entries.count() > i;i ++) { + if (m_entries.at(i)->id() == j.toObject()["id"].toString().toLongLong()) { + entry = m_entries.at(i); + break; + } + if (m_entries.at(i)->id() < j.toObject()["id"].toString().toLongLong()) break; + } + if (entry == nullptr) { + entry = new SaoriViewEntry(j.toObject()["id"].toString().toLongLong(),ui->scrollAreaWidgetContents); + connect(entry,&SaoriViewEntry::anchorClicked,this,&SaoriView::linkClicked); + m_entries.insert(i,entry); + qobject_cast<QBoxLayout*>(ui->scrollAreaWidgetContents->layout())->insertWidget(i,entry); + if (m_viewname == "notifications") entry->setContent(notificationParser(j.toObject())); + else entry->setContent(statusParser(j.toObject())); + } + } + if (m_entries.count()) { + if (m_entries.first()->id() > m_maxid) reload(); + } +} + +void SaoriView::linkClicked(const QUrl &url) +{ + if (url.toString().left(6) == "media:") { + QUrl u(url.toString().mid(6)); + emit openMediaView(u); + } +} + +void SaoriView::on_pushButton_newest_clicked() +{ + ui->scrollArea->verticalScrollBar()->setValue(0); +} + +void SaoriView::on_pushButton_reload_clicked() +{ + reload(); } diff --git a/saoriview.h b/saoriview.h index 2f793be..60540f0 100644 --- a/saoriview.h +++ b/saoriview.h @@ -28,7 +28,10 @@ #define SAORIVIEW_H #include <QWidget> -#include <QMap> +#include <QList> +#include <QJsonObject> +#include <QJsonArray> +#include <saoriviewentry.h> namespace Ui { class SaoriView; @@ -46,13 +49,39 @@ protected: QString m_viewname; QString m_account; + QList<SaoriViewEntry*> m_entries; + qlonglong m_maxid; + static QList<SaoriView*> m_viewList; +protected: + void reload(); + const QString instanceInfoParser(const QString instance); + const QString statusParser(const QJsonObject json); + const QString accountParser(const QJsonObject json); + const QString mediaParser(const QJsonObject json); + const QString contentParser(const QString content); + const QString notificationParser(const QJsonObject json); + + static const QString htmlDiv(const QString divclass,const QString text); + static const QString htmlSpan(const QString spanclass,const QString text); + static const QString htmlImg(const QString type,const QString url); + static const QString htmlAnc(const QString link,const QString text); + static const QString htmlHr(); + + public slots: - void recived(QMap<QString,QString> result); + void recived(const QString timeline,const QByteArray data); + void linkClicked(const QUrl &url); private: Ui::SaoriView *ui; + +signals: + void openMediaView(const QUrl); +private slots: + void on_pushButton_newest_clicked(); + void on_pushButton_reload_clicked(); }; #endif // SAORIVIEW_H diff --git a/saoriview.ui b/saoriview.ui index e4b2987..e8fc732 100644 --- a/saoriview.ui +++ b/saoriview.ui @@ -16,18 +16,42 @@ <layout class="QVBoxLayout" name="verticalLayout"> <item> <widget class="QScrollArea" name="scrollArea"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOn</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAsNeeded</enum> + </property> + <property name="sizeAdjustPolicy"> + <enum>QAbstractScrollArea::AdjustIgnored</enum> + </property> <property name="widgetResizable"> <bool>true</bool> </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> <widget class="QWidget" name="scrollAreaWidgetContents"> <property name="geometry"> <rect> <x>0</x> <y>0</y> - <width>307</width> + <width>293</width> <height>316</height> </rect> </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <layout class="QVBoxLayout" name="verticalLayout_2"/> </widget> </widget> @@ -38,7 +62,7 @@ <number>0</number> </property> <item> - <widget class="QPushButton" name="pushButton_5"> + <widget class="QPushButton" name="pushButton_pin"> <property name="toolTip"> <string>Pin</string> </property> @@ -52,7 +76,7 @@ </widget> </item> <item> - <widget class="QPushButton" name="pushButton_6"> + <widget class="QPushButton" name="pushButton_notify"> <property name="toolTip"> <string>Notify</string> </property> @@ -66,7 +90,7 @@ </widget> </item> <item> - <widget class="QPushButton" name="pushButton_2"> + <widget class="QPushButton" name="pushButton_autoreload"> <property name="toolTip"> <string>Auto reload</string> </property> @@ -80,7 +104,7 @@ </widget> </item> <item> - <widget class="QPushButton" name="pushButton_4"> + <widget class="QPushButton" name="pushButton_newest"> <property name="toolTip"> <string>Newest</string> </property> @@ -91,7 +115,7 @@ </widget> </item> <item> - <widget class="QPushButton" name="pushButton_3"> + <widget class="QPushButton" name="pushButton_reload"> <property name="toolTip"> <string>Reload</string> </property> @@ -115,7 +139,7 @@ </spacer> </item> <item> - <widget class="QPushButton" name="pushButton"> + <widget class="QPushButton" name="pushButton_toot"> <property name="sizePolicy"> <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> <horstretch>0</horstretch> diff --git a/saoriviewentry.cpp b/saoriviewentry.cpp new file mode 100644 index 0000000..64265d4 --- /dev/null +++ b/saoriviewentry.cpp @@ -0,0 +1,142 @@ +/*** + +The MIT License + +Copyright (c) 2018 Teppei Tamra (TAM) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +***/ + +#include "saoriviewentry.h" +#include "saoriapplication.h" +#include "saoricache.h" +#include <QRegExp> +#include <QImage> +#include <QDebug> + +SaoriViewEntry::SaoriViewEntry(qlonglong id, QWidget *parent) : + QTextBrowser(parent) +{ + setReadOnly(true); + setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); + setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Minimum); + setOpenLinks(false); + setOpenExternalLinks(false); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_id = id; + connect(SaoriApplication::saori()->cache(),&SaoriCache::downloaded,this,&SaoriViewEntry::downloaded); +} + +void SaoriViewEntry::setContent(const QString content) +{ + m_original = content; + document()->clear(); + QStringList imgs; + for (int i = 0;(i = m_original.indexOf("<img(",i)) != -1;++ i) { + int p = m_original.indexOf(')',i); + imgs << m_original.mid(i + 5,p - (i + 5)); + } + for (auto i:imgs) { + int p = i.indexOf(':'); + QString imageurl; + if (i.mid(p + 1,2) != ":/") { + imageurl = SaoriApplication::saori()->cache()->fileCache(QUrl(i.mid(p + 1))); + } else { + imageurl = i.mid(p + 1); + } + m_urlmap[i] = imageurl; + //qDebug() << imageurl; + QImage img(imageurl); + imageResizer(i.left(p),img); + document()->addResource(QTextDocument::ImageResource,QUrl("img:" + imageurl),QVariant(img)); + } + setText(designedText()); + // QTextBrowserのサイズを確定させるトリック。 + QResizeEvent e(size(),size()); + resizeEvent(&e); +} + +qlonglong SaoriViewEntry::id() +{ + return m_id; +} + +void SaoriViewEntry::resizeEvent(QResizeEvent *e) +{ + QTextBrowser::resizeEvent(e); + document()->setTextWidth(qreal(e->size().width())); + setMinimumHeight(document()->size().height() + 5); +} + +const QString SaoriViewEntry::designedText() +{ + QString result; + result += "<head><link rel=\"stylesheet\" type=\"text/css\" href=\":/css/saoristyle.css\" /></head>"; + result += "<body>"; + result += imageReplacer(); + result += "</body>"; + return result; +} + +const QString SaoriViewEntry::imageReplacer() +{ + QString result = m_original; + for (auto k:m_urlmap.keys()) { + result.replace(QString("<img(" + k + ")"), + QString("<img src=\"img:" + m_urlmap[k] + "\"")); + } + return result; +} + +void SaoriViewEntry::imageResizer(const QString type, QImage &image) +{ + QStringList c; + c << "avatar" << "mavatar" << "media"; + switch (c.indexOf(type)) { + case 0: + image = image.scaled(64,64,Qt::KeepAspectRatio,Qt::SmoothTransformation); + break; + case 1: + image = image.scaled(32,32,Qt::KeepAspectRatio,Qt::SmoothTransformation); + break; + case 2: + image = image.scaledToWidth(200,Qt::SmoothTransformation); + break; + default: + break; + } + return; +} + +void SaoriViewEntry::downloaded(const QUrl url) +{ + m_urlmap[url.toString()] = SaoriApplication::saori()->cache()->fileCache(url); + setContent(m_original); + for (auto i = m_urlmap.begin();i != m_urlmap.end();i ++) { + if (i.value().left(2) == ":/") { + if (i.key().left(2) != ":/") { + return; + } + } + } + disconnect(SaoriApplication::saori()->cache(),&SaoriCache::downloaded,this,&SaoriViewEntry::downloaded); +} diff --git a/saoriviewentry.h b/saoriviewentry.h new file mode 100644 index 0000000..737e5a2 --- /dev/null +++ b/saoriviewentry.h @@ -0,0 +1,59 @@ +/*** + +The MIT License + +Copyright (c) 2018 Teppei Tamra (TAM) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +***/ + +#ifndef SAORIVIEWENTRY_H +#define SAORIVIEWENTRY_H + +#include <QTextBrowser> +#include <QUrl> +#include <QMap> + +class SaoriViewEntry : public QTextBrowser +{ + Q_OBJECT + +public: + SaoriViewEntry(qlonglong id,QWidget *parent = nullptr); + void setContent(const QString content); + qlonglong id(); + +protected: + virtual void resizeEvent(QResizeEvent *e); + const QString designedText(); + const QString imageReplacer(); + void imageResizer(const QString type,QImage &image); + +protected: + qlonglong m_id; + QString m_original; + QMap<QString,QString> m_urlmap; + +protected slots: + void downloaded(const QUrl url); + +}; + +#endif // SAORIVIEWENTRY_H diff --git a/saoriwindow.cpp b/saoriwindow.cpp index df44ea7..16ece61 100644 --- a/saoriwindow.cpp +++ b/saoriwindow.cpp @@ -25,6 +25,7 @@ ***/ #include <QMdiSubWindow> +#include <QPixmap> #include "saoriwindow.h" #include "ui_saoriwindow.h" #include "saoriapplication.h" @@ -45,19 +46,77 @@ delete ui; } +QPair<QString,QString> SaoriWindow::getTimelineTitle(const QString timeline) +{ + // trick for translation. + QStringList trtl; + QString tlText,icon; + trtl << "home" << "local" << "public" << "notifications" << "instance"; + switch (trtl.indexOf(timeline)) { + case 0: + tlText = tr("home"); + icon = ":/icons/ionicons/chatbubbles.svg"; + break; + case 1: + tlText = tr("local"); + icon = ":/icons/ionicons/chatbubble.svg"; + break; + case 2: + tlText = tr("public"); + icon = ":/icons/ionicons/earth.svg"; + break; + case 3: + tlText = tr("notifications"); + icon = ":/icons/ionicons/alert.svg"; + break; + case 4: + tlText = tr("instance"); + icon = ""; + break; + default: + tlText = timeline; + break; + } + + return QPair<QString,QString>(tlText,icon); + +} + void SaoriWindow::openView(const QStringList viewName) { if (auto view = SaoriView::findView(viewName.at(1),viewName.at(0))) { ui->mdiArea->setActiveSubWindow(qobject_cast<QMdiSubWindow*>(view->parent())); return; } - QString title = viewName.at(1) + ":" + viewName.at(0); + QString title = getTimelineTitle(viewName.at(1)).first + ":" + viewName.at(0); auto view = new SaoriView(viewName.at(1),viewName.at(0)); auto sub = ui->mdiArea->addSubWindow(view); + connect(view,&SaoriView::openMediaView,this,&SaoriWindow::openMediaView); sub->setWindowTitle(title); + sub->setWindowIcon(QIcon(getTimelineTitle(viewName.at(1)).second)); sub->show(); } +void SaoriWindow::openMediaView(const QUrl url) +{ + // TODO 専用のwidget作った方がいいでしょう。 + auto view = new QLabel(); + auto sub = ui->mdiArea->addSubWindow(view); + sub->setWindowTitle(url.toString()); + QString image = SaoriApplication::saori()->cache()->fileCache(url); + if (image.mid(0,2) == ":/") + connect(SaoriApplication::saori()->cache(),&SaoriCache::downloaded,this,[=](const QUrl i){ + if (i.toString() == url.toString()) { + view->setPixmap(SaoriApplication::saori()->cache()->fileCache(url)); + sub->resize(view->pixmap()->size()); + } + }); + view->setPixmap(image); + view->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding); + sub->show(); + sub->resize(view->pixmap()->size()); +} + void SaoriWindow::openAccountDialog() @@ -67,6 +126,7 @@ dialog->m_account->setParent(QApplication::instance()); SaoriApplication::getAccountList()->append(dialog->m_account); } + updateTimelineList(); delete dialog; } @@ -80,35 +140,11 @@ item->setIcon(0,QIcon(":/icons/ionicons/person.svg")); accountItem->addChild(item); for (auto tl:SaoriApplication::getAccountList()->at(i)->timelineList()) { - // trick for translation. - QStringList trtl; - QString tlText,icon; - trtl << "home" << "local" << "public" << "notifications"; - switch (trtl.indexOf(tl)) { - case 0: - tlText = tr("home"); - icon = ":/icons/ionicons/chatbubbles.svg"; - break; - case 1: - tlText = tr("local"); - icon = ":/icons/ionicons/chatbubble.svg"; - break; - case 2: - tlText = tr("public"); - icon = ":/icons/ionicons/earth.svg"; - break; - case 3: - tlText = tr("notifications"); - icon = ":/icons/ionicons/alert.svg"; - break; - default: - tlText = tl; - break; - } - auto tlItem = new QTreeWidgetItem(item,QStringList() << tlText + auto title = getTimelineTitle(tl); + auto tlItem = new QTreeWidgetItem(item,QStringList() << title.first << SaoriApplication::getAccountList()->at(i)->name() << tl); - if (!icon.isEmpty()) tlItem->setIcon(0,QIcon(icon)); + if (!title.second.isEmpty()) tlItem->setIcon(0,QIcon(title.second)); item->addChild(tlItem); } } @@ -116,7 +152,7 @@ for (int i = 0;i < SaoriApplication::getInstanceList()->count();i ++) { auto *item = new QTreeWidgetItem(instanceItem,QStringList() << SaoriApplication::getInstanceList()->at(i)->instance().toString()); instanceItem->addChild(item); - item->addChild(new QTreeWidgetItem(item,QStringList() << tr("Infomation"))); + //item->addChild(new QTreeWidgetItem(item,QStringList() << tr("Infomation"))); } } @@ -124,7 +160,27 @@ { auto account = SaoriApplication::findAccount(item->text(1)); if (account) { - qDebug() << account->instance()->timelineUrl(item->text(2)); openView(QStringList() << item->text(1) << item->text(2)); + return; } + auto instance = SaoriApplication::findInstance(QUrl(item->text(0))); + if (instance) { + openView(QStringList() << instance->instance().toString() << "instance"); + } +} + +void SaoriWindow::on_actionTabbedView_mode_toggled(bool arg1) +{ + if (ui->mdiArea->viewMode() == QMdiArea::TabbedView) { + if (arg1 == false) { + ui->mdiArea->setViewMode(QMdiArea::SubWindowView); + ui->actionTiled->setEnabled(true); + } + } else { + if (arg1 == true) { + ui->mdiArea->setViewMode(QMdiArea::TabbedView); + ui->actionTiled->setEnabled(false); + } + } + return; } diff --git a/saoriwindow.h b/saoriwindow.h index 58587ea..da84032 100644 --- a/saoriwindow.h +++ b/saoriwindow.h @@ -29,6 +29,8 @@ #include <QMainWindow> #include <QTreeWidgetItem> +#include <QPair> +#include <QUrl> namespace Ui { class SaoriWindow; @@ -41,10 +43,14 @@ public: explicit SaoriWindow(QWidget *parent = 0); ~SaoriWindow(); + static QPair<QString,QString> getTimelineTitle(const QString timeline); protected: void openView(const QStringList viewName); +public slots: + void openMediaView(const QUrl url); + protected slots: void openAccountDialog(); void updateTimelineList(); @@ -54,6 +60,8 @@ private slots: void on_timelineTree_itemDoubleClicked(QTreeWidgetItem *item, int); + void on_actionTabbedView_mode_toggled(bool arg1); + private: Ui::SaoriWindow *ui; }; diff --git a/saoriwindow.ui b/saoriwindow.ui index dff0541..ec66142 100644 --- a/saoriwindow.ui +++ b/saoriwindow.ui @@ -13,6 +13,10 @@ <property name="windowTitle"> <string>Saori</string> </property> + <property name="windowIcon"> + <iconset resource="saori.qrc"> + <normaloff>:/icons/saori.svg</normaloff>:/icons/saori.svg</iconset> + </property> <property name="documentMode"> <bool>false</bool> </property> @@ -136,7 +140,7 @@ </widget> </item> <item row="1" column="1"> - <widget class="QSpinBox" name="spinBox"/> + <widget class="QSpinBox" name="spinBox_reloadInterval"/> </item> <item row="0" column="0" colspan="2"> <widget class="QTreeWidget" name="timelineTree"> @@ -209,7 +213,9 @@ </action> </widget> <layoutdefault spacing="6" margin="11"/> - <resources/> + <resources> + <include location="saori.qrc"/> + </resources> <connections> <connection> <sender>actionShow_Hide_Timeline_List</sender>