とりあえずのFeedリーダー作った
ここ数日、Feedのニュースを表示するPlasmoidがPlasma5にしたら無くなっちゃったから自分で作っちゃおうと思ってQMLを勉強していたが、とりあえずの物を作った。
ダウンロード
モジュールはQtリファレンスのバージョンを指定してimportしてあるんで、Archとか最新版がインストールされそうな環境じゃないと動かないかも。
-iじゃなくて-uで更新。
[内容]
RSS1.0 RSS2.0 Atom いずれかのFeedのURLをPlasmoidの設定から登録すると60分間隔で更新する。
Feedの構造によっては処理できないものもあるかもです。
現時点での問題は、
設定変更時にイベント処理させる方法がわからないので、設定変更後に次回取得まで待つかKDE再起動(ログアウト)しないと更新されない。
ListViewのサイズを設定していないので長いタイトルは横にはみ出すぽい。
↓コード(修正しているので実物と違っています)
contents/ui/main.qm (メインUI)
contents/ui/configGeneral.qml (設定画面)
contents/config/config.qml (設定画面のカテゴリ)
contents/config/main.xml (設定ファイル)
contents/code/script.js
JavaScriptのコードはFeedのURLを配列で受け取って複数Feedを合成するように作ってあるんだが、
KDE4時代も元々1ウィジェット1Feedで使ってたし、設定UI作るのめんどいから1Feedしか登録できないようにした。
タイトルと記事URL以外に、要約と日付も取得しているんだが表示してない。
ツールチップか何かで表示しようか考えてパース処理は作ったんだが。
データの保存は当初JavaScriptのlocalStorage(DOMストレージ)が使えると思ってたんだが、QMLでは無理らしい。
QtにlocalStorageという機能があるんだが、これはWeb SQL Database相当のもので、Sqliteクエリ投げる感じのやつ。
設定画面で[適用]ボタン押した時に処理する方法がマジわかんない・・・
ダウンロード
モジュールは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再起動(ログアウト)しないと更新されない。
↓コード(修正しているので実物と違っています)
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
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]);
}
});
}
}
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
}
}
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"
}
}
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>
<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(/"/g,'"');
str=str.replace(/&/g,'&');
return str;
}
qmlsceneコマンドで.qmlを実行する場合は.jsのimportはパス指定しないと.qmlと同じディレクトリだが、Plasmoidとしてインストールした場合はcodeディレクトリがimportされるぽい。
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(/"/g,'"');
str=str.replace(/&/g,'&');
return str;
}
JavaScriptのコードはFeedのURLを配列で受け取って複数Feedを合成するように作ってあるんだが、
KDE4時代も元々1ウィジェット1Feedで使ってたし、設定UI作るのめんどいから1Feedしか登録できないようにした。
タイトルと記事URL以外に、要約と日付も取得しているんだが表示してない。
ツールチップか何かで表示しようか考えてパース処理は作ったんだが。
データの保存は当初JavaScriptのlocalStorage(DOMストレージ)が使えると思ってたんだが、QMLでは無理らしい。
QtにlocalStorageという機能があるんだが、これはWeb SQL Database相当のもので、Sqliteクエリ投げる感じのやつ。
設定画面で[適用]ボタン押した時に処理する方法がマジわかんない・・・