ジャンル不定の日記です。

とりあえずのFeedリーダー作った

ここ数日、Feedのニュースを表示するPlasmoidがPlasma5にしたら無くなっちゃったから自分で作っちゃおうと思ってQMLを勉強していたが、とりあえずの物を作った。


ダウンロード

モジュールはQtリファレンスのバージョンを指定してimportしてあるんで、Archとか最新版がインストールされそうな環境じゃないと動かないかも。
$ plasmapkg2 -i ディレクトリ
ってやるとインストールされて[Add Widgets...]から設置できる。
-iじゃなくて-uで更新。

$ plasmapkg2 -r com.example.feed
でアンインストール。

[内容]
RSS1.0 RSS2.0 Atom いずれかのFeedのURLをPlasmoidの設定から登録すると60分間隔で更新する。
Feedの構造によっては処理できないものもあるかもです。

現時点での問題は、
設定変更時にイベント処理させる方法がわからないので、設定変更後に次回取得まで待つかKDE再起動(ログアウト)しないと更新されない。
ListViewのサイズを設定していないので長いタイトルは横にはみ出すぽい。


↓コード(修正しているので実物と違っています)

metadata.desktop

[Desktop Entry]
Name=Feed Reader
ServiceTypes=Plasma/Applet
Type=Service
X-KDE-PluginInfo-Author=My Name
X-KDE-PluginInfo-Email=mail@example.com
X-KDE-PluginInfo-Name=com.example.feed
X-KDE-PluginInfo-License=GPL
X-KDE-PluginInfo-Version=1.0
X-KDE-PluginInfo-Website=http://example.com/
X-Plasma-API=declarativeappletscript
X-Plasma-MainScript=ui/main.qml


contents/ui/main.qm (メインUI)
import QtQuick 2.5
import QtQml 2.2
import org.kde.plasma.plasmoid 2.0
import "script.js" as JavaScript

ListView{
property string feed: plasmoid.configuration.feed;
anchors.fill:parent
ListModel{id:model}
model:model
delegate:Text{
color:"blue"
text:title
MouseArea{
anchors.fill:parent
cursorShape:Qt.PointingHandCursor
onClicked:Qt.openUrlExternally(url)
}
}
Timer{
id:timer
repeat:true
interval:60*60*1000
onTriggered:listUpdate()
}
Component.onCompleted:{
timer.start();
listUpdate();
}
function listUpdate(){
if(!feed.match(/^https?:/)) return;
JavaScript.FeedGet([feed],function(list){
model.clear();
for(var i=0;i<list.length;i++){
model.append(list[i]);
}
});
}
}

contents/ui/configGeneral.qml (設定画面)
import QtQuick 2.0
import QtQuick.Controls 1.4

Item {
property alias cfg_feed:feed.text
TextField{
id:feed
}
}

contents/config/config.qml (設定画面のカテゴリ)
import org.kde.plasma.configuration 2.0

ConfigModel{
ConfigCategory{
name: "General"
icon: "plasma"
source: "configGeneral.qml"
}
}

contents/config/main.xml (設定ファイル)
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile name=""/>
<group name="General">
<entry name="feed" type="String">
<default></default>
</entry>
</group>
</kcfg>


contents/code/script.js
var list=[];
var exist=[];
function HttpGet(url,cb){
var http=new XMLHttpRequest();
http.open("GET",url);
http.onreadystatechange=function(){
if(http.readyState==4){
cb(http.responseText);
}
}
http.send();
}
function FeedGet(url,cb){
for(var i=0;i<url.length;i++){
(function(i){
HttpGet(url[i],function(file){
if(file.match(/<feed\W/)){
ParseAtom(file);
}else{
ParseRss(file);
}
list=list.sort(function(a,b){
if(exist[a.url]<exist[b.url]) return 1;
if(exist[a.url]>exist[b.url]) return -1;
return 0;
});
if(i==url.length-1) cb(list);
});
})(i);
}
}
function ParseAtom(file){
var item=file.match(/<entry\W[\s\S]*?<\/entry>/g);
for(var i=0;i<item.length;i++){
var url="";
var title="";
var desc="";
var date=new Date();
if(item[i].match(/<link [^>]*href\s*=[\s"']*([^\s"'>]*)/)) url=RegExp.$1;
if(item[i].match(/<title[^>]*>([\s\S]*?)<\/title>/)) title=RegExp.$1;
if(item[i].match(/<summary[^>]*>([\s\S]*?)<\/summary>/)) desc=RegExp.$1;
if(item[i].match(/<updated[^>]*>([\s\S]*?)<\/updated>/)) date=new Date(RegExp.$1);
if(!exist[url]){
url=HtmlEscapeU(url);
title=HtmlEscapeU(title);
desc=HtmlEscapeU(desc);
exist[url]=date.getTime();
list.push({"url":url,"title":title,"desc":desc,"date":date.toISOString()});
}
}
}
function ParseRss(file){
var item=file.match(/<item\W[\s\S]*?<\/item>/g);
for(var i=0;i<item.length;i++){
var url="";
var title="";
var desc="";
var date=new Date();
if(item[i].match(/<link[^>]*>([\s\S]*?)<\/link>/)) url=RegExp.$1;
if(item[i].match(/<title[^>]*>([\s\S]*?)<\/title>/)) title=RegExp.$1;
if(item[i].match(/<description[^>]*>([\s\S]*?)<\/description>/)) desc=RegExp.$1;
if(item[i].match(/<dc:date[^>]*>([\s\S]*?)<\/dc:date>/)) date=new Date(RegExp.$1);
if(!exist[url]){
url=HtmlEscapeU(url);
title=HtmlEscapeU(title);
desc=HtmlEscapeU(desc);
exist[url]=date.getTime();
list.push({"url":url,"title":title,"desc":desc,"date":date.toISOString()});
}
}
}
function HtmlEscapeU(str){
str=str.replace(/&#(\d+);/g,function(m,$1){return String.fromCharCode($1);});
str=str.replace(/&quot;/g,'"');
str=str.replace(/&amp;/g,'&');
return str;
}
qmlsceneコマンドで.qmlを実行する場合は.jsのimportはパス指定しないと.qmlと同じディレクトリだが、Plasmoidとしてインストールした場合はcodeディレクトリがimportされるぽい。


JavaScriptのコードはFeedのURLを配列で受け取って複数Feedを合成するように作ってあるんだが、
KDE4時代も元々1ウィジェット1Feedで使ってたし、設定UI作るのめんどいから1Feedしか登録できないようにした。

タイトルと記事URL以外に、要約と日付も取得しているんだが表示してない。
ツールチップか何かで表示しようか考えてパース処理は作ったんだが。

データの保存は当初JavaScriptのlocalStorage(DOMストレージ)が使えると思ってたんだが、QMLでは無理らしい。
QtにlocalStorageという機能があるんだが、これはWeb SQL Database相当のもので、Sqliteクエリ投げる感じのやつ。


設定画面で[適用]ボタン押した時に処理する方法がマジわかんない・・・

Plasmoidの設定でハマってる・・・

引き続き、QMLの勉強中で、RSSやAtom等を取得してリスト表示するのは概ね出来そうなんだが、Plasmoidの設定関連でハマってる。
これはQtQuickとは別でKDEの情報探さないとダメみたいね。
Qtのリファレンス等の情報も不満だが、KDEの情報はもっと調べにくい。

/usr/share/plasma/plasmoids/ 以下にあるPlasmoidのコード参考にしたりとかして、
設定値を取得することはできたんだが、[適用]ボタン押した時にイベント駆動で処理する方法がわからない・・・
とりあえず、現時点でわかった情報。


contents/ui/main.qml
以外に、
contents/config/config.qml
contents/config/main.xml
contents/ui/xxxx.qml
が必要ぽい。

contents/config/config.qml
import org.kde.plasma.configuration 2.0

ConfigModel{
    ConfigCategory{
        name: "General"
        icon: "plasma"
        source: "configGeneral.qml"
    }
}
Plasmoidの設定が呼びだされた場合は、このファイルに従って設定画面のカテゴリが追加される。
この場合、[General]カテゴリが追加され、sourceで指定した contents/ui/configGeneral.qml が[General]カテゴリの設定画面になる。

contents/ui/configGeneral.qml
import QtQuick 2.0
import QtQuick.Controls 1.4

Item {
    property alias cfg_tf: tf.text
    TextField{
       id:tf
    }
}
これでTextFieldが一個だけの設定画面になる。
TextFieldのidがtfなのでtf.textが文字列となるが、エイリアスで cfg_ で始まるプロパティと同一にする必要があるぽい。

contents/config/main.xml
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
  <kcfgfile name=""/>
  <group name="General">
    <entry name="tf" type="String">
      <default>test</default>
    </entry>
  </group>
</kcfg>
この.xmlファイルが設定ファイルになるぽい。
ファイルがなくてもエラーにならないが、再起動すると消える。
ここで<entry>属性のnameが"tf"となっているが、これが設定画面用.qmlのcfg_tfの値になるぽい。
configGeneral.qmlのTextFieldの値ととcfg_tfをaliasにしているので、TextFieldを変更すると保存される模様。

contents/ui/main.qml
import QtQuick 2.0
import org.kde.plasma.plasmoid 2.0

Column{
    id:root
    width:640
    height:480
    property string tf: plasmoid.configuration.tf;
    Text{
       text:tf
    }
}
いつものmain.qmlだが、org.kde.plasma.plasmoidをimportしている。
plasmoid.configuration.tf で設定値が取得できる模様。
ただし、[適用]ボタン押さなくてもすぐに反映しちゃうぽい。


で、[適用]ボタンを押した時にmain.qmlで処理したいんだが、その方法がわからない・・・