From f8b0b2279a0e2903482ed410270b3d7c672bc614 Mon Sep 17 00:00:00 2001 From: jnphilipp Date: Mon, 9 Oct 2017 21:31:29 +0200 Subject: [PATCH] Refactor to python3. --- .gitignore | 5 +- Makefile | 174 ++ README.md | 27 +- dark/indicator-feedindicator-attention.png | Bin 748 -> 0 bytes dark/indicator-feedindicator.png | Bin 729 -> 0 bytes debian_packaging/generate_debian_package.sh | 32 - debian_packaging/skeleton/DEBIAN/control | 14 - debian_packaging/skeleton/DEBIAN/postinst | 13 - debian_packaging/skeleton/DEBIAN/prerm | 13 - .../usr/share/doc/feedindicator/copyright | 27 - feedindicator | 1529 ----------------- feedindicator-icon.png | Bin 1441 -> 0 bytes feedindicator-install.sh | 29 - feedindicator-uninstall.sh | 44 - feedindicator.bash-completion | 12 + feedindicator.desktop | 10 - feedindicator/__init__.py | 40 + feedindicator/__main__.py | 30 + feedindicator/config/__init__.py | 25 + feedindicator/config/constants.py | 52 + feedindicator/config/manager.py | 113 ++ feedindicator/dialogs/__init__.py | 29 + feedindicator/dialogs/about.py | 54 + feedindicator/dialogs/add_feed.py | 131 ++ feedindicator/dialogs/preferences.py | 355 ++++ feedindicator/feedindicator.py | 100 ++ feedindicator/feeds.py | 174 ++ feedindicator/indicator.py | 500 ++++++ .../locale/de_DE/LC_MESSAGES/feedindicator.mo | Bin 0 -> 3606 bytes .../locale/de_DE/LC_MESSAGES/feedindicator.po | 221 +++ feedindicator/threads/__init__.py | 27 + feedindicator/threads/feeds.py | 50 + feedindicator/utils/__init__.py | 29 + feedindicator/utils/autostart.py | 47 + feedindicator/utils/core.py | 34 + feedindicator/utils/db.py | 55 + feedindicator/utils/sqlite.py | 109 ++ feedindicator/utils/version.py | 47 + hicolor/indicator-feedindicator-attention.png | Bin 654 -> 0 bytes hicolor/indicator-feedindicator.png | Bin 693 -> 0 bytes icons/active.png | Bin 0 -> 6816 bytes icons/attention.png | Bin 0 -> 6781 bytes .../logo-128x128.png | Bin .../logo-48x48.png | Bin languages/english.properties | 59 - languages/german.properties | 59 - light/indicator-feedindicator-attention.png | Bin 654 -> 0 bytes light/indicator-feedindicator.png | Bin 693 -> 0 bytes 48 files changed, 2434 insertions(+), 1835 deletions(-) create mode 100644 Makefile delete mode 100644 dark/indicator-feedindicator-attention.png delete mode 100644 dark/indicator-feedindicator.png delete mode 100755 debian_packaging/generate_debian_package.sh delete mode 100644 debian_packaging/skeleton/DEBIAN/control delete mode 100755 debian_packaging/skeleton/DEBIAN/postinst delete mode 100755 debian_packaging/skeleton/DEBIAN/prerm delete mode 100644 debian_packaging/skeleton/usr/share/doc/feedindicator/copyright delete mode 100755 feedindicator delete mode 100644 feedindicator-icon.png delete mode 100755 feedindicator-install.sh delete mode 100755 feedindicator-uninstall.sh create mode 100644 feedindicator.bash-completion delete mode 100644 feedindicator.desktop create mode 100644 feedindicator/__init__.py create mode 100644 feedindicator/__main__.py create mode 100644 feedindicator/config/__init__.py create mode 100644 feedindicator/config/constants.py create mode 100644 feedindicator/config/manager.py create mode 100644 feedindicator/dialogs/__init__.py create mode 100644 feedindicator/dialogs/about.py create mode 100644 feedindicator/dialogs/add_feed.py create mode 100644 feedindicator/dialogs/preferences.py create mode 100644 feedindicator/feedindicator.py create mode 100644 feedindicator/feeds.py create mode 100644 feedindicator/indicator.py create mode 100644 feedindicator/locale/de_DE/LC_MESSAGES/feedindicator.mo create mode 100644 feedindicator/locale/de_DE/LC_MESSAGES/feedindicator.po create mode 100644 feedindicator/threads/__init__.py create mode 100644 feedindicator/threads/feeds.py create mode 100644 feedindicator/utils/__init__.py create mode 100644 feedindicator/utils/autostart.py create mode 100644 feedindicator/utils/core.py create mode 100644 feedindicator/utils/db.py create mode 100644 feedindicator/utils/sqlite.py create mode 100644 feedindicator/utils/version.py delete mode 100644 hicolor/indicator-feedindicator-attention.png delete mode 100644 hicolor/indicator-feedindicator.png create mode 100644 icons/active.png create mode 100644 icons/attention.png rename feedindicator-logo.png => icons/logo-128x128.png (100%) rename feedindicator-48x48.png => icons/logo-48x48.png (100%) delete mode 100644 languages/english.properties delete mode 100644 languages/german.properties delete mode 100644 light/indicator-feedindicator-attention.png delete mode 100644 light/indicator-feedindicator.png diff --git a/.gitignore b/.gitignore index 05cc6ff..7e287d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -debian_packaging/feedindicator_*.deb -debian_packaging/package +__pycache__ +bin +build diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..733dbe6 --- /dev/null +++ b/Makefile @@ -0,0 +1,174 @@ +BASH_COMPLETION_DIR=/usr/share/bash-completion/completions/ +BIN_DIR=/usr/bin/ +DOC_DIR=/usr/share/doc/feedindicator/ +MAN_DIR=/usr/share/man/man1/ +SHARE_DIR=/usr/share/feedindicator/ + + +all: bin/feedindicator bin/feedindicator.desktop + + +bin: + @mkdir bin + + +build: + @mkdir build + + +build/package/DEBIAN: build + @mkdir -p build/package/DEBIAN + + +bin/feedindicator: bin + @echo "#!/usr/bin/env bash\n" > bin/feedindicator + @echo "cd $(SHARE_DIR)" >> bin/feedindicator + @echo "python3 -m feedindicator \$$@" >> bin/feedindicator + @chmod a+x bin/feedindicator + + +bin/feedindicator.desktop: bin + @echo "[Desktop Entry]" > bin/feedindicator.desktop + @echo "Version=2.0.0" >> bin/feedindicator.desktop + @echo "Type=Application" >> bin/feedindicator.desktop + @echo "Terminal=false" >> bin/feedindicator.desktop + @echo "Exec=feedindicator" >> bin/feedindicator.desktop + @echo "Icon=feedindicator" >> bin/feedindicator.desktop + @echo "Name=Feedindicator" >> bin/feedindicator.desktop + @echo "Comment=A RSS feed reader for the indicator area." >> bin/feedindicator.desktop + @echo "Categories=Internet;Network;" >> bin/feedindicator.desktop + @chmod a+x bin/feedindicator.desktop + + +build/package/DEBIAN/md5sums: bin/feedindicator bin/feedindicator.desktop build/copyright build/changelog build/feedindicator.1 build/package/DEBIAN + @mkdir -m 755 -p build/package$(BASH_COMPLETION_DIR) + @mkdir -m 755 -p build/package$(BIN_DIR) + @mkdir -m 755 -p build/package$(DOC_DIR) + @mkdir -m 755 -p build/package$(MAN_DIR) + @mkdir -m 755 -p build/package$(SHARE_DIR) + @find build/package -type d -exec chmod 755 {} \; + + @cp -r bin/feedindicator build/package$(BIN_DIR) + @chmod 755 build/package$(BIN_DIR)feedindicator + @cp bin/feedindicator.desktop build/package$(SHARE_DIR) + @chmod 755 build/package$(SHARE_DIR)feedindicator.desktop + @cp -r feedindicator build/package$(SHARE_DIR) + @cp -r icons build/package$(SHARE_DIR) + @find build/package$(SHARE_DIR) -type f -exec chmod 644 {} \; + @find build/package$(SHARE_DIR) -type d -exec chmod 755 {} \; + @cp feedindicator.bash-completion build/package$(BASH_COMPLETION_DIR) + @chmod 644 build/package$(BASH_COMPLETION_DIR)feedindicator.bash-completion + + @cat build/feedindicator.1 | gzip -n9 > build/package$(MAN_DIR)feedindicator.1.gz + @chmod 644 build/package$(MAN_DIR)feedindicator.1.gz + + @cat build/changelog | gzip -n9 > build/package$(DOC_DIR)changelog.gz + @chmod 644 build/package$(DOC_DIR)changelog.gz + + @cp build/copyright build/package$(DOC_DIR)copyright + @chmod 644 build/package$(DOC_DIR)copyright + + @mkdir -p build/package/DEBIAN + @md5sum `find build/package -type f -not -path "*DEBIAN*"` > build/md5sums + @sed -e "s/build\/package\///" build/md5sums > build/package/DEBIAN/md5sums + @chmod 644 build/package/DEBIAN/md5sums + + +build/package/DEBIAN/control: build/package/DEBIAN/md5sums + @echo "Package: feedindicator" > build/package/DEBIAN/control + @echo "Version: `grep "__version_info__ =" feedindicator/__init__.py | grep -oE "[0-9]+, [0-9]+, [0-9]+" | sed -e "s/, /./g"`" >> build/package/DEBIAN/control + @echo "Section: web" >> build/package/DEBIAN/control + @echo "Priority: optional" >> build/package/DEBIAN/control + @echo "Architecture: all" >> build/package/DEBIAN/control + @echo "Depends: python3 (>= 3), python3-feedparser, python3-gi, hicolor-icon-theme, indicator-application, xdg-utils" >> build/package/DEBIAN/control + @echo "Installed-Size: `du -csk build/package/usr | grep -oE "[0-9]+\stotal" | cut -f 1`" >> build/package/DEBIAN/control + @echo "Maintainer: Nathanael Philipp " >> build/package/DEBIAN/control + @echo "Homepage: https://github.com/jnphilipp/Feedindicator" >> build/package/DEBIAN/control + @echo "Description: RSS feed updates in the indicator area\n Editable, sortable list of feed URLs.\n Notification popups of new feed items.\n Adjustable update timer." >> build/package/DEBIAN/control + + +build/package/DEBIAN/postinst: build/package/DEBIAN + @echo "#!/bin/sh -e" > build/package/DEBIAN/postinst + @echo "xdg-icon-resource install --theme hicolor --novendor --size 512 $(SHARE_DIR)icons/active.png feedindicator-active" >> build/package/DEBIAN/postinst + @echo "xdg-icon-resource install --theme hicolor --novendor --size 512 $(SHARE_DIR)icons/attention.png feedindicator-attention" >> build/package/DEBIAN/postinst + @echo "xdg-icon-resource install --theme hicolor --novendor --size 128 --context apps $(SHARE_DIR)icons/logo-128x128.png feedindicator" >> build/package/DEBIAN/postinst + @echo "xdg-icon-resource install --theme hicolor --novendor --size 48 --context apps $(SHARE_DIR)icons/logo-48x48.png feedindicator" >> build/package/DEBIAN/postinst + @echo "xdg-desktop-menu install --novendor $(SHARE_DIR)feedindicator.desktop" >> build/package/DEBIAN/postinst + @chmod 755 build/package/DEBIAN/postinst + + +build/package/DEBIAN/prerm: build/package/DEBIAN + @echo "#!/bin/sh -e" > build/package/DEBIAN/prerm + @echo "xdg-icon-resource uninstall --theme hicolor --novendor --size 512 feedindicator-active" >> build/package/DEBIAN/prerm + @echo "xdg-icon-resource uninstall --theme hicolor --novendor --size 512 feedindicator-attention" >> build/package/DEBIAN/prerm + @echo "xdg-icon-resource uninstall --theme hicolor --novendor --size 128 --context apps feedindicator" >> build/package/DEBIAN/prerm + @echo "xdg-icon-resource uninstall --theme hicolor --novendor --size 48 --context apps feedindicator" >> build/package/DEBIAN/prerm + @echo "xdg-desktop-menu uninstall --novendor feedindicator.desktop" >> build/package/DEBIAN/prerm + @chmod 755 build/package/DEBIAN/prerm + + +build/copyright: build + @echo "Upstream-Name: feedindicator\nSource: https://github.com/jnphilipp/Feedindicator\n\nFiles: *\nCopyright: Copyright 2010-2017 Dave Gardner , Michael Judge , Nicolas Raoul , Nathanael Philipp (jnphilipp) \nLicense: GPL-3+\n This program is free software; you can redistribute it\n and/or modify it under the terms of the GNU General Public\n License as published by the Free Software Foundation; either\n version 3 of the License, or (at your option) any later\n version.\n .\n This program is distributed in the hope that it will be\n useful, but WITHOUT ANY WARRANTY; without even the implied\n warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n PURPOSE. See the GNU General Public License for more\n details.\n .\n You should have received a copy of the GNU General Public\n License along with this package; if not, write to the Free\n Software Foundation, Inc., 51 Franklin St, Fifth Floor,\n Boston, MA 02110-1301 USA\n .\n On Debian systems, the full text of the GNU General Public\n License version 3 can be found in the file\n '/usr/share/common-licenses/GPL-3'." > build/copyright + + +build/changelog: build + @git log --oneline --decorate > build/changelog + + +build/feedindicator.1: build + @help2man -n "feedindicator - A RSS feed reader for the indicator area." feedindicator > build/feedindicator.1 + + +install: bin/feedindicator bin/feedindicator.desktop build/copyright build/changelog build/feedindicator.1 + @apt install python3 python3-gi python3-feedparser hicolor-icon-theme indicator-application xdg-utils + @xdg-icon-resource install --theme hicolor --novendor --size 512 icons/active.png feedindicator-active + @xdg-icon-resource install --theme hicolor --novendor --size 512 icons/attention.png feedindicator-attention + @xdg-icon-resource install --theme hicolor --novendor --size 128 --context apps icons/logo-128x128.png feedindicator + @xdg-icon-resource install --theme hicolor --novendor --size 48 --context apps icons/logo-48x48.png feedindicator + @xdg-desktop-menu install --novendor bin/feedindicator.desktop + @mkdir -p $(SHARE_DIR) + @cp -r feedindicator $(SHARE_DIR) + @install bin/feedindicator $(BIN_DIR) + @install feedindicator.bash-completion $(BASH_COMPLETION_DIR) + @cat build/feedindicator.1 | gzip -n9 > $(MAN_DIR)feedindicator.1.gz + @mkdir -p $(DOC_DIR) + @cat build/changelog | gzip -n9 > $(DOC_DIR)changelog.gz + @install build/copyright $(DOC_DIR)copyright + @echo "feedindicator install completed." + + +uninstall: + @rm -r $(SHARE_DIR) + @rm -r $(DOC_DIR) + @rm $(BIN_DIR)/feedindicator + @rm $(BASH_COMPLETION_DIR)feedindicator.bash-completion + @rm $(MAN_DIR)feedindicator.1.gz + @xdg-icon-resource uninstall --theme hicolor --novendor --size 512 feedindicator-active + @xdg-icon-resource uninstall --theme hicolor --novendor --size 512 feedindicator-attention + @xdg-icon-resource uninstall --theme hicolor --novendor --size 128 --context apps feedindicator + @xdg-icon-resource uninstall --theme hicolor --novendor --size 48 --context apps feedindicator + @xdg-desktop-menu uninstall --novendor feedindicator.desktop + @if [ -f ~/.config/feedindicator/feedindicator.desktop ]; then\ + unlink ~/.config/feedindicator/feedindicator.desktop;\ + fi + @if [ -d ~/.cache/feedindicator ]; then\ + rm -r ~/.cache/feedindicator;\ + fi + @if [ -d ~/.config/feedindicator ]; then\ + rm -r ~/.config/feedindicator;\ + fi + @if [ -d ~/.local/share/feedindicator ]; then\ + rm -r ~/.local/share/feedindicator;\ + fi + @echo "feedindicator uninstall completed." + + +deb: build/package/DEBIAN/control build/package/DEBIAN/postinst build/package/DEBIAN/prerm + fakeroot dpkg-deb -b build/package build/feedindicator.deb + lintian -Ivi build/feedindicator.deb + + +clean: + @rm -rf ./bin + @rm -rf ./build + @find . -name __pycache__ -exec rm -rf {} \; diff --git a/README.md b/README.md index 29175df..d75179f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,25 @@ -Feedindicator -============= +# Feedindicator -RSS feed updates in Ubuntu Unity indicator area +A RSS feed reader for the indicator area. -http://code.google.com/p/feedindicator/ +## Requirements + * ```python3``` + * ```python3-gi``` + * ```python3-feedparser``` + * ```indicator-application``` or ```Gtk3 StatusIcon``` + +## Install/Uninstall + * to install run ```make install``` + * to uninstall run ```make uninstall``` + +## Run + To run after install either use the command ```feedindicator``` or the desktop file. + +## Build Deb-Paket +### Requirements + * ```help2man``` + * ```fakeroot``` + * ```lintian``` + +### Build + * run ```make deb``` diff --git a/dark/indicator-feedindicator-attention.png b/dark/indicator-feedindicator-attention.png deleted file mode 100644 index 4dbced3d0b9cf6b75ef69d97e72a7281e07a256e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 748 zcmVPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2ipP? z1u{EsP4$KV00L}DL_t(I%k7m*XcJKwM$e?lBuyqSN=hGE3T}cHg|6bl7D15|vJgbE zf@l|Rl%h}+K_QDSM7k(Nkm}k+gQ8Gqv5P7$N~~z9qy?#>gvQ#II(bZ9lgGH|!$gwC z?)$Iq-2cv*b3bP8z{wg;xcK%ZqW6NpatVnGL&g^(bNJkxZTs0E|jA&Z$~lQ>}yO& zwV(hllP35Y6H-Sp+$=|9-qK!~aaL8G9-CqTXt@2Fd}NVaWP$wh0_wLQXj;Vwcx;Mg zE>#|#zLEui&gw#RcO$yH0m%LMMB=5N!tdpZ=A_E!bk(-G@$d|>XSYbbeM&)&U}*2e zc_Tpm@m>&Wrc@nV@40|bXCVFI1@fcwq~DJr=#5A{1Jrvi)&%RTHOyUon}b4*x{T^~<}8vD7qH#vhQGp8y0br>^6;I*Ph9LwYuVU@&1l>)$n!s^Ii& zfW-5A$d7v|tbfPQ-ii6d6_QgUJDH1zJbMR=?k=q72GO)4@v%El$YVKq4Z&n3J3q0R z5P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2ipP? z1u`xJkP&SF00LP_L_t(I%k7k1NYhamho61uz&RmiOE5Ezm47v1^0 zJLi4h=e*}VhaYTeS)!GT{hE9kpaZmka-EDXfE5@S9h+PVZ%WrB=@g2@6ZN&#*_y)K ztRzLEA{uc~@Cjq@J~)jgbBZAN0A7S+l9h>RX{k-kjWvb(a&3W37B7p{s!SFy%h;8k zDwih&Z11Ok&CM?ihhyS3xk^o8ZWcgO%OyHGyLsF{$dvsfp-?E|0n0V3Mz?kpgs=bg z+jk~xQ%u;V0LaeDr0q-tDce*;s)-5?>t4@I+#W9;&u2!g<2aly&R%GzzP6gG$}&VE zQPeSsBLxPBMhFIi964Nqq3arzM@#Yh1N7eSW56;T^Q%PcxZi96AWkZ!=6Ds&jWy)v z?BRN+0i(%Ge%{`HjwNQtt*7cJ(H0R3g)o|)akagReL2}w=yfbD`7v}qi29%r!4>*L zoNH^Q>&6w*Qd4j^U6`K_(NK2`l`0vB(}jK7xpDBM&CV@DFJiHTi|1RBN+k3>8Nm1B zCpv8r0513J#=&>)KH`maoTp|B={vSlT%1oJ7-VL~jar=!fZOd|KX_5_2>|7#2T4|L z;eaL&fZdsD06epEWT8%kwknfi`Ng8(6JEa^w;N4nhu`nF%*`*j|GRB&>aTtSdsx+irC$(&00000 LNkvXXu0mjf%@#)y diff --git a/debian_packaging/generate_debian_package.sh b/debian_packaging/generate_debian_package.sh deleted file mode 100755 index 9c2c939..0000000 --- a/debian_packaging/generate_debian_package.sh +++ /dev/null @@ -1,32 +0,0 @@ -# Generate the Debian package for feedindicator -# Remember to change the version number in skeleton/DEBIAN/control - -rm -rf package *.deb -cp -r skeleton package -cd package - -# Copy program and icons -mkdir usr/bin -mkdir usr/share/feedindicator -cp ../../feedindicator usr/bin/ -cp ../../*.desktop usr/share/feedindicator/ -cp ../../*.png usr/share/feedindicator/ -cp -r ../../dark/ usr/share/feedindicator/ -cp -r ../../light/ usr/share/feedindicator/ -cp -r ../../hicolor/ usr/share/feedindicator/ - -# Calculate MD5 sums -md5sum $(find usr -type f) > DEBIAN/md5sums -chmod 644 DEBIAN/md5sums - -# Calculate installed size -INSTALLED_SIZE=`du -ks usr|cut -f 1` -sed -e "s/INSTALLED_SIZE/$INSTALLED_SIZE/" ../skeleton/DEBIAN/control > DEBIAN/control - -# Generate Debian package -cd .. -VERSION=`grep "Version" skeleton/DEBIAN/control | sed -e "s/.* //"` -fakeroot dpkg-deb -b package feedindicator_$VERSION.deb - -# Check the package -lintian -Ivi feedindicator_$VERSION.deb diff --git a/debian_packaging/skeleton/DEBIAN/control b/debian_packaging/skeleton/DEBIAN/control deleted file mode 100644 index 2bf9d93..0000000 --- a/debian_packaging/skeleton/DEBIAN/control +++ /dev/null @@ -1,14 +0,0 @@ -Package: feedindicator -Version: 1.05-1 -Section: web -Priority: optional -Architecture: all -Depends: python (>= 2.3), python-feedparser, python-appindicator, hicolor-icon-theme, indicator-application, xdg-utils -Installed-Size: INSTALLED_SIZE -Maintainer: Nicolas Raoul -Homepage: http://code.google.com/p/feedindicator/ -Description: RSS feed updates in the indicator area - Editable, sortable list of feed URLs. - Notification popups of new feed items. - Adjustable update timer. - diff --git a/debian_packaging/skeleton/DEBIAN/postinst b/debian_packaging/skeleton/DEBIAN/postinst deleted file mode 100755 index d3a48c7..0000000 --- a/debian_packaging/skeleton/DEBIAN/postinst +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -xdg-icon-resource install --theme ubuntu-mono-dark --novendor --size 22 /usr/share/feedindicator/dark/indicator-feedindicator.png indicator-feedindicator -xdg-icon-resource install --theme ubuntu-mono-dark --novendor --size 22 /usr/share/feedindicator/dark/indicator-feedindicator-attention.png indicator-feedindicator-attention -xdg-icon-resource install --theme ubuntu-mono-light --novendor --size 22 /usr/share/feedindicator/light/indicator-feedindicator.png indicator-feedindicator -xdg-icon-resource install --theme ubuntu-mono-light --novendor --size 22 /usr/share/feedindicator/light/indicator-feedindicator-attention.png indicator-feedindicator-attention -xdg-icon-resource install --theme hicolor --novendor --size 22 /usr/share/feedindicator/hicolor/indicator-feedindicator.png indicator-feedindicator -xdg-icon-resource install --theme hicolor --novendor --size 22 /usr/share/feedindicator/hicolor/indicator-feedindicator-attention.png indicator-feedindicator-attention -xdg-icon-resource install --theme hicolor --novendor --size 128 --context apps /usr/share/feedindicator/feedindicator-logo.png feedindicator -xdg-icon-resource install --theme hicolor --novendor --size 48 --context apps /usr/share/feedindicator/feedindicator-48x48.png feedindicator - -xdg-desktop-menu install --novendor /usr/share/feedindicator/feedindicator.desktop - diff --git a/debian_packaging/skeleton/DEBIAN/prerm b/debian_packaging/skeleton/DEBIAN/prerm deleted file mode 100755 index 4d00540..0000000 --- a/debian_packaging/skeleton/DEBIAN/prerm +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -xdg-icon-resource uninstall --theme ubuntu-mono-dark --size 22 indicator-feedindicator -xdg-icon-resource uninstall --theme ubuntu-mono-dark --size 22 indicator-feedindicator-attention -xdg-icon-resource uninstall --theme ubuntu-mono-light --size 22 indicator-feedindicator -xdg-icon-resource uninstall --theme ubuntu-mono-light --size 22 indicator-feedindicator-attention -xdg-icon-resource uninstall --theme hicolor --size 22 indicator-feedindicator -xdg-icon-resource uninstall --theme hicolor --size 22 indicator-feedindicator-attention -xdg-icon-resource uninstall --theme hicolor --size 128 --context apps feedindicator -xdg-icon-resource uninstall --theme hicolor --size 48 --context apps feedindicator - -xdg-desktop-menu uninstall feedindicator.desktop - diff --git a/debian_packaging/skeleton/usr/share/doc/feedindicator/copyright b/debian_packaging/skeleton/usr/share/doc/feedindicator/copyright deleted file mode 100644 index 180b7c7..0000000 --- a/debian_packaging/skeleton/usr/share/doc/feedindicator/copyright +++ /dev/null @@ -1,27 +0,0 @@ -Upstream-Name: feedindicator -Source: http://code.google.com/p/feedindicator/ - -Files: * -Copyright: Copyright 2010-2011 Dave Gardner & Michael Judge -License: GPL-3+ - This program is free software; you can redistribute it - and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation; either - version 3 of the License, or (at your option) any later - version. - . - This program is distributed in the hope that it will be - useful, but WITHOUT ANY WARRANTY; without even the implied - warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - PURPOSE. See the GNU General Public License for more - details. - . - You should have received a copy of the GNU General Public - License along with this package; if not, write to the Free - Software Foundation, Inc., 51 Franklin St, Fifth Floor, - Boston, MA 02110-1301 USA - . - On Debian systems, the full text of the GNU General Public - License version 3 can be found in the file - `/usr/share/common-licenses/GPL-3'. - diff --git a/feedindicator b/feedindicator deleted file mode 100755 index 0828720..0000000 --- a/feedindicator +++ /dev/null @@ -1,1529 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# feedindicator -# Copyright (C) 2010-2011 Dave Gardner & Michael Judge -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# [SNIPPET_NAME: Parse an RSS feed] -# [SNIPPET_CATEGORIES: feedparser] -# [SNIPPET_DESCRIPTION: Parse and iterate over the items in an RSS feed] -# [SNIPPET_AUTHOR: Tim Voet ] -# [SNIPPET_DOCS: http://www.feedparser.org/docs/introduction.html] -# [SNIPPET_LICENSE: GPL] - -# need python-feedparser and app-indicator(should come with ubuntu 10.04 and above) - -import pygtk -pygtk.require('2.0') -import glib -import gtk -import appindicator -import feedparser -import gc -import hashlib -import os -import pynotify -import string -import sys -import urllib -import webbrowser -import gconf -import time -import gobject -import shutil -import sqlite3 as lite -from threading import Thread -from time import sleep -from gconf import VALUE_BOOL, VALUE_INT, VALUE_STRING, VALUE_FLOAT -from types import BooleanType, StringType, IntType, FloatType -from configobj import ConfigObj - -# clear out the html element list so all are removed -feedparser._HTMLSanitizer.acceptable_elements = [] - -# common name used for file paths, gconf and indicator identifier -app_identifier = 'feedindicator' - -# indicator icon names -app_indicator_icon = 'indicator-feedindicator' -app_indicator_icon_attention = 'indicator-feedindicator-attention' - -# XDG config -xdg_config_dir = glib.get_user_config_dir() -app_config_dir = os.path.join(xdg_config_dir, app_identifier) - -# XDG cache -xdg_cache_dir = glib.get_user_cache_dir() -app_cache_dir = os.path.join(xdg_cache_dir, app_identifier) - -# /usr/share/ -app_share_dir = '/usr/share/'+app_identifier+'/' -app_icon = os.path.join(app_share_dir, 'feedindicator-icon.png') -app_logo = os.path.join(app_share_dir, 'feedindicator-logo.png') -app_language_dir = os.path.join(app_share_dir, 'languages/') - -# database -app_database = os.path.join(app_config_dir, 'feedindicator.db') - -# language properties file -app_language_file = os.path.join(app_config_dir, 'language.properties') - -# autostart file -app_autostart_folder = os.path.join(xdg_config_dir, 'autostart') -app_autostart_file = os.path.join(app_autostart_folder, app_identifier+'.desktop') - -# general app info mostly used in about dialog -app_name = 'Feedindicator' -app_version = '1.03' -app_comments = 'RSS feed updates in the indicator area' -app_copyright = 'Copyright (C) 2010-2011 Dave Gardner & Michael Judge' -app_website = 'http://code.google.com/p/feedindicator/' -app_license = (('This program is free software: you can redistribute it and/or modify\n'+ -'it under the terms of the GNU General Public License as published by\n'+ -'the Free Software Foundation, either version 3 of the License, or\n'+ -'(at your option) any later version.\n'+ -'\n'+ -'This program is distributed in the hope that it will be useful,\n'+ -'but WITHOUT ANY WARRANTY; without even the implied warranty of\n'+ -'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n'+ -'GNU General Public License for more details.\n'+ -'\n'+ -'You should have received a copy of the GNU General Public License\n'+ -'along with this program. If not, see .')) -app_authors = ['Dave Gardner ','Michael Judge '] -app_documenters = ['Dave Gardner ','Michael Judge '] - -# default autostart status -autostart = False - -# default feed refresh time (minutes*seconds) -refreshtime = 30*60 - -# default stop the timer from running? -stoptimer = False - -# default menu items to show per feed -itemsperfeed = 5 - -# default put feed items in submenus -usesubmenus = False - -# default show notification popups -shownotifications = True - -# default show notification popups at begining and end of update -shownotificationsupdate = True - -# default sound for new posts -sound = "/usr/bin/mplayer -ao alsa /usr/share/sounds/ubuntu/stereo/message.ogg" - -# default position of the feeds top or button of the menu -feedstop = True - -# default shows feeds with no unread posts -unreadfeeds = True - -# language properties -language_properties = None - - - -################################################## -############### SQLite ############### -################################################## -class SQLite: - def get_con(): - return self._con - - con = property(get_con) - - def __init__(self): - self._con = None - - def open(self, path): - """opens a new connection""" - try: - self._con = lite.connect(path) - except lite.Error, e: - print e - - def close(self): - """closes the current connection""" - if self._con: - try: - self._con.close() - except lite.Error, e: - print e - - def execute(self, query, data=()): - """executes the given query and returns the results""" - if self._con: - try: - cur = self._con.cursor() - if data == (): - cur.execute(query) - else: - cur.execute(query, data) - rows = cur.fetchall() - return rows - except lite.Error, e: - print e - - return None - - def executemany(self, query, data): - """executes the given query with executemany()""" - if self._con: - try: - cur = self._con.cursor() - cur.executemany(query, data) - except lite.Error, e: - print e - - def commit(self): - """commits changes to the database""" - if self._con: - try: - self._con.commit() - except lite.Error, e: - self._con.rollback() - print e - - def contains(self, query, data=()): - """checks wheter the given query has results or not""" - if self._con: - try: - cur = self._con.cursor() - if data == (): - cur.execute(query) - else: - cur.execute(query, data) - rows = cur.fetchone() - - if rows: - return True; - else: - return False; - except lite.Error, e: - print e - - - -################################################## -############### Exit ############### -################################################## -class Exit: - def __init__(self, widget): - sys.exit() - - - -################################################## -############### GConf ############### -################################################## -class GConf: - def __init__ (self, appname, allowed={}): - self._domain = '/apps/%s/' % appname - self._allowed = allowed - self._gconf_client = gconf.client_get_default () - - def __getitem__ (self, attr): - return self.get_value (attr) - - def __setitem__ (self, key, val): - allowed = self._allowed - if allowed.has_key(key): - if not key in allowed[key]: - good = ', '.join (allowed[key]) - return False - self.set_value (key, val) - - def _get_type (self, key): - KeyType = type (key) - if KeyType == BooleanType: - return 'bool' - elif KeyType == StringType: - return 'string' - elif KeyType == IntType: - return 'int' - elif KeyType == FloatType: - return 'float' - else: - return None - - # Public functions - def set_allowed (self, allowed): - self._allowed = allowed - - def set_domain (self, domain): - self._domain = domain - - def get_domain (self): - return self._domain - - def get_gconf_client (self): - return self._gconf_client - - def get_value (self, key): - """returns the value of key 'key' """ - if '/' in key: - raise 'GConfError', 'key must not contain /' - value = self._gconf_client.get(self._domain + key) - if value is not None: - ValueType = value.type - if ValueType == VALUE_BOOL: - return value.get_bool() - elif ValueType == VALUE_INT: - return value.get_int() - elif ValueType == VALUE_STRING: - return value.get_string() - elif ValueType == VALUE_FLOAT: - return value.get_float() - else: - return None - else: - return None - - def set_value (self, key, value): - """sets the value of key 'key' to 'value' """ - value_type = self._get_type(value) - if value_type is not None: - if '/' in key: - raise 'GConfError', 'key must not contain /' - func = getattr (self._gconf_client, 'set_' + value_type) - apply(func, (self._domain + key, value)) - - def get_string (self, key): - if '/' in key: - raise 'GConfError', 'key must not contain /' - return self._gconf_client.get_string(self._domain + key) - - def set_string (self, key, value): - if type (value) != StringType: - raise 'GConfError', 'value must be a string' - if '/' in key: - raise 'GConfError', 'key must not contain /' - self._gconf_client.set_string(self._domain + key, value) - - def get_bool (self, key): - if '/' in key: - raise 'GConfError', 'key must not contain /' - return self._gconf_client.get_bool(self._domain + key) - - def set_bool (self, key, value): - if type (value) != IntType and (key != 0 or key != 1): - raise 'GConfError', 'value must be a boolean' - if '/' in key: - raise 'GConfError', 'key must not contain /' - self._gconf_client.set_bool(self._domain + key, value) - - def get_int (self, key): - if '/' in key: - raise 'GConfError', 'key must not contain /' - return self._gconf_client.get_int(self._domain + key) - - def set_int (self, key, value): - if type (value) != IntType: - raise 'GConfError', 'value must be an int' - if '/' in key: - raise 'GConfError', 'key must not contain /' - self._gconf_client.set_int(self._domain + key, value) - - def get_float (self, key): - if '/' in key: - raise 'GConfError', 'key must not contain /' - return self._gconf_client.get_float(self._domain + key) - - def set_float (self, key, value): - if type (value) != FloatType: - raise 'GConfError', 'value must be a float' - if '/' in key: - raise 'GConfError', 'key must not contain /' - self._gconf_client.set_float(self._domain + key, value) - - - -################################################## -############### FeedThread ############### -################################################## -class FeedThread(Thread): - def run(self): - """updates all feeds in the database und updates the infos""" - sq = SQLite() - sq.open(app_database) - - feeds = sq.execute('select feed_url, title, url, img from feeds') - if len(feeds) > 0: - for feed in feeds: - feedinfo = self.parse_feed(feed[0]) - - if feedinfo['success']: - if feed[1] == None: - sq.execute('update feeds set title=? where feed_url=?', (unicode(feedinfo['feeddata']['title']), feed[0])) - if not feed[2] == unicode(feedinfo['feeddata']['link']): - sq.execute('update feeds set url=? where feed_url=?', (unicode(feedinfo['feeddata']['link']), feed[0])) - if not unicode(feedinfo['feeddata']['img']) == 'None': - sq.execute('update feeds set img=? where feed_url=?', (unicode(feedinfo['feeddata']['img']), feed[0])) - - items = () - posts = () - for postdata in feedinfo['postdata']: - if not postdata['date'] == '': - md5 = hashlib.md5(str(postdata['date']) + '@' + str(postdata['title'])).hexdigest() - posts = posts + (md5,) - if not sq.contains('select * from posts where id=?', (md5,)): - items = items + ((md5, unicode(postdata['link']), unicode(postdata['title']), unicode(feed[0])), ) - else: - md5 = hashlib.md5(str(postdata['link'])).hexdigest() - posts = posts + (md5,) - if not sq.contains('select * from posts where id=?', (md5,)): - items = items + ((md5, unicode(postdata['link']), unicode(postdata['title']), unicode(feed[0])), ) - - if not items == (): - sq.executemany('insert into posts (id, post_url, title, feed_url) values (?,?,?,?)', items) - - placeholder= '?' - placeholders= ', '.join(placeholder for unused in posts) - posts = posts + (feed[0],) - sq.execute('delete from posts where read=\'true\' and id not in (%s) and feed_url=?' % placeholders, posts) - - sq.commit() - sq.close() - - gobject.idle_add(indicator.finnished_update) - - def parse_feed(self, url): - """parses a feed and returns the infos""" - feedinfo = {} - feedinfo['feeddata'] = {} - feedinfo['postdata'] = [] - feedinfo['success'] = False - try: - rssfeed = feedparser.parse(url) - postdata = [] - for val in rssfeed.entries: - val.title = string.replace(val.title, '\n', '') - if len(val.title) > 72: - substr = val.title[:72] - substr = substr.rpartition(' ') - if substr[0] != '': - val.title = substr[0] + '...' - - date = '' - if 'published' in val: - date = val.published - elif 'updated' in val: - date = val.updated - elif 'created' in val: - date = val.created - - v = {'title': val.title, 'link': val.link, 'date': date} - postdata.append(v) - feedinfo['postdata'] = postdata - feedimg = None - webimg = None - if 'image' in rssfeed.feed: - if 'href' in rssfeed.feed.image: - webimg = rssfeed.feed.image.href - elif 'url' in rssfeed.feed.image: - webimg = rssfeed.feed.image.url - elif 'logo' in rssfeed.feed: - webimg = rssfeed.feed.logo - elif 'icon' in rssfeed.feed: - webimg = rssfeed.feed.icon - if webimg != None: - ext = webimg.rsplit('.', 1) - if len(ext[1]) > 5 or len(ext[1]) == 0: - ext[0] = webimg - ext[1] = 'jpg' - md5hash = hashlib.md5(str(url)).hexdigest() - localimg = os.path.join(app_cache_dir, md5hash + '.' + ext[1]) - urllib.urlretrieve(webimg, localimg) - feedimg = md5hash + '.' + ext[1] - feedinfo['feeddata'] = {'title': rssfeed.feed.title, 'link': rssfeed.feed.link, 'img': feedimg} - feedinfo['success'] = True - except: - pass - - return feedinfo - - - -################################################## -############### AddFeedDialog ############### -################################################## -class AddFeedDialog: - def __init__(self, widget): - self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) - self.window.set_size_request(450, 120) - self.window.set_position(gtk.WIN_POS_CENTER) - self.window.set_title(language_properties['add_feed'] + ' | ' + app_name) - self.window.connect('delete_event', self.cancel_dialog) - self.window.connect('key-press-event', self.keypress) - - vbox = gtk.VBox(False, 10) - vbox.set_border_width(10) - self.window.add(vbox) - vbox.show() - - hboxl = gtk.HBox(False, 0) - vbox.pack_start(hboxl, False, True, 0) - hboxl.show() - - label = gtk.Label(language_properties['add_dialog_column']) - label.set_justify(gtk.JUSTIFY_LEFT) - label.set_line_wrap(True) - hboxl.pack_start(label, False, True, 0) - label.show() - - hbox2 = gtk.HBox(False, 0) - vbox.pack_start(hbox2, False, True, 0) - hbox2.show() - - self._textbox = gtk.Entry() - hbox2.pack_start(self._textbox, True, True, 0) - self._textbox.connect("activate", self.save_dialog) - self._textbox.show() - - hbox3 = gtk.HBox(True, 0) - vbox.pack_end(hbox3, False, True, 5) - hbox3.show() - - button_cancel = gtk.Button(language_properties['cancel']) - hbox3.pack_start(button_cancel, True, True, 0) - button_cancel.connect_object('clicked', self.cancel_dialog, button_cancel) - button_cancel.show() - button_save = gtk.Button(language_properties['add']) - hbox3.pack_start(button_save, True, True, 0) - button_save.connect_object('clicked', self.save_dialog, None) - button_save.show() - - self.window.set_keep_above(True) - self.window.show() - - def cancel_dialog(self, widget, data=None): - """cancel dialog""" - self.window.destroy() - - def save_dialog(self, data): - """save feed to config file and close add feed dialog""" - self.window.set_modal(True) - sq = SQLite() - sq.open(app_database) - sq.execute('insert into feeds (feed_url) values (?)', (unicode(self._textbox.get_text()),)) - sq.commit() - sq.close() - self.window.destroy() - - indicator.update_feeds(None, True, False) - - def keypress(self, widget, data): - """keypress handler""" - if data.keyval == gtk.keysyms.Escape: - self.window.destroy() - elif data.keyval == gtk.keysyms.Return: - self.save_dialog(None) - - - -################################################## -############### AboutDialog ############### -################################################## -class AboutDialog: - def __init__(self, widget): - about = gtk.AboutDialog() - about.set_logo(gtk.gdk.pixbuf_new_from_file(app_logo)) - about.set_name(app_name) - about.set_program_name(app_name) - about.set_version(app_version) - about.set_comments(app_comments) - about.set_copyright(app_copyright) - about.set_license(app_license) - about.set_website(app_website) - about.set_website_label(app_website) - about.set_authors(app_authors) - about.set_documenters(app_documenters) - about.run() - about.destroy() - - - -################################################## -############### ConfigureDialog ############### -################################################## -class ConfigureDialog: - def __init__(self, widget): - self.refreshtimeset = refreshtime - self.itemsperfeedset = itemsperfeed - self.autostartset = autostart - self.shownotificationsset = shownotifications - self.shownotificationsupdateset = shownotificationsupdate - self.usesubmenusset = usesubmenus - self.feedstopset = feedstop - self.unreadfeedsset = unreadfeeds - self.langaugeset = '' - - self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) - self.window.set_size_request(900, 550) - self.window.set_border_width(10) - self.window.set_position(gtk.WIN_POS_CENTER) - self.window.set_title(str(language_properties['configure'] % app_name)) - self.window.connect('delete_event', self.cancel_dialog) - self.window.connect('key-press-event', self.keypress) - - table = gtk.Table(3,6,False) - self.window.add(table) - - notebook = gtk.Notebook() - notebook.set_tab_pos(gtk.POS_LEFT) - table.attach(notebook, 0,6,0,1) - notebook.show() - - #Feed List - frame_feed_list = gtk.Frame(language_properties['feed_list']) - frame_feed_list.set_border_width(10) - frame_feed_list.set_size_request(800, 500) - frame_feed_list.show() - - vbox = gtk.VBox(False, 10) - vbox.set_border_width(10) - frame_feed_list.add(vbox) - vbox.show() - - label = gtk.Label(language_properties['configure_dialog_label']) - label.set_justify(gtk.JUSTIFY_LEFT) - label.set_line_wrap(True) - vbox.pack_start(label, False, True, 0) - label.show() - - scrolled_window = gtk.ScrolledWindow() - scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) - scrolled_window.set_shadow_type(gtk.SHADOW_IN) - vbox.pack_start(scrolled_window, True, True, 0) - scrolled_window.show() - - liststore = gtk.ListStore(str, str, str) - - sq = SQLite() - sq.open(app_database) - - feeds = sq.execute('select feed_url, title from feeds order by upper(title)') - for feed in feeds: - liststore.append([unicode(feed[1]), unicode(feed[0]), unicode(feed[0])]) - - sq.close() - - self.treeview = gtk.TreeView(liststore) - textrenderer = gtk.CellRendererText() - self.titlecolumn = gtk.TreeViewColumn(language_properties['column_title']) - self.treeview.append_column(self.titlecolumn) - self.titlecolumn.pack_start(textrenderer, True) - self.titlecolumn.add_attribute(textrenderer, 'text', 0) - textrenderer.set_property('editable', True) - textrenderer.connect('edited', self.cell_edited, liststore, 0) - - textrenderer = gtk.CellRendererText() - self.urlcolumn = gtk.TreeViewColumn(language_properties['column_url']) - self.treeview.append_column(self.urlcolumn) - self.urlcolumn.pack_start(textrenderer, True) - self.urlcolumn.add_attribute(textrenderer, 'text', 1) - textrenderer.set_property('editable', True) - textrenderer.connect('edited', self.cell_edited, liststore, 1) - - self.treeview.set_headers_clickable(False) - self.treeview.set_reorderable(True) - self.treeview.connect('cursor-changed', self.selection_made) - scrolled_window.add(self.treeview) - self.treeview.show() - - hbox1 = gtk.HBox(True, 0) - vbox.pack_start(hbox1, False, True, 0) - hbox1.show() - - self.button_change = gtk.Button(language_properties['add_new']) - hbox1.pack_start(self.button_change, True, True, 0) - self.button_change.connect_object('clicked', self.button_add_clicked, liststore) - self.button_change.show() - self.button_remove = gtk.Button(language_properties['remove']) - hbox1.pack_start(self.button_remove, True, True, 0) - self.button_remove.connect_object('clicked', self.button_remove_clicked, liststore) - self.button_remove.set_sensitive(False) - self.button_remove.show() - - notebook.append_page(frame_feed_list, gtk.Label(language_properties['feed_list'])) - - - #Feed Options - frame_feed_options = gtk.Frame(language_properties['feed_options']) - frame_feed_options.set_border_width(10) - frame_feed_options.set_size_request(800, 500) - frame_feed_options.show() - - vbox = gtk.VBox(False, 10) - vbox.set_border_width(10) - frame_feed_options.add(vbox) - vbox.show() - - hbox2sl = gtk.HBox(False, 0) - vbox.pack_start(hbox2sl, False, True, 0) - hbox2sl.show() - - self.scaletimerlabel = gtk.Label('') - hbox2sl.pack_start(self.scaletimerlabel, False, False, 0) - self.scaletimerlabel.show() - - hbox2s = gtk.HBox(False, 0) - vbox.pack_start(hbox2s, False, True, 0) - hbox2s.show() - - adjtimer = gtk.Adjustment(int(self.refreshtimeset/60), 1.0, 90.0, 1.0, 10.0, 0.0) - adjtimer.connect('value_changed', self.timer_scale) - scaletimer = gtk.HScale(adjtimer) - scaletimer.set_draw_value(False) - scaletimer.set_digits(0) - hbox2s.pack_start(scaletimer, True, True, 0) - self.timer_scale(adjtimer) - scaletimer.show() - - hbox2il = gtk.HBox(False, 0) - vbox.pack_start(hbox2il, False, True, 0) - hbox2il.show() - - self.scaleitemslabel = gtk.Label('') - hbox2il.pack_start(self.scaleitemslabel, False, False, 0) - self.scaleitemslabel.show() - - hbox2i = gtk.HBox(False, 0) - vbox.pack_start(hbox2i, False, True, 0) - hbox2i.show() - - adjitems = gtk.Adjustment(int(self.itemsperfeedset), 1.0, 30.0, 1.0, 10.0, 0.0) - adjitems.connect('value_changed', self.items_scale) - scaleitems = gtk.HScale(adjitems) - scaleitems.set_draw_value(False) - scaleitems.set_digits(0) - hbox2i.pack_start(scaleitems, True, True, 0) - self.items_scale(adjitems) - scaleitems.show() - - check_usesubmenus = gtk.CheckButton(language_properties['submenu_feed']) - vbox.pack_start(check_usesubmenus, False, True, 0) - if usesubmenus: - check_usesubmenus.set_active(True) - check_usesubmenus.connect('toggled', self.usesubmenus_toggle) - check_usesubmenus.show() - - check_feedstop = gtk.CheckButton(language_properties['feeds_top_menu']) - vbox.pack_start(check_feedstop, False, True, 0) - if feedstop: - check_feedstop.set_active(True) - check_feedstop.connect('toggled', self.feedstop_toggle) - check_feedstop.show() - - check_unreadfeeds = gtk.CheckButton(language_properties['unread_feeds_menu']) - vbox.pack_start(check_unreadfeeds, False, True, 0) - if unreadfeeds: - check_unreadfeeds.set_active(True) - check_unreadfeeds.connect('toggled', self.unreadfeeds_toggle) - check_unreadfeeds.show() - - notebook.append_page(frame_feed_options, gtk.Label(language_properties['feed_options'])) - - - #Global Options - frame_feed_options = gtk.Frame(language_properties['global_options']) - frame_feed_options.set_border_width(10) - frame_feed_options.set_size_request(800, 500) - frame_feed_options.show() - - vbox = gtk.VBox(False, 10) - vbox.set_border_width(10) - frame_feed_options.add(vbox) - vbox.show() - - check_autostart = gtk.CheckButton(language_properties['run_startup']) - vbox.pack_start(check_autostart, False, True, 0) - if autostart: - check_autostart.set_active(True) - check_autostart.connect('toggled', self.autostart_toggle) - check_autostart.show() - - check_shownotifications = gtk.CheckButton(language_properties['show_notifications']) - vbox.pack_start(check_shownotifications, False, True, 0) - if shownotifications: - check_shownotifications.set_active(True) - check_shownotifications.connect('toggled', self.shownotifications_toggle) - check_shownotifications.show() - - self.check_shownotificationsupdate = gtk.CheckButton(language_properties['show_notifications_update']) - vbox.pack_start(self.check_shownotificationsupdate, False, True, 0) - if not shownotifications: - self.check_shownotificationsupdate.set_sensitive(False) - if shownotificationsupdate: - self.check_shownotificationsupdate.set_active(True) - self.check_shownotificationsupdate.connect('toggled', self.shownotificationsupdate_toggle) - self.check_shownotificationsupdate.show() - - hbox3l = gtk.HBox(False, 0) - vbox.pack_start(hbox3l, False, True, 0) - hbox3l.show() - - entrylabel = gtk.Label(language_properties['signal_command']) - hbox3l.pack_start(entrylabel, False, False, 0) - entrylabel.set_line_wrap(True) - entrylabel.show() - - hbox3 = gtk.HBox(True, 0) - vbox.pack_start(hbox3, False, True, 0) - hbox3.show() - - self._textbox = gtk.Entry() - hbox3.pack_start(self._textbox, True, True, 0) - self._textbox.set_text(sound) - self._textbox.show() - - hbox4 = gtk.HBox(True, 0) - vbox.pack_start(hbox4, False, True, 0) - hbox4.show() - - langaugelabel = gtk.Label(language_properties['choose_language']) - hbox4.pack_start(langaugelabel, True, True, 0) - langaugelabel.show() - - language_store = gtk.ListStore(str, str) - languages = os.listdir(app_language_dir) - i = 0 - index = 0 - for lang in languages: - prop = ConfigObj(os.path.join(app_language_dir, lang)) - language_store.append([lang, prop['language']]) - if prop['language'] == language_properties['language']: - index = i - i = i + 1 - - combobox_language = gtk.ComboBox(language_store) - cell = gtk.CellRendererText() - combobox_language.pack_start(cell, True) - combobox_language.add_attribute(cell, 'text', 1) - combobox_language.set_active(index) - combobox_language.connect('changed', self.language_toggle) - hbox4.pack_start(combobox_language, True, True, 0) - combobox_language.show() - - notebook.append_page(frame_feed_options, gtk.Label(language_properties['global_options'])) - - table.attach(gtk.Label(''), 0,1,1,2) - button_about = gtk.Button(language_properties['about']) - table.attach(button_about, 3,4,1,2) - button_about.connect_object('clicked', AboutDialog, button_about) - button_about.show() - button_cancel = gtk.Button(language_properties['cancel']) - table.attach(button_cancel, 4,5,1,2) - button_cancel.connect_object('clicked', self.cancel_dialog, button_cancel) - button_cancel.show() - button_save = gtk.Button(language_properties['save']) - button_save.connect('clicked', self.save_dialog, liststore) - table.attach(button_save, 5,6,1,2) - button_save.show() - - table.show() - self.window.set_keep_above(True) - self.window.show() - - def selection_made(self, data=None): - """entry clicked so we enable the remove button""" - treesel = self.treeview.get_selection() - sel = treesel.get_selected() - if sel[1] != None: - self.button_remove.set_sensitive(True) - - def cell_edited(self, cell, row, new_text, data, column): - """update entry with edited value""" - data[row][column] = new_text - return - - def button_add_clicked(self, data): - """add entry to list and focus cursor""" - sel = data.append(['', '', '']) - pos = data.get_path(sel) - self.treeview.set_cursor(pos, self.urlcolumn, True) - - def button_remove_clicked(self, data): - """remove selected entry from list""" - treesel = self.treeview.get_selection() - sel = treesel.get_selected() - - sq = SQLite() - sq.open(app_database) - - feed_url = sel[0].get_value(sel[1], 2) - sq.execute('delete from posts where feed_url=?', (feed_url,)) - - img = sq.execute('select img from feeds where feed_url=?', (feed_url,)) - if os.path.exists(os.path.join(app_cache_dir, unicode(img[0]))): - os.remove(os.path.join(app_cache_dir, unicode(img[0]))) - - sq.execute('delete from feeds where feed_url=?', (feed_url,)) - sq.commit() - sq.close() - - data.remove(sel[1]) - self.button_remove.set_sensitive(False) - - def timer_scale(self, widget): - """refreshtimer slider interaction""" - value = widget.get_value(); - if int(value) == 1: - self.refreshtimeset = int(value)*60 - self.scaletimerlabel.set_text(language_properties['update_feed_every_m']) - else: - self.refreshtimeset = int(value)*60 - self.scaletimerlabel.set_text(str(language_properties['update_feed_every'] % str(self.refreshtimeset/60))) - - def items_scale(self, widget): - """scaleitems slider interaction""" - value = widget.get_value(); - if int(value) == 1: - self.itemsperfeedset = int(value) - self.scaleitemslabel.set_text(language_properties['menu_shows_one_item_per_feed']) - else: - self.itemsperfeedset = int(value) - self.scaleitemslabel.set_text(str(language_properties['menu_shows'] % str(self.itemsperfeedset))) - - def autostart_toggle(self, widget): - """check toggle status""" - if widget.get_active(): - self.autostartset = True - else: - self.autostartset = False - - def shownotifications_toggle(self, widget): - """check toggle status""" - if widget.get_active(): - self.shownotificationsset = True - self.check_shownotificationsupdate.set_sensitive(True) - else: - self.shownotificationsset = False - self.check_shownotificationsupdate.set_sensitive(False) - - def shownotificationsupdate_toggle(self, widget): - """check toggle status""" - if widget.get_active(): - self.shownotificationsupdateset = True - else: - self.shownotificationsupdateset = False - - def usesubmenus_toggle(self, widget): - """check toggle status""" - if widget.get_active(): - self.usesubmenusset = True - else: - self.usesubmenusset = False - - def language_toggle(self, widget): - """check toggle status""" - tree_iter = widget.get_active_iter() - if tree_iter != None: - model = widget.get_model() - self.langaugeset = model[tree_iter][0] - - def feedstop_toggle(self, widget): - """check toggle status""" - if widget.get_active(): - self.feedstopset = True - else: - self.feedstopset = False - - def unreadfeeds_toggle(self, widget): - """check toggle status""" - if widget.get_active(): - self.unreadfeedsset = True - else: - self.unreadfeedsset = False - - def autostart_create(self): - """create autostart file""" - global autostart - content = "\n"+"[Desktop Entry]\n"+"Type=Application\n"+"Exec="+app_identifier+" --autostarted\n"+"X-GNOME-Autostart-enabled=true\n"+"Icon="+app_identifier+"\n"+"Name="+app_name+"\n"+"Comment="+app_comments - if not os.path.exists(app_autostart_folder): - os.makedirs(app_autostart_folder, 0700) - f = open(app_autostart_file, 'w') - f.write(content) - f.close() - autostart = True - - def autostart_delete(self): - """remove autostart file""" - global autostart - if os.path.exists(app_autostart_file): - os.remove(app_autostart_file) - autostart = False - - def keypress(self, widget, data): - """keypress handler""" - if data.keyval == gtk.keysyms.Escape: - self.window.destroy() - - def cancel_dialog(self, widget, data=None): - """cancel dialog""" - self.window.destroy() - - def save_dialog(self, widget, data): - """save list to config file and close config dialog""" - global refreshtime - global itemsperfeed - global autostart - global shownotifications - global shownotificationsupdate - global usesubmenus - global sound - global feedstop - global unreadfeeds - global language_properties - - refreshtime = self.refreshtimeset - gconf['refreshtime'] = self.refreshtimeset - itemsperfeed = self.itemsperfeedset - gconf['itemsperfeed'] = self.itemsperfeedset - shownotifications = self.shownotificationsset - gconf['shownotifications'] = self.shownotificationsset - shownotificationsupdate = self.shownotificationsupdateset - gconf['shownotificationsupdate'] = self.shownotificationsupdateset - usesubmenus = self.usesubmenusset - gconf['usesubmenus'] = self.usesubmenusset - feedstop = self.feedstopset - gconf['feedstop'] = self.feedstopset - unreadfeeds = self.unreadfeedsset - gconf['unreadfeeds'] = self.unreadfeedsset - sound = self._textbox.get_text() - gconf['sound'] = self._textbox.get_text() - autostart = self.autostartset - if autostart == True: - self.autostart_create() - else: - self.autostart_delete() - - if not self.langaugeset == '': - shutil.copyfile(os.path.join(app_language_dir, self.langaugeset), app_language_file) - language_properties = ConfigObj(app_language_file) - - sq = SQLite() - sq.open(app_database) - - for row in data: - if row[2] == '' and row[0] == '': - sq.execute('insert into feeds (feed_url) values (?)', (unicode(row[1]),)) - elif row[2] == '' and row[0] != '': - sq.execute('insert into feeds (feed_url, title) values (?, ?)', (unicode(row[1]), unicode(row[0]))) - elif row[2] == row[1]: - sq.execute('update feeds set title=? where feed_url=?', (unicode(row[0]), unicode(row[2]))) - else: - sq.execute('update posts set feed_url=? where feed_url=?', (unicode(row[1]), unicode(row[2]))) - sq.execute('update feeds set title=?, feed_url=? where feed_url=?', (unicode(row[0]), unicode(row[1]), unicode(row[2]))) - - sq.commit() - sq.close() - - self.window.destroy() - indicator.update_feeds(None, True, True) - - - -################################################## -############### AppIndicator ############### -################################################## -class AppIndicator: - def __init__(self): - global refreshtime - global stoptimer - global itemsperfeed - global autostart - global shownotifications - global shownotificationsupdate - global usesubmenus - global feedstop - global unreadfeeds - global sound - - if gconf['refreshtime'] is not None: - refreshtime = gconf['refreshtime']; - else: - gconf['refreshtime'] = refreshtime; - - if gconf['itemsperfeed'] is not None: - itemsperfeed = gconf['itemsperfeed']; - else: - gconf['itemsperfeed'] = itemsperfeed; - - if gconf['stoptimer'] is not None: - stoptimer = gconf['stoptimer']; - else: - gconf['stoptimer'] = stoptimer; - - if gconf['shownotifications'] is not None: - shownotifications = gconf['shownotifications']; - else: - gconf['shownotifications'] = shownotifications; - - if gconf['shownotificationsupdate'] is not None: - shownotificationsupdate = gconf['shownotificationsupdate']; - else: - gconf['shownotificationsupdate'] = shownotificationsupdate; - - if gconf['usesubmenus'] is not None: - usesubmenus = gconf['usesubmenus']; - else: - gconf['usesubmenus'] = usesubmenus; - - if gconf['feedstop'] is not None: - feedstop = gconf['feedstop']; - else: - gconf['feedstop'] = feedstop; - - if gconf['unreadfeeds'] is not None: - unreadfeeds = gconf['unreadfeeds']; - else: - gconf['unreadfeeds'] = unreadfeeds; - - if gconf['sound'] is not None: - sound = gconf['sound']; - else: - gconf['sound'] = sound; - - if os.path.exists(app_autostart_file): - autostart = True - - if not os.path.exists(app_database): - sq = SQLite() - sq.open(app_database) - sq.execute('CREATE TABLE feeds (feed_url TEXT PRIMARY KEY, url TEXT, title TEXT, img TEXT)') - sq.execute('CREATE TABLE posts (id TEXT PRIMARY KEY, post_url TEXT, title TEXT, read BOOLEAN DEFAULT false, feed_url TEXT NOT NULL, FOREIGN KEY(feed_url) REFERENCES feeds(feed_url))') - - app_config_file = os.path.join(app_config_dir, 'feeds.cfg') - app_cache_file = os.path.join(app_cache_dir, 'feeds.dat') - - if os.path.exists(app_config_file): - rssf = open(app_config_file) - for line in rssf: - line = string.replace(line, '\n', '') - sq.execute('insert into feeds (feed_url) values (?)', (unicode(line),)) - - rssf.close() - os.remove(app_config_file) - - if os.path.exists(app_cache_file): - os.remove(app_cache_file) - - sq.commit() - sq.close() - else: - sq = SQLite() - sq.open(app_database) - - posts = sq.execute('select id from posts') - if posts == None: - sq.execute('DROP TABLE posts') - sq.execute('CREATE TABLE posts (id TEXT PRIMARY KEY, post_url TEXT, title TEXT, read BOOLEAN DEFAULT false, feed_url TEXT NOT NULL, FOREIGN KEY(feed_url) REFERENCES feeds(feed_url))') - - sq.commit() - sq.close() - - self.ind = appindicator.Indicator(app_name, app_identifier, appindicator.CATEGORY_APPLICATION_STATUS) - self.ind.set_status(appindicator.STATUS_ACTIVE) #STATUS_PASSIVE = hidden, STATUS_ACTIVE = visible, STATUS_ATTENTION = want the users attention - self.ind.set_attention_icon(app_indicator_icon_attention) - self.ind.set_icon(app_indicator_icon) - - self.menu = gtk.Menu() - self.update_feeds(None, True, True) - - def set_status(self, status): - """turns the appindicator to attention and back to normal""" - if status: - self.ind.set_status(appindicator.STATUS_ATTENTION) - else: - self.ind.set_status(appindicator.STATUS_ACTIVE) - - def send_notification(self, title, message, image): - """send feed updates to notify-osd""" - if not title == None: - pynotify.init(title) - if image == None: - image = 'file://' + app_icon - n = pynotify.Notification((title if not title == '' else ''), message, image) - n.set_hint_string('x-canonical-append','') - n.show() - - def menuitem_response_website(self, data, url): - """open the feed's website url""" - webbrowser.open(url) - - def menuitem_response(self, data, post_id, url): - """open the website page url""" - webbrowser.open(url) - - sq = SQLite() - sq.open(app_database) - sq.execute('update posts set read =\'true\' where id =? ', (post_id,)) - sq.commit() - sq.close() - - sleep(0.5) - self.render_menu() - - def updates_toggle(self, data): - """toggle updating""" - global stoptimer - if stoptimer == True: - stoptimer = False - gconf['stoptimer'] = False - if shownotifications: - self.send_notification(app_name + ' - ' + language_properties['started'], str(language_properties['feed_update_time_notification'] % str(refreshtime/60)), app_logo) - self.update_feeds(None, True, True) - else: - stoptimer = True - gconf['stoptimer'] = True - if shownotifications: - self.send_notification(app_name + ' - ' + language_properties['stopped'], language_properties['feed_update_stop_notification'], app_logo) - - def render_menu_empty(self, data): - """populate an empty menu""" - for child in self.menu.get_children(): - child.destroy() - menu_loading = gtk.MenuItem(language_properties['loading']) - self.menu.append(menu_loading) - menu_loading.set_sensitive(False) - self.menu.show_all() - self.ind.set_menu(self.menu) - self.set_status(False) - - def render_menu(self): - """populate the menu""" - global stoptimer - global feedstop - global unreadfeeds - - for child in self.menu.get_children(): - child.destroy() - - sq = SQLite() - sq.open(app_database) - - if unreadfeeds: - feeds = sq.execute('select title, url, feed_url, (select count(*) from posts where posts.feed_url=feeds.feed_url and read=\'false\') as c from feeds order by c desc, upper(title)') - else: - feeds = sq.execute('select title, url, feed_url, (select count(*) from posts where posts.feed_url=feeds.feed_url and read=\'false\') as c from feeds where c > 0 order by c desc, upper(title)') - empty = len(feeds) > 0 - hasfeeds = len(sq.execute('select count(*) from feeds')) > 0 - - if feedstop == False: - self.render_menu_conf(hasfeeds) - if hasfeeds: - self.menu.append(gtk.SeparatorMenuItem()) - - if not empty: - no_unread = gtk.MenuItem('No unread feeds.') - self.menu.append(no_unread) - no_unread.set_sensitive(False) - - - - for feed in feeds: - posts = sq.execute(str('select id, title, post_url from posts where feed_url=? and read == \'false\' order by id limit %i' % itemsperfeed), (feed[2],)) - if usesubmenus == True: - menu_header = self.create_menu_header(('(' + unicode(feed[3]) + ') ' if not feed[3] == 0 else '') + unicode(feed[0]), '') - menu_row = self.create_menu_header(language_properties['open_website'], feed[1]) - submenu = gtk.Menu() - submenu.append(menu_row) - submenu_open_unread = gtk.MenuItem(language_properties['open_all_unread']) - submenu_open_unread.connect('activate', self.open_unread, feed[2]) - submenu.append(submenu_open_unread) - - if int(feed[3]) > itemsperfeed: - submenu_open2 = gtk.MenuItem(language_properties['open_displayed']) - submenu_open2.connect('activate', self.feed_open_displayed, feed[2]) - submenu.append(submenu_open2) - - submenu_read = gtk.MenuItem(language_properties['mark_as_read']) - submenu_read.connect('activate', self.feed_read, feed[2]) - submenu.append(submenu_read) - - if int(feed[3]) > itemsperfeed: - submenu_read2 = gtk.MenuItem(language_properties['mark_displayed_as_read']) - submenu_read2.connect('activate', self.feed_read_displayed, feed[2]) - submenu.append(submenu_read2) - - submenu_read = gtk.MenuItem(language_properties['mark_as_unread']) - submenu_read.connect('activate', self.feed_unread, feed[2]) - submenu.append(submenu_read) - - if len(posts) > 0: - posts.reverse() - submenu.append(gtk.SeparatorMenuItem()) - - for post in posts: - menu_row = self.create_menu_item(post[1], post[0], post[2]) - submenu.append(menu_row) - - menu_header.set_submenu(submenu) - self.menu.append(menu_header) - else: - menu_header = self.create_menu_header(('(' + unicode(feed[3]) + ') ' if not feed[3] == 0 else '') + unicode(feed[0]), feed[1]) - self.menu.append(menu_header) - - if len(posts) > 0: - posts.reverse() - - for post in posts: - menu_row = self.create_menu_item(post[1], post[0], post[2]) - self.menu.append(menu_row) - - hasunread = sq.contains('select * from posts where read=\'false\'') - - sq.close() - - if feedstop == True: - self.render_menu_conf(hasfeeds) - - self.menu.show_all() - self.ind.set_menu(self.menu) - - if hasunread: - self.set_status(True) - else: - self.set_status(False) - - def render_menu_conf(self, hasfeeds): - global stoptimer - global feedstop - - if hasfeeds: - if feedstop == True: - self.menu.append(gtk.SeparatorMenuItem()) - - menu_open_unread = gtk.MenuItem(language_properties['open_all_unread']) - menu_open_unread.connect('activate', self.open_unread, '') - self.menu.append(menu_open_unread) - menu_read = gtk.MenuItem(language_properties['mark_all_as_read']) - menu_read.connect('activate', self.feed_read, '') - self.menu.append(menu_read) - menu_unread = gtk.MenuItem(language_properties['mark_all_as_unread']) - menu_unread.connect('activate', self.feed_unread, '') - self.menu.append(menu_unread) - menu_update = gtk.MenuItem(language_properties['reload_all_feeds']) - menu_update.connect('activate', self.update_feeds, True, False) - self.menu.append(menu_update) - self.menu.append(gtk.SeparatorMenuItem()) - if refreshtime/60 == 1: - menu_updatecheck = gtk.CheckMenuItem(language_properties['update_feed_every_m']) - else: - menu_updatecheck = gtk.CheckMenuItem(str(language_properties['update_feed_every'] % str(refreshtime/60))) - if stoptimer == True: - menu_updatecheck.set_active(False) - else: - menu_updatecheck.set_active(True) - menu_updatecheck.connect('toggled', self.updates_toggle) - self.menu.append(menu_updatecheck) - else: - menu_notice = gtk.MenuItem(language_properties['no_feeds_defined']) - self.menu.append(menu_notice) - menu_notice.set_sensitive(False) - self.menu.append(gtk.SeparatorMenuItem()) - - menu_add = gtk.MenuItem(language_properties['add_feed']) - self.menu.append(menu_add) - menu_add.connect('activate', AddFeedDialog) - menu_configure = gtk.MenuItem(str(language_properties['configure'] % app_name)) - self.menu.append(menu_configure) - menu_configure.connect('activate', ConfigureDialog) - menu_quit = gtk.MenuItem(language_properties['quit']) - self.menu.append(menu_quit) - menu_quit.connect('activate', Exit) - - def create_menu_header(self, title, url): - """create the menu heading for each feed""" - menu_row = gtk.MenuItem(title) - if url != '': - menu_row.connect('activate', self.menuitem_response_website, url) - return menu_row - - def create_menu_item(self, title, post_id, url): - """create the menu entries for each feed""" - if usesubmenus == True: - menu_row = gtk.MenuItem(title) - else: - menu_row = gtk.MenuItem(" " + title) - menu_row.connect('activate', self.menuitem_response, post_id, url) - return menu_row - - def open_unread(self, data, url): - """opens all unread post of the given feed""" - sq = SQLite() - sq.open(app_database) - - if not url == '': - posts = sq.execute('select post_url from posts where feed_url=? and read=\'false\'', (url,)) - for post in posts: - webbrowser.open(post[0]) - sleep(0.6) - - sq.execute('update posts set read=\'true\' where feed_url=?', (url,)) - else: - feeds = sq.execute('select feed_url from feeds') - - for feed in feeds: - posts = sq.execute('select post_url from posts where feed_url=? and read=\'false\'', feed) - for post in posts: - webbrowser.open(post[0]) - sleep(0.6) - - sq.execute('update posts set read=\'true\' where feed_url=?', feed) - - sq.commit() - sq.close() - - sleep(0.5) - self.render_menu() - - def feed_open_displayed(self, data, url): - """opens all unread post of the given feed""" - sq = SQLite() - sq.open(app_database) - - posts = sq.execute(str('select post_url from posts where feed_url=? and read=\'false\' order by id limit %i' % itemsperfeed), (url,)) - for post in posts: - webbrowser.open(post[0]) - sleep(0.6) - - sq.execute(str('update posts set read=\'true\' where id IN (select id from posts where feed_url=? and read=\'false\' order by id limit %i)' % itemsperfeed), (url,)) - sq.commit() - sq.close() - - sleep(0.5) - self.render_menu() - - def feed_read(self, data, url): - """marks all posts of all feeds or a specific feed as read""" - sq = SQLite() - sq.open(app_database) - if url == '': - sq.execute('update posts set read=\'true\'') - else: - sq.execute('update posts set read=\'true\' where feed_url=?', (url,)) - sq.commit() - sq.close() - - sleep(0.5) - self.render_menu() - - def feed_read_displayed(self, data, url): - """marks displayed posts of given feed as read""" - sq = SQLite() - sq.open(app_database) - sq.execute(str('update posts set read=\'true\' where feed_url=? and id in (select id from posts where feed_url=? and read == \'false\' order by id limit %i)' % itemsperfeed), (url,url)) - sq.commit() - sq.close() - - sleep(0.5) - self.render_menu() - - def feed_unread(self, data, url): - """marks all posts of all feeds or a specific feed as read""" - sq = SQLite() - sq.open(app_database) - if url == '': - sq.execute('update posts set read=\'false\'') - else: - sq.execute('update posts set read=\'false\' where feed_url=?', (url,)) - sq.commit() - sq.close() - - sleep(0.5) - self.render_menu() - - def update_feeds(self, data=None, timeroverride=False, starttimer=False): - """start updating feeds""" - global stoptimer - - if stoptimer == True and timeroverride == False: - return False - - if starttimer: - glib.timeout_add_seconds(refreshtime, self.update_feeds, True) - - if shownotifications and shownotificationsupdate: - self.send_notification(app_name, language_properties['feed_update_start_notification'], app_logo) - - self.render_menu_empty(None) - FeedThread().start() - return True - - def finnished_update(self): - """renders menu and shows notification after FeadThread has ended""" - self.render_menu() - - if shownotifications: - sq = SQLite() - sq.open(app_database) - - feeds = sq.execute('select title, img, feed_url from feeds where feed_url in (select feed_url from posts where read = \'false\' group by feed_url limit 50) order by (select count(feed_url) as c from posts where read = \'false\' group by feed_url order by c desc limit 49), upper(title)') #notification stack is limit to 50 - - if len(feeds) > 0: - if not sound == '': - os.system(sound) - - for feed in feeds: - count = 0 - notifymsg = '' - posts = sq.execute('select title from posts where read = \'false\' and feed_url=? limit 3', (feed[2],)) - - if len(posts) > 0: - for post in posts: - notifymsg += '\n' + unicode(post[0]) - - if feed[1] == None: - self.send_notification(feed[0], notifymsg, None) - else: - self.send_notification(feed[0], notifymsg, os.path.join(app_cache_dir, feed[1])) - else: - if shownotificationsupdate: - self.send_notification(app_name, language_properties['feed_update_finish_notification'], app_logo) - - sq.close() - - - -################################################## -def main(): - gtk.gdk.threads_init() - gtk.mainloop() - return 0 - -if __name__ == "__main__": - """delay to make sure config files are accessible on autostart""" - cmdline = sys.argv - if len(cmdline) > 1: - if cmdline[1] == '--autostarted': - time.sleep(5) - if gc.isenabled() == False: - gc.enable() - - if not os.path.exists(app_language_file): - shutil.copyfile(os.path.join(app_language_dir, 'english.properties'), app_language_file) - language_properties = ConfigObj(app_language_file) - - gconf = GConf(app_identifier) - indicator = AppIndicator() - main() \ No newline at end of file diff --git a/feedindicator-icon.png b/feedindicator-icon.png deleted file mode 100644 index ea50b84b7cbde1d44a462406a25a3408b7f710da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1441 zcmV;S1z!4zP)=tnCF@!27UVoNp9%Cwa}=wk}=xc45ku@B|sDDJJ-mXTQQ$^6JB0hK&3NFK%pM=^eN@Hl(g#tfNK zl7@U%xnbQN+)5xa7)}E&4>%v0BZxvq45ift@a8?(e66>SKp;cfxkc$a*`(e<$(yZXrpz~SaqCHCzNaaD~>^=r{2lLwNH`UImJ_%r(Hz!t?ZpK;de0@|&}WfB%l_yWA&V&j_9hf!x|q{+7?@RQsHWFQ?V` z`v({Uox)S;zV!~t=YUjff_A9`G#-OKI{`|$=XLXjdh?;(7)B!8Zv*Go)GJ#USYTm6 zWE93vN1=5daHSrv2g>Hww3o48PJ%{fZQponce%jrAz0$72Bvl{IHwWHL(hOPfpWp| z^o#y{()K>*fqD>@QT7lZg9a?%oH}L0UdVO(Ag$k#++M}T} zuJ_(`PuRd6Y3TM`-E|D|qb-nw`yg*>5rU(#UreA4^+5Zx!=Cpw4I&);ePTQeZKU7D zf)(6ufCF4!yVF(OND;fQ+?p=0vGbp}%P4`X`7R@)Q(b}yyEviYJjH=AMqkZ)nZ z!T{g5JJoKmCDpbkWVxQW*lw@w)B(VRn+iefpFa2cAi`jR6Ul^Dc*jDee6er0ZJoQ0 zK)diYNa;p%L;CM!*TNF*Im0xOG{i8+bgD&WFxBxf-|eyIRE&x1;Uoiq=iXezqBlW8 zFO`RC9t+m0m|XHsCR=TH=D9QGDR6FSptcG_m;UIIc(2CWC4>0x8n~~t0JGEMVAw|` z-YD{3ZE7qoI+nWuE*C1IKjhfSZ8_RluXdcbOS%*QC9IJ&$g+WR!FdCILA&nLFx=lS vno~GikYsz_!aJ5b)Sh>7+O6&Re*z2u`siD0Aip}`00000NkvXXu0mjfx+uQB diff --git a/feedindicator-install.sh b/feedindicator-install.sh deleted file mode 100755 index 77039e0..0000000 --- a/feedindicator-install.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh - -sudo apt-get install python-feedparser python-configobj -sudo xdg-icon-resource install --theme ubuntu-mono-dark --novendor --size 22 dark/indicator-feedindicator.png indicator-feedindicator -sudo xdg-icon-resource install --theme ubuntu-mono-dark --novendor --size 22 dark/indicator-feedindicator-attention.png indicator-feedindicator-attention -sudo xdg-icon-resource install --theme ubuntu-mono-light --novendor --size 22 light/indicator-feedindicator.png indicator-feedindicator -sudo xdg-icon-resource install --theme ubuntu-mono-light --novendor --size 22 light/indicator-feedindicator-attention.png indicator-feedindicator-attention -sudo xdg-icon-resource install --theme hicolor --novendor --size 22 hicolor/indicator-feedindicator.png indicator-feedindicator -sudo xdg-icon-resource install --theme hicolor --novendor --size 22 hicolor/indicator-feedindicator-attention.png indicator-feedindicator-attention -sudo xdg-icon-resource install --theme hicolor --novendor --size 128 --context apps feedindicator-logo.png feedindicator -sudo xdg-icon-resource install --theme hicolor --novendor --size 48 --context apps feedindicator-48x48.png feedindicator - -sudo xdg-desktop-menu install --novendor feedindicator.desktop - -if [ ! -d /usr/share/feedindicator/ ] - then - sudo mkdir /usr/share/feedindicator -fi - -sudo cp feedindicator-icon.png /usr/share/feedindicator/feedindicator-icon.png -sudo cp feedindicator-logo.png /usr/share/feedindicator/feedindicator-logo.png -sudo cp -r languages/ /usr/share/feedindicator/ - -sudo cp feedindicator /usr/bin/feedindicator -sudo chmod +x /usr/bin/feedindicator - -MESSAGE="Feedindicator install completed." -echo $MESSAGE - diff --git a/feedindicator-uninstall.sh b/feedindicator-uninstall.sh deleted file mode 100755 index f032bcc..0000000 --- a/feedindicator-uninstall.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/sh - -sudo xdg-icon-resource uninstall --theme ubuntu-mono-dark --size 22 indicator-feedindicator -sudo xdg-icon-resource uninstall --theme ubuntu-mono-dark --size 22 indicator-feedindicator-attention -sudo xdg-icon-resource uninstall --theme ubuntu-mono-light --size 22 indicator-feedindicator -sudo xdg-icon-resource uninstall --theme ubuntu-mono-light --size 22 indicator-feedindicator-attention -sudo xdg-icon-resource uninstall --theme hicolor --size 22 indicator-feedindicator -sudo xdg-icon-resource uninstall --theme hicolor --size 22 indicator-feedindicator-attention -sudo xdg-icon-resource uninstall --theme hicolor --size 128 --context apps feedindicator -sudo xdg-icon-resource uninstall --theme hicolor --size 48 --context apps feedindicator - -sudo xdg-desktop-menu uninstall feedindicator.desktop - -sudo unlink /usr/bin/feedindicator - -sudo unlink /usr/share/feedindicator/feedindicator-icon.png -sudo unlink /usr/share/feedindicator/feedindicator-logo.png - -sudo rmdir /usr/share/feedindicator - -[ ! "$XDG_CACHE_HOME" ] && XDG_CACHE_HOME=~/.cache -cachedir="$XDG_CACHE_HOME"/feedindicator -[ ! "$XDG_CONFIG_HOME" ] && XDG_CONFIG_HOME=~/.config -configdir="$XDG_CONFIG_HOME"/feedindicator -autostartfile="$XDG_CONFIG_HOME"/autostart/feedindicator.desktop - -if [ -f "$autostartfile" ] -then -unlink "$autostartfile" -fi - -if [ -d "$configdir" ] -then -rm -r "$configdir" -fi - -if [ -d "$cachedir" ] -then -rm -r "$cachedir" -fi - -MESSAGE="Feedindicator uninstall completed." -echo $MESSAGE - diff --git a/feedindicator.bash-completion b/feedindicator.bash-completion new file mode 100644 index 0000000..b858f7f --- /dev/null +++ b/feedindicator.bash-completion @@ -0,0 +1,12 @@ +_feedindicator() +{ + local cur prev opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + opts="add update --autostarted -h --help -v --version" + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + return 0 +} +complete -F _feedindicator feedindicator diff --git a/feedindicator.desktop b/feedindicator.desktop deleted file mode 100644 index 99c2196..0000000 --- a/feedindicator.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Version=1.0 -Type=Application -Terminal=false -Exec=feedindicator -Icon=feedindicator -Name=Feedindicator -Comment=RSS feed updates in the indicator area -Categories=Internet;Network; - diff --git a/feedindicator/__init__.py b/feedindicator/__init__.py new file mode 100644 index 0000000..fd1268a --- /dev/null +++ b/feedindicator/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + +"""A RSS feed reader for the indicator area.""" + + +import os.path + + +__author__ = 'Nathanael Philipp' +__copyright__ = 'Copyright 2017 Nathanael Philipp (jnphilipp)' +__license__ = 'GPLv3' +__maintainer__ = __author__ +__email__ = 'mail@jnphilipp.org' +__app_name__ = 'feedindicator' +__version_info__ = (2, 0, 0) +__version__ = '.'.join(str(e) for e in __version_info__) +__description__ = 'A RSS feed reader for the indicator area.' +__github__ = 'https://github.com/jnphilipp/Feedindicator' + +basedir = os.path.dirname(os.path.realpath(__file__)) diff --git a/feedindicator/__main__.py b/feedindicator/__main__.py new file mode 100644 index 0000000..ce3dfa2 --- /dev/null +++ b/feedindicator/__main__.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import sys +import feedindicator.feedindicator + + +if __name__ == '__main__': + sys.exit(feedindicator.feedindicator.main()) diff --git a/feedindicator/config/__init__.py b/feedindicator/config/__init__.py new file mode 100644 index 0000000..a73e023 --- /dev/null +++ b/feedindicator/config/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +from .constants import * +from .manager import ConfigManager diff --git a/feedindicator/config/constants.py b/feedindicator/config/constants.py new file mode 100644 index 0000000..f346773 --- /dev/null +++ b/feedindicator/config/constants.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import feedindicator +import os + +from gi.repository import GLib + + +# indicator icon names +active_icon = 'feedindicator-active' +attention_icon = 'feedindicator-attention' + +# XDG config +xdg_config_dir = GLib.get_user_config_dir() +app_config_dir = os.path.join(xdg_config_dir, feedindicator.__app_name__) +app_autostart_dir = os.path.join(xdg_config_dir, 'autostart') +app_autostart_file = os.path.join(app_autostart_dir, + '%s.desktop' % feedindicator.__app_name__) + +# XDG cache +xdg_cache_dir = GLib.get_user_cache_dir() +app_cache_dir = os.path.join(xdg_cache_dir, feedindicator.__app_name__) + +# XDG data +xdg_data_dir = GLib.get_user_data_dir() +app_data_dir = os.path.join(xdg_data_dir, feedindicator.__app_name__) +app_database = os.path.join(app_data_dir, 'db.sqlite3') + +# # /usr/share/ +base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +app_locale_dir = os.path.join(base_dir, 'locale') diff --git a/feedindicator/config/manager.py b/feedindicator/config/manager.py new file mode 100644 index 0000000..b287343 --- /dev/null +++ b/feedindicator/config/manager.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import os + +from configparser import RawConfigParser +from feedindicator.config.constants import app_config_dir + + +class ConfigManager: + """Configuration manager. All configurations can either be acces through + key words or per '.'. + + Attributes: + _configs: configuration dict + """ + def __init__(self): + """"Init config manager with default configurations.""" + self.__dict__['_configs'] = {# defaults + 'autostart': False, + 'refreshtime': 30 * 60, + 'stoptimer': False, + 'items_per_feed': 10, + 'show_notifications': True, + 'show_update_notifications': True, + 'feeds_at_top': False, + 'show_unread_feeds': True + } + + def __getattr__(self, key): + return self.__dict__['_configs'][key] + + def __setattr__(self, key, val): + if key in self.__dict__['_configs']: + self.__dict__['_configs'][key] = val + else: + raise KeyError(key) + + def __getitem__(self, key): + return self._configs[key] + + def __setitem__(self, key, val): + if key in self._configs: + self._configs[key] = val + else: + raise KeyError(key) + + def items(self): + """Get all configurations.""" + return dict((k, v) for k, v in self._configs.items()) + + def update(self, updates): + """Update configurations with given dict.""" + for k, v in updates.items(): + self._configs[k] = v + + def keys(self): + """Get all configuration names.""" + return self._configs.keys() + + def load(self): + """Load configurations from file.""" + parser = RawConfigParser() + parser.optionxform = str + parser.read(os.path.join(app_config_dir, 'config')) + if parser.has_option('Options', 'autostart'): + self.autostart = parser.getboolean('Options', 'autostart') + if parser.has_option('Options', 'refreshtime'): + self.refreshtime = parser.getint('Options', 'refreshtime') + if parser.has_option('Options', 'stoptimer'): + self.stoptimer = parser.getboolean('Options', 'stoptimer') + if parser.has_option('Options', 'items_per_feed'): + self.items_per_feed = parser.getint('Options', 'items_per_feed') + if parser.has_option('Options', 'show_notifications'): + self.show_notifications = parser.getboolean('Options', + 'show_notifications') + if parser.has_option('Options', 'show_update_notifications'): + self.show_update_notifications = parser. \ + getboolean('Options', 'show_update_notifications') + if parser.has_option('Options', 'feeds_at_top'): + self.feeds_at_top = parser.getboolean('Options', 'feeds_at_top') + if parser.has_option('Options', 'show_unread_feeds'): + self.show_unread_feeds = parser.getboolean('Options', + 'show_unread_feeds') + + def save(self): + """Save configuration to file.""" + parser = RawConfigParser() + parser.optionxform = str + parser.read_dict({'Options': self._configs}) + with open(os.path.join(app_config_dir, 'config'), 'w', + encoding='utf-8') as f: + parser.write(f) diff --git a/feedindicator/dialogs/__init__.py b/feedindicator/dialogs/__init__.py new file mode 100644 index 0000000..4c734ae --- /dev/null +++ b/feedindicator/dialogs/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +from feedindicator.dialogs.about import AboutDialog +from feedindicator.dialogs.add_feed import AddFeedDialog +from feedindicator.dialogs.preferences import PreferencesDialog + + +__all = ('AboutDialog', 'AddFeedDialog', 'PreferencesDialog') diff --git a/feedindicator/dialogs/about.py b/feedindicator/dialogs/about.py new file mode 100644 index 0000000..b4d4a64 --- /dev/null +++ b/feedindicator/dialogs/about.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import feedindicator +import gi +gi.require_version('Gtk', '3.0') + +from feedindicator import config +from gi.repository import Gtk + + +class AboutDialog(Gtk.AboutDialog): + """About dialog.""" + def __init__(self, widget): + """Init dialog. + + Args: + widget: Gtk widget + """ + Gtk.AboutDialog.__init__(self, _('About')) + self.set_icon_name(feedindicator.__app_name__) + self.set_logo(Gtk.IconTheme.get_default(). \ + load_icon(feedindicator.__app_name__, 128, 0)) + self.set_name(feedindicator.__app_name__) + self.set_program_name(feedindicator.__app_name__) + self.set_version(feedindicator.__version__) + self.set_comments(feedindicator.__description__) + self.set_copyright(feedindicator.__copyright__) + self.set_license(feedindicator.__license__) + self.set_website(feedindicator.__github__) + self.set_website_label(feedindicator.__github__) + self.set_authors(feedindicator.__author__.split(', ')) + self.run() + self.destroy() diff --git a/feedindicator/dialogs/add_feed.py b/feedindicator/dialogs/add_feed.py new file mode 100644 index 0000000..e6a805a --- /dev/null +++ b/feedindicator/dialogs/add_feed.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import feedindicator +import gi +gi.require_version('Gtk', '3.0') +import sqlite3 +import sys + +from feedindicator import feeds +from gi.repository import Gtk, Gdk + + +class AddFeedDialog(Gtk.Window): + """Dialog to add a new feed url. + + Attributes: + _callback: callback function after url has been added + _callback_args: arguments for callback function + _textbox: textbox for feed url + """ + def __init__(self, widget, callback, *args): + """Init dialog. + + Args: + widget: Gtk widget + callback: callback function after url has been added + args: arguments for callback function + """ + Gtk.Window.__init__(self, title=_('Add feed')) + self._callback = callback + self._callback_args = args + + self.set_keep_above(True) + self.set_position(Gtk.WindowPosition.CENTER) + self.set_icon_name(feedindicator.__app_name__) + self.connect('delete_event', self._close_window) + self.connect('key-press-event', self._keypress) + + vbox = Gtk.VBox(False, 10) + vbox.set_border_width(10) + self.add(vbox) + + box = Gtk.HBox(False, 0) + vbox.pack_start(box, False, True, 0) + + label = Gtk.Label(label=_('Feed url')) + label.set_justify(Gtk.Justification.LEFT) + label.set_line_wrap(True) + box.pack_start(label, False, True, 0) + + box = Gtk.HBox(False, 0) + vbox.pack_start(box, False, True, 0) + + self._textbox = Gtk.Entry() + box.pack_start(self._textbox, True, True, 0) + self._textbox.connect("activate", self._save) + + box = Gtk.HBox(True, 0) + vbox.pack_end(box, False, True, 5) + + button = Gtk.Button(_('Cancel')) + box.pack_start(button, True, True, 0) + button.connect('clicked', self._close_window) + button = Gtk.Button(_('Add feed')) + box.pack_start(button, True, True, 0) + button.connect('clicked', self._save) + + self.set_keep_above(True) + self.show_all() + + def _keypress(self, widget, data): + """Keypress handler. + + Args: + widget: Gtk widget + data: key event data + """ + if data.keyval == Gdk.KEY_Escape: + self._close_window(widget) + + def _close_window(self, widget, data=None): + """Close dialog. + + Args: + widget: Gtk widget + data: optional key event data + """ + self.close() + + def _save(self, widget): + """Save feed and close dialog. + + Args: + widget: Gtk widget + """ + self.set_modal(True) + try: + if self._textbox.get_text(): + feeds.add(self._textbox.get_text()) + self._close_window(widget) + if self._callback: + self._callback(self._callback_args) + except sqlite3.Error as e: + print('Could not add feed.', e, file=sys.stderr) + dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, + Gtk.ButtonsType.CANCEL, + _('Could not add feed.')) + dialog.format_secondary_text(str(e)) + dialog.run() + dialog.destroy() diff --git a/feedindicator/dialogs/preferences.py b/feedindicator/dialogs/preferences.py new file mode 100644 index 0000000..edb0081 --- /dev/null +++ b/feedindicator/dialogs/preferences.py @@ -0,0 +1,355 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import feedindicator +import gettext +import gi +gi.require_version('Gtk', '3.0') +import os + +from feedindicator import config, feeds +from feedindicator.utils import autostart, SQLite +from gi.repository import Gtk, Gdk + + +class PreferencesDialog(Gtk.Window): + """Preferences dialog. + + Attributes: + _configs: dict with configs + _config_manager: config manager + _callback: callback function after url has been added + _callback_args: arguments for callback function + _feeds: list store for the feeds with id, title, feed_url + _treeview: treeview for the feeds + _btn_remove_feed: remove button for feeds + _refreshtime_label: label for refresh time config + _scaletime: scaler for refresh time config + _items_per_feed_label: label for items per feed config + _btn_show_update_notifications: check button for show update + notifications config + """ + def __init__(self, widget, config_manager, callback, *args): + """Init dialog. + + Args: + widget: Gtk widget + configs: dict with configs + callback: callback function after url has been added + *args: callback function args + """ + Gtk.Window.__init__(self, title=_('Preferences')) + self._config_manager = config_manager + self._configs = self._config_manager.items() + self._callback = callback + self._callback_args = args + + self.set_position(Gtk.WindowPosition.CENTER) + self.set_keep_above(True) + self.set_icon_name(feedindicator.__app_name__) + self.connect('delete_event', self._close_window) + self.connect('key-press-event', self._keypress) + + vbox = Gtk.VBox(False, 1) + vbox.set_border_width(1) + self.add(vbox) + + notebook = Gtk.Notebook() + notebook.set_tab_pos(Gtk.PositionType.LEFT) + vbox.pack_start(notebook, False, True, 0) + + # Feed List + frame = Gtk.Frame(label=_('Feed list')) + frame.set_border_width(1) + + box = Gtk.VBox(False, 1) + box.set_border_width(1) + frame.add(box) + + label = Gtk.Label(label=_('Configure the feeds.')) + label.set_justify(Gtk.Justification.LEFT) + label.set_line_wrap(True) + box.pack_start(label, False, True, 0) + + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, + Gtk.PolicyType.ALWAYS) + scrolled_window.set_shadow_type(Gtk.ShadowType.IN) + scrolled_window.set_min_content_height(500) + scrolled_window.set_min_content_width(500) + box.pack_start(scrolled_window, False, True, 0) + + self._feeds = Gtk.ListStore(int, str, str) + with SQLite() as db: + for feed in db.s('SELECT id, title, feed_url FROM feeds ORDER ' + + 'BY UPPER(title)'): + self._feeds.append(feed) + + self._treeview = Gtk.TreeView.new_with_model(self._feeds) + for i, column_title in enumerate([_('ID'), _('Title'), _('URL')]): + renderer = Gtk.CellRendererText() + if i > 0: + renderer.set_property('editable', True) + renderer.connect('edited', self._cell_edited, i) + column = Gtk.TreeViewColumn(column_title, renderer, text=i) + self._treeview.append_column(column) + + self._treeview.set_headers_clickable(False) + self._treeview.set_reorderable(True) + self._treeview.connect('cursor-changed', self._selection_made) + scrolled_window.add(self._treeview) + + hbox = Gtk.HBox(True, 1) + box.pack_start(hbox, False, True, 0) + + btn = Gtk.Button(label=_('Add')) + btn.connect('clicked', self._add_feed) + hbox.pack_start(btn, False, True, 0) + + self._btn_remove_feed = Gtk.Button(label=_('Remove')) + self._btn_remove_feed.connect('clicked', self._remove_feed) + self._btn_remove_feed.set_sensitive(False) + hbox.pack_start(self._btn_remove_feed, False, True, 0) + notebook.append_page(frame, Gtk.Label(label=_('Feed list'))) + + # Options + frame = Gtk.Frame(label=_('Options')) + frame.set_border_width(1) + + box = Gtk.VBox(False, 1) + box.set_border_width(1) + frame.add(box) + + btn = Gtk.CheckButton(label=_('Auto update feeds')) + btn.set_active(not self._configs['stoptimer']) + btn.connect('toggled', self._toggle_config, 'stoptimer') + box.pack_start(btn, False, True, 0) + + hbox = Gtk.HBox(True, 0) + box.pack_start(hbox, False, True, 0) + + self._refreshtime_label = Gtk.Label(label='') + hbox.pack_start(self._refreshtime_label, False, True, 0) + + adjtimer = Gtk.Adjustment(value=self._configs['refreshtime'] / 60, + lower=1.0, upper=90.0, step_increment=1.0, + page_increment=10.0, page_size=0.0) + adjtimer.connect('value_changed', self._change_refreshtime) + self._scaletime = Gtk.HScale(adjustment=adjtimer) + self._scaletime.set_draw_value(False) + self._scaletime.set_digits(0) + self._scaletime.set_sensitive(not self._configs['stoptimer']) + hbox.pack_start(self._scaletime, False, True, 0) + self._change_refreshtime(adjtimer) + + hbox = Gtk.HBox(True, 0) + box.pack_start(hbox, False, True, 0) + + self._items_per_feed_label = Gtk.Label(label='') + hbox.pack_start(self._items_per_feed_label, False, False, 0) + + adjitems = Gtk.Adjustment(value=self._configs['items_per_feed'], + lower=1.0, upper=30.0, step_increment=1.0, + page_increment=10.0, page_size=0.0) + adjitems.connect('value_changed', self._change_items_per_feed) + scaleitems = Gtk.HScale(adjustment=adjitems) + scaleitems.set_draw_value(False) + scaleitems.set_digits(0) + hbox.pack_start(scaleitems, True, True, 0) + self._change_items_per_feed(adjitems) + + btn = Gtk.CheckButton(label=_('Show feeds at top of menu')) + btn.set_active(self._configs['feeds_at_top']) + btn.connect('toggled', self._toggle_config, 'feeds_at_top') + box.pack_start(btn, False, True, 0) + + btn = Gtk.CheckButton(label=_('Show feeds with no unread posts')) + btn.set_active(self._configs['show_unread_feeds']) + btn.connect('toggled', self._toggle_config, 'show_unread_feeds') + box.pack_start(btn, False, True, 0) + + btn = Gtk.CheckButton(label=_('Launch at system startup')) + btn.set_active(self._configs['autostart']) + btn.connect('toggled', self._toggle_config, 'autostart') + box.pack_start(btn, False, True, 0) + + btn = Gtk.CheckButton(label=_('Show notifications')) + btn.set_active(self._configs['show_notifications']) + btn.connect('toggled', self._toggle_config, 'show_notifications') + box.pack_start(btn, False, True, 0) + + self._btn_show_update_notifications = Gtk. \ + CheckButton(label=_('Show notifications at beginning ' + + 'and end of update')) + self._btn_show_update_notifications. \ + set_sensitive(self._configs['show_notifications']) + self._btn_show_update_notifications. \ + set_active(self._configs['show_update_notifications']) + self._btn_show_update_notifications. \ + connect('toggled', self._toggle_config, + 'show_update_notifications') + box.pack_start(self._btn_show_update_notifications, False, True, 0) + + notebook.append_page(frame, Gtk.Label(label=_('Options'))) + + box = Gtk.HBox(True, 0) + vbox.pack_end(box, False, True, 0) + + btn = Gtk.Button(label=_('Cancel')) + btn.connect('clicked', self._close_window) + box.pack_start(btn, False, True, 0) + + btn = Gtk.Button(label=_('Save')) + btn.connect('clicked', self._save) + box.pack_start(btn, False, True, 0) + self.show_all() + + def _keypress(self, widget, data): + """Keypress handler. + + Args: + widget: Gtk widget + data: key event data + """ + if data.keyval == Gdk.KEY_Escape: + self._close_window(widget) + + def _close_window(self, widget, data=None): + """Close dialog. + + Args: + widget: Gtk widget + data: optional key event data + """ + self.close() + + def _save(self, widget): + """Save config and feeds and close dialog. + + Args: + widget: Gtk widget + """ + self._config_manager.update(self._configs) + self._config_manager.save() + with SQLite() as db: + for feed in self._feeds: + if feed[0] == -1 and feed[2]: + if not feed[1]: + feed[1] = None + db.s('INSERT INTO feeds (feed_url, title) VALUES (?,?)', + (feed[2], feed[1])) + else: + db.s('UPDATE feeds set title=?, feed_url=? WHERE id=?', + (feed[1], feed[2], feed[0])) + self._close_window(widget) + self._callback(self._callback_args) + + def _selection_made(self, widget): + """Feed selected from list, so we enable the remove button. + + Args: + widget: Gtk widget + """ + sel = self._treeview.get_selection().get_selected() + if sel[1] is not None: + self._btn_remove_feed.set_sensitive(True) + + def _cell_edited(self, widget, path, text, column): + """Update list store after cell has been edited. + + Args: + widget: Gtk widget + path: row id + text: new text + column: column id + """ + self._feeds[path][column] = text + + def _add_feed(self, widget): + """Add a row to list. + + Args: + widget: Gtk widget + """ + sel = self._feeds.append([-1, '', '']) + pos = self._feeds.get_path(sel) + self._treeview.set_cursor(pos, None, False) + + def _remove_feed(self, widget): + """Remove selected feed from list and delete from database. + + Args: + widget: Gtk widget + """ + sel = self._treeview.get_selection().get_selected() + feed_id = sel[0].get_value(sel[1], 0) + if feed_id != -1: + feeds.delete(feed_id) + self._feeds.remove(sel[1]) + self._treeview.get_selection().unselect_all() + self._btn_remove_feed.set_sensitive(False) + + def _change_refreshtime(self, widget): + """Change refresh time. + + Args: + widget: Gtk widget + """ + value = widget.get_value() + self._configs['refreshtime'] = int(value * 60) + text = gettext.ngettext('Update feeds every minute', + 'Update feeds every %(minutes)d minutes', + value) % {'minutes': value} + self._refreshtime_label.set_text(text) + + def _change_items_per_feed(self, widget): + """Change items per feed. + + Args: + widget: Gtk widget + """ + value = widget.get_value() + self._configs['items_per_feed'] = int(value) + text = gettext.ngettext('Show 1 item per feed', + 'Show %(items)d items per feed', + value) % {'items': value} + self._items_per_feed_label.set_text(text) + + def _toggle_config(self, widget, key): + """Toggles config values. + + Args: + widget: Gtk widget + key: config key + """ + self._configs[key] = widget.get_active() + if key == 'autostart': + if self._configs[key]: + autostart.create() + else: + autostart.delete() + elif key == 'stoptimer': + if self._configs[key]: + self._scaletime.set_sensitive(True) + else: + self._scaletime.set_sensitive(False) diff --git a/feedindicator/feedindicator.py b/feedindicator/feedindicator.py new file mode 100644 index 0000000..6888628 --- /dev/null +++ b/feedindicator/feedindicator.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + +import feedindicator +import gettext +import gi +gi.require_version('Notify', '0.7') +import os +import sqlite3 +import sys + +from argparse import ArgumentParser, RawTextHelpFormatter +from feedindicator import config, feeds, utils +from feedindicator.indicator import AppIndicator +from gi.repository import Gtk, Notify +from time import sleep + + +def _add_feed(args): + try: + feeds.add(args.url) + except sqlite3.Error as e: + print(_('Could not add feed.'), e, file=sys.stderr) + + +def _update_feeds(args): + try: + nb_new_posts = feeds.update() + print(_('Feeds updated. New posts: %(nr)d') % {'nr': nb_new_posts}) + except Exception as e: + print(_('Error while updating feeds.'), e, file=sys.stderr) + + +def main(): + gettext.install(feedindicator.__app_name__, config.app_locale_dir, + codeset="utf-8") + Notify.init(feedindicator.__app_name__) + + if not os.path.exists(config.app_cache_dir): + os.makedirs(config.app_cache_dir) + if not os.path.exists(config.app_config_dir): + os.makedirs(config.app_config_dir) + if not os.path.exists(config.app_data_dir): + os.makedirs(config.app_data_dir) + + parser = ArgumentParser(prog=feedindicator.__app_name__, + description=feedindicator.__description__, + formatter_class=RawTextHelpFormatter) + parser.add_argument('-v', '--version', action='version', + version=utils.app_version()) + parser.add_argument('--autostarted', action='store_true', help='Option ' + + 'to indicate feedindicator was autostarted.') + subparsers = parser.add_subparsers(dest='subparser') + + # create the parser for the "add" subcommand + add_parser = subparsers.add_parser('add', help='Add a new feed.') + add_parser.set_defaults(func=_add_feed) + add_parser.add_argument('url', help='Feed URL') + + # create the parser for the "update" subcommand + update_parser = subparsers.add_parser('update', help='Update feeds.') + update_parser.set_defaults(func=_update_feeds) + + argv = sys.argv[1:] + args = parser.parse_args(argv) + if args.autostarted: + sleep(5) + + config_manager = config.ConfigManager() + config_manager.load() + + try: + utils.db_init() + except sqlite3.Error as e: + print(_('Could not init database.'), e, file=sys.stderr) + + if args.subparser: + args.func(args) + else: + indicator = AppIndicator(config_manager) + Gtk.main() diff --git a/feedindicator/feeds.py b/feedindicator/feeds.py new file mode 100644 index 0000000..3d228e7 --- /dev/null +++ b/feedindicator/feeds.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import feedparser +import json +import os +import urllib.request +import sys + +from feedindicator import config, utils +from feedindicator.utils import SQLite + + +# clear out the html element list so all are removed +feedparser._HTMLSanitizer.acceptable_elements = [] + + +def add(url): + """Add a new feed url to the database. + + Args: + url: feed url + """ + with SQLite() as db: + db.s('INSERT INTO feeds (feed_url) VALUES (?)', (url,)) + print(_('Feed added.')) + + +def delete(feed_id): + """Delete a feed and all its posts from the database. + + Args: + feed_id: feed id + """ + with SQLite() as db: + img = db.s('SELECT img FROM feeds WHERE id=?', (feed_id,))[0][0] + if img and os.path.exists(os.path.join(config.app_cache_dir, img)): + os.remove(os.path.join(config.app_cache_dir, img)) + db.s('DELETE FROM posts WHERE feed_id=?', (feed_id,)) + db.s('DELETE FROM feeds WHERE id=?', (feed_id,)) + + +def update(): + """Update all feeds.""" + new_posts = 0 + with SQLite() as db: + feeds = db.s('SELECT id, feed_url, title, url, img FROM feeds') + for feed in feeds: + result = _parse_feed(feed[1]) + if not result['success']: + continue + + if feed[2] == None: + db.s('UPDATE feeds SET title=? WHERE id=?', + (result['feed']['title'], feed[0])) + if feed[3] != result['feed']['link']: + db.s('UPDATE feeds SET url=? WHERE id=?', + (result['feed']['link'], feed[0])) + if not result['feed']['img']: + db.s('UPDATE feeds SET img=? WHERE id=?', + (result['feed']['img'], feed[0])) + + items = () + hashes = () + for p in result['posts']: + hash = utils.get_hash(p['date'], p['title']) + hashes += (hash,) + if db.s('SELECT COUNT(*) FROM posts WHERE hash=?', + (hash,))[0][0] == 0: + items += ((hash, p['link'], p['title'], + json.dumps(p['raw']), feed[0]),) + + new_posts += len(items) + if len(items) > 0: + db.many('INSERT INTO posts (hash, url, title, raw, feed_id) ' + + 'VALUES (?,?,?,?,?)', items) + db.s('DELETE FROM posts WHERE read="true" AND hash NOT IN ' + '(%s) AND feed_id=?' % ','.join('?' for p in hashes), + hashes + (feed[0],)) + return new_posts + + +def _parse_feed(url): + """Parses a feed and returns the data. + + Args: + url: feed url + """ + result = { + 'feed': {}, + 'posts': [], + 'success': False + } + + try: + rssfeed = feedparser.parse(url) + posts = [] + for e in rssfeed.entries: + e.title = e.title.replace('\n', '') + if len(e.title) > 72: + substr = e.title[:72].rpartition(' ') + if substr[0] != '': + e.title = substr[0] + '...' + + date = None + if 'published' in e: + date = e.published + elif 'updated' in e: + date = e.updated + elif 'created' in e: + date = e.created + posts.append({ + 'title': e.title, + 'link': e.link, + 'date': date, + 'raw': e + }) + result['posts'] = posts + feedimg = None + webimg = None + if 'image' in rssfeed.feed: + if 'href' in rssfeed.feed.image: + webimg = rssfeed.feed.image.href + elif 'url' in rssfeed.feed.image: + webimg = rssfeed.feed.image.url + elif 'logo' in rssfeed.feed: + webimg = rssfeed.feed.logo + elif 'icon' in rssfeed.feed: + webimg = rssfeed.feed.icon + if webimg != None: + ext = webimg.rsplit('.', 1) + if len(ext[1]) > 5 or len(ext[1]) == 0: + ext[0] = webimg + ext[1] = 'jpg' + shash = hashlib.sha512(url.encode('utf-8')).hexdigest() + localimg = os.path.join(config.app_cache_dir, + '%s.%s' % (shash, ext[1])) + feedimg = '%s.%s' % (shash, ext[1]) + + request = urllib.request.Request(webimg) + with urllib.request.urlopen(request) as response: + with open(localimg, 'bw') as f: + f.write(response.read()) + + result['feed'] = { + 'title': rssfeed.feed.title, + 'link': rssfeed.feed.link, + 'img': feedimg + } + result['success'] = True + except Exception as e: + print('Error while parsing feed (feed url: %s).' % url, e, + file=sys.stderr) + return result diff --git a/feedindicator/indicator.py b/feedindicator/indicator.py new file mode 100644 index 0000000..2b5fdcb --- /dev/null +++ b/feedindicator/indicator.py @@ -0,0 +1,500 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + +import feedindicator +import gettext +import gi +gi.require_version('Gtk', '3.0') +import os +import sys +import webbrowser + +from feedindicator import config, feeds +from feedindicator.dialogs import AboutDialog, AddFeedDialog, PreferencesDialog +from feedindicator.threads import FeedThread +from feedindicator.utils import SQLite +from gi.repository import GLib, Gtk, Notify +from time import sleep, time + +try: + gi.require_version('AppIndicator3', '0.1') + from gi.repository import AppIndicator3 as appindicator +except (ImportError, ValueError): + appindicator = None + + +class AppIndicator: + """App indicator. + + Attributes: + _config_manager: config manager + _feeds_thread: feeds thread + _indicator: app indicator + _status_icon: Gtk status icon, if app indicator isn't available + _menu: menu + """ + def __init__(self, config_manager): + """Init indicator. + + Args: + config_manager: config manager + """ + self._config_manager = config_manager + self._feeds_thread = None + + if appindicator: + # Create indicator + self._indicator = appindicator.Indicator. \ + new(feedindicator.__app_name__, feedindicator.__app_name__, + appindicator.IndicatorCategory.APPLICATION_STATUS) + self._indicator.set_status(appindicator.IndicatorStatus.ACTIVE) + self._indicator.set_attention_icon_full(config.attention_icon, + _('New Posts.')) + self._indicator.set_icon_full(config.active_icon, + _('Nothing new.')) + else: + # Create status icon + self._status_icon = Gtk.StatusIcon() + self._status_icon.set_name(feedindicator.__app_name__) + self._status_icon.set_from_icon_name(config.active_icon) + self._status_icon.set_tooltip_text(feedindicator.__app_name__) + + # Create popup menu + self._menu = Gtk.Menu() + if appindicator: + self._menu.show_all() + self._indicator.set_menu(self._menu) + else: + self._status_icon.connect('activate', self._toggle_status_icon) + self._status_icon.connect('popup-menu', self._popup_menu) + self._status_icon.set_visible(True) + + self._update(None, True, True) + + def _popup_menu(self, widget, button, time): + """Callback when the popup menu on the status icon has to open. + + Args: + widget: Gtk widget + button: mouse button + time: activation time + """ + self._menu.show_all() + self._menu.popup(None, None, Gtk.StatusIcon.position_menu, + self._status_icon, button, time) + + def _toggle_status_icon(self, widget): + """Callback when a request to toggle feedindicator was made. + + Args: + widget: Gtk widget + """ + self._popup_menu(widget, 0, Gtk.get_current_event_time()) + + def _set_status(self, status): + """Turns the appindicator to attention and back to normal. + + Args: + status: new indicator status: attention/active + """ + if status and appindicator: + self._indicator.set_status(appindicator.IndicatorStatus.ATTENTION) + elif status and not appindicator: + self._status_icon.set_from_icon_name(config.attention_icon) + elif not status and appindicator: + self._indicator.set_status(appindicator.IndicatorStatus.ACTIVE) + else: + self._status_icon.set_from_icon_name(config.active_icon) + + def _exit(self, widget): + """Close Feedindicator. + + Args: + widget: Gtk widget + """ + Notify.uninit() + Gtk.main_quit() + + def _notify(self, title, msg, img=feedindicator.__app_name__): + """Send feed updates to notify-osd. + + Args: + title: notification title + msg: message + img: image (default: app icon) + """ + n = Notify.Notification.new(title, msg, img) + n.show() + + def _open_website(self, widget, url, post_id=None): + """Open website. + + Args: + widget: Gtk widget + url: url to open + post_id: optional post id to change status to read + """ + webbrowser.open(url) + + if post_id: + with SQLite() as db: + db.s('UPDATE posts SET read="true" WHERE id=?', (post_id,)) + sleep(0.5) + self._render_menu() + + def _update(self, widget=None, timeroverride=False, starttimer=False): + """Start updating feeds. + + Args: + widget: Gtk widget + timeroverride: if true timer will be overridden + starttimer: if true timer will be started + """ + if self._config_manager.stoptimer and not timeroverride: + return False + + with SQLite() as db: + if db.s('SELECT COUNT(*) FROM feeds')[0][0] > 0: + if starttimer: + GLib.timeout_add_seconds(self._config_manager.refreshtime, + self._update, True) + + self._loading_menu() + if db.s('SELECT COUNT(*) FROM feeds')[0][0] != 0: + self._feeds_thread = FeedThread(self._finished_update) + self._feeds_thread.start() + if self._config_manager.show_notifications: + if self._config_manager.show_update_notifications: + self._notify(feedindicator.__app_name__, + _('Begin updating Feeds.')) + else: + self._render_menu() + return True + + def _finished_update(self, *args): + """Renders menu and shows notification after updating is finished.""" + if self._feeds_thread is not None and self._feeds_thread.is_alive(): + return + + if self._feeds_thread: + self._feeds_thread.join() + sleep(1) + self._feeds_thread = None + self._render_menu() + + if self._config_manager.show_notifications: + with SQLite() as db: + feeds = db.s('SELECT id, title, img FROM feeds WHERE ' + + 'feed_url IN (SELECT feed_id FROM posts WHERE ' + + 'read="false" GROUP BY feed_id LIMIT 50) ORDER' + + ' BY (SELECT count(feed_id) AS c FROM posts ' + + 'WHERE read="false" GROUP BY feed_id ORDER BY ' + + 'c desc), UPPER(title)') + for feed in feeds: + img = os.path.join(config.app_cache_dir, + feed[2]) if feed[2] else None + posts = db.s('SELECT title FROM posts WHERE read=' + + '"false" AND feed_id=? LIMIT 3', (feed[0],)) + if len(posts) > 0: + msg = '\n'.join('* %s' % p[0] for p in posts) + self._notify(feed[1], msg, img) + if len(feeds) == 0: + if self._config_manager.show_update_notifications: + self._notify(feedindicator.__app_name__, + _('Finished updating feeds.')) + + def _clear_menu(self): + """Removes all entries from menu.""" + for child in self._menu.get_children(): + child.destroy() + + def _loading_menu(self): + """Populate a loading menu.""" + self._clear_menu() + + item = Gtk.MenuItem(label=_('Loading')) + item.set_sensitive(False) + self._menu.append(item) + self._menu.show_all() + if appindicator: + self._indicator.set_menu(self._menu) + self._set_status(False) + + def _render_menu(self): + """Populate the menu.""" + self._clear_menu() + + with SQLite() as db: + feeds = db.s('SELECT id, title, url, feed_url, (SELECT COUNT(*) ' + + 'FROM posts WHERE posts.feed_id=feeds.id AND ' + + 'read="false") AS c FROM feeds ORDER BY c DESC, ' + + 'UPPER(title)') + + if not self._config_manager.feeds_at_top: + self._conf_menu(len(feeds) > 0) + self._menu.append(Gtk.SeparatorMenuItem()) + + if len(feeds) > 0: + self._feeds_menu_header() + for feed in feeds: + posts = db.s('SELECT id, title, url FROM posts WHERE ' + + 'feed_id=? AND read="false" ORDER BY id ' + + 'LIMIT %d' % + self._config_manager.items_per_feed, + (feed[0],)) + if self._config_manager.show_unread_feeds: + self._feed_submenu(feed, posts) + else: + if feed[4] > 0: + self._feed_submenu(feed, posts) + if db.s('SELECT COUNT(*) FROM posts WHERE ' + + 'read="false"')[0][0] == 0: + menu_notice = Gtk.MenuItem(label=_('No unread posts.')) + menu_notice.set_sensitive(False) + self._menu.append(menu_notice) + else: + item = Gtk.MenuItem(label=_('No feeds defined!')) + item.set_sensitive(False) + self._menu.append(item) + + if self._config_manager.feeds_at_top: + self._menu.append(Gtk.SeparatorMenuItem()) + self._conf_menu(len(feeds) > 0) + + self._menu.show_all() + if appindicator: + self._indicator.set_menu(self._menu) + self._set_status(db.s('SELECT COUNT(*) FROM posts WHERE ' + + 'read="false"')[0][0] > 0) + + def _feeds_menu_header(self): + """Add items to menu with for all feeds.""" + item = Gtk.MenuItem(label=_('Open all unread')) + item.connect('activate', self._open_unread, '') + self._menu.append(item) + + item = Gtk.MenuItem(label=_('Mark all as read')) + item.connect('activate', self._mark_feed_as_read, '') + self._menu.append(item) + + item = Gtk.MenuItem(label=_('Mark all as unread')) + item.connect('activate', self._mark_feed_as_unread) + self._menu.append(item) + + item = Gtk.MenuItem(label=_('Reload all feeds')) + item.connect('activate', self._update, True, False) + self._menu.append(item) + self._menu.append(Gtk.SeparatorMenuItem()) + + def _feed_submenu(self, feed, posts): + """Add a feed submenu to the menu. + + Args: + feed: tuple feed info (id, title, url, feed_url, number of posts) + posts: list of posts to append with (id, title, url) for each post + """ + menu_header = Gtk.MenuItem('%s (%d)'.replace(' (0)', '') % (feed[1], + feed[4])) + + submenu = Gtk.Menu() + item = Gtk.MenuItem(_('Open Website')) + item.connect('activate', self._open_website, feed[2]) + submenu.append(item) + + item = Gtk.MenuItem(label=_('Open unread')) + item.connect('activate', self._open_unread, feed[0]) + submenu.append(item) + + if feed[4] > self._config_manager.items_per_feed: + item = Gtk.MenuItem(label=_('Open displayed posts')) + item.connect('activate', self._open_displayed, feed[0]) + submenu.append(item) + + item = Gtk.MenuItem(label=_('Mark as read')) + item.connect('activate', self._mark_feed_as_read, feed[0]) + submenu.append(item) + + if feed[4] > self._config_manager.items_per_feed: + item = Gtk.MenuItem(label=_('Mark displayed posts as read')) + item.connect('activate', self._mark_displayed_as_read, feed[0]) + submenu.append(item) + + item = Gtk.MenuItem(label=_('Mark as unread')) + item.connect('activate', self._mark_feed_as_unread, feed[0]) + submenu.append(item) + + for i, post in enumerate(posts): + if i == 0: + submenu.append(Gtk.SeparatorMenuItem()) + item = Gtk.MenuItem(post[1]) + item.connect('activate', self._open_website, post[2], post[0]) + submenu.append(item) + + menu_header.set_submenu(submenu) + self._menu.append(menu_header) + + def _conf_menu(self, has_feeds): + """Adds config items to menu. + + Args: + has_feeds: if feeds are displayed in the menu + """ + if self._config_manager.feeds_at_top: + self._menu.append(Gtk.SeparatorMenuItem()) + + item = Gtk.MenuItem(label=_('Add feed')) + item.connect('activate', AddFeedDialog, self._update, None, True, + False) + self._menu.append(item) + + if has_feeds: + item = Gtk.CheckMenuItem(label=self._timer_text()) + if self._config_manager.stoptimer: + item.set_active(False) + else: + item.set_active(True) + item.connect('toggled', self._toggle_update) + self._menu.append(item) + + menu_configure = Gtk.MenuItem(label=_('Preferences')) + menu_configure.connect('activate', PreferencesDialog, + self._config_manager, self._update, None, True, + True) + self._menu.append(menu_configure) + menu_about = Gtk.MenuItem(label=_('About')) + menu_about.connect('activate', AboutDialog) + self._menu.append(menu_about) + item = Gtk.MenuItem(label=_('Exit')) + item.connect('activate', self._exit) + self._menu.append(item) + + def _open_unread(self, widget, feed_id=None): + """Opens all unread post of the given feed or all unread posts. + + Args: + widget: Gtk widget + feed_id: optional feed id, if not given opens all unread + """ + with SQLite() as db: + if feed_id: + posts = db.s('SELECT url FROM posts WHERE feed_id=? AND ' + + 'read="false"', (feed_id,)) + for post in posts: + webbrowser.open(post[0]) + sleep(0.6) + + db.s('UPDATE posts SET read="true" WHERE feed_id=?', + (feed_id,)) + else: + for feed in db.s('SELECT id FROM feeds'): + posts = db.s('SELECT url FROM posts WHERE feed_id=? AND ' + + 'read="false"', feed) + for post in posts: + webbrowser.open(post[0]) + sleep(0.6) + db.s('UPDATE posts SET read="true" WHERE feed_id=?', feed) + sleep(0.5) + self._render_menu() + + def _open_displayed(self, widget, feed_id): + """Opens all unread post of the given feed or all unread posts. + + Args: + widget: Gtk widget + feed_id: optional feed id, if not given opens all unread + """ + with SQLite() as db: + posts = db.s('SELECT id, url FROM posts WHERE feed_id=? AND ' + + 'read="false" ORDER BY id DESC LIMIT %d' % + self._config_manager.items_per_feed, (feed_id,)) + for post in posts: + webbrowser.open(post[1]) + db.s('UPDATE posts SET read="true" WHERE id=?', (post[0],)) + sleep(0.6) + self._render_menu() + + def _mark_feed_as_read(self, widget, feed_id=None): + """Mark all posts of all feeds or a specific feed as read. + + Args: + widget: Gtk widget + feed_id: optinal feed id, if not given all will be updated + """ + with SQLite() as db: + f = 'WHERE feed_id=?' if feed_id else '' + db.s('UPDATE posts SET read="true" %s' % f, + (feed_id,) if feed_id else ()) + sleep(0.5) + self._render_menu() + + def _mark_displayed_as_read(self, widget, feed_id): + """Marks displayed posts of given feed as read. + + Args: + widget: Gtk widget + feed_id: feed id + """ + with SQLite() as db: + db.s('UPDATE posts SET read="true" WHERE feed_id=? AND id IN ' + + '(SELECT id FROM posts WHERE feed_id=? AND read="false" ' + + 'ORDER BY id DESC LIMIT %d)' % + self._config_manager.items_per_feed, (feed_id, feed_id)) + sleep(0.5) + self._render_menu() + + def _mark_feed_as_unread(self, widget, feed_id=None): + """Mark all posts of all feeds or a specific feed as read. + + Args: + widget: Gtk widget + feed_id: optinal feed id, if not given all will be updated + """ + with SQLite() as db: + f = 'WHERE feed_id=?' if feed_id else '' + db.d('UPDATE posts SET read="false" %s' % f, + (feed_id,) if feed_id else ()) + sleep(0.5) + self._render_menu() + + def _toggle_update(self, widget): + """Toggle timer updating. + + Args: + widget: Gtk widget + """ + if self._config_manager.stoptimer: + self._config_manager.stoptimer = False + if self._config_manager.show_notifications: + self._notify(feedindicator.__app_name__, self._timer_text()) + self._update(None, True, True) + else: + self._config_manager.stoptimer = True + if self._config_manager.show_notifications: + self._notify(config.app_name, + _('Feeds will not update automatically.')) + + def _timer_text(self): + """"Text for timer element.""" + minutes = self._config_manager.refreshtime / 60 + return gettext.ngettext('Update feeds every minute', + 'Update feeds every %(minutes)d minutes', + minutes) % {'minutes': minutes} diff --git a/feedindicator/locale/de_DE/LC_MESSAGES/feedindicator.mo b/feedindicator/locale/de_DE/LC_MESSAGES/feedindicator.mo new file mode 100644 index 0000000000000000000000000000000000000000..a4ad91a5538a9c2e4433bad4bc5666db8e83d134 GIT binary patch literal 3606 zcmaKu&u<$=6vqcz3YZ^-Qhv2iCO}F;W7iIprfvh|hXaMAjq;;K96)35v%SrF)}5V= zn}Xni1cD2y1PG}@NQ4`3LXnW#3qBx0B_s|#@(+Ln;s)Zt_syL4cq}*a5wk{ zcn|m?_yD*Hvi)UnFZeTvkN6E=w}F3xw}StIJpT?jmEcbBaWD_^Jd5Cspib}4f;Z#- zeef>u0=NtO0=yku1MdXeAjkI$m;wI)9|8XXS=Szj=jVq&er`17OW;o2p9VRu8pwLz z1ljL<>HUWwKYtO#M|_Phgd)BLhr#c_2f^P#_PYZQv(Eh>=l%!?DPjraJf8)h2V?M4 ztjStBK8qx=|5G6QD}(og5h%eAKzzhk_+q_l;C}EY@BsK1$m_BXNtgtOK};=LAnSP> zgo(HS@;Y1u`S~xwA@B;=2mTH6I_!lQ_P-Bg{m+0L=P{7i`4q@H40s>-E_eX^48%wL zh%cV^D#+v8kbGX}J>VVSQy|AVmiEu3Tmo6|J0R=)1Z4ZqL5}YmkmLA1jYh-`yo9E@V@4a}B z@x{63{rDIfQs{C4h&}wZ>Eb+dPB?#qXgM^%bDd%3;K9b@G%wqnflxXCIJe#ADEann&%|NX{f|I{K7Y8)U0p-Mra6OZ< z8phfiO47)3ee1@ZYUxqc*W~r_6N!&xQZ})5HrLe3iIYvz@-&qr-%@hJL8PMG;3iRt z(Yk^4Lmi1(Lsc?_4HAht3Xl_*3#SAfjc}R7yyCBV*pdY(MGBi_k{#$e`v^aV zvB1z|W=}3;C-_ugEQ@nOMLC==9L^T<+5FRTc({1z(1HALJ`cm}94-4i$N$<*tH@>- zjM7oS4vI1}t#mC`HOelKYV_O}v14AJEM;bn&y>3I3b}j+C*G3AG%HQ$v2@NJ#4h#q zQCX=g6OkQVT$sv^bnVz3wnMh0DV{oy?OmmHI5)Wb<&85$%g*GU7#FpO35UOEuC-mBBGt?{H;OE!kM1_W0QBX|bH= zLO^n?I}&3bdDz=iNdX%xmkm|w>70>VN((5@q0@4PKIB+xzf!l-d94u(v9d+RW3x%0 zByR{^R(gr&=-Oi|Sqg$4+jYCn5XxvIQ8Fc=uuoAvfV@_FwGIWI(|SGmQpMF8Swrxg z!;1m-8O?Nh#Z0n_Zk~20Dc~;*#4SIT5@aA9l;7-JGSKmT)(%lWe?~-Pks%F zO9I~7XEz}^@iJDYca@P`6nN4@5*tpoe1qDQu64)Q?J>C=nmQ^M>ArN1bx|GXsQSFO zA{70Y@EYNYPbj#uy;iO2PF8v!>kPC%W`mw^I&Y%IPT<{1PByu4VYZU5-JawL(~Kud z;XZJyP|_A9f^FxU1Zns!UPZddZBaaLjjG9}3EeSDhq~_wXV6}S`6iRZjnXp2iG~Iv z=lO7u^c!-9bo){|Rbtmb30mT;, +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +from feedindicator.threads.feeds import FeedThread + + +__all = ('FeedThread',) diff --git a/feedindicator/threads/feeds.py b/feedindicator/threads/feeds.py new file mode 100644 index 0000000..06df2a0 --- /dev/null +++ b/feedindicator/threads/feeds.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +from feedindicator import feeds +from gi.repository import GLib +from threading import Thread + + +class FeedThread(Thread): + """Feed thread, to run feed updates in a separate thread. + + Attributes: + _callback: callback function after url has been added + _callback_args: arguments for callback function + """ + def __init__(self, callback, *args): + """Init feed thread. + + Args: + callback: callback function after url has been added + args: arguments for callback function + """ + Thread.__init__(self, name='FeedThread') + self._callback = callback + self._callback_args = args + + def run(self): + """Updates all feeds in the database und updates the infos""" + feeds.update() + GLib.idle_add(self._callback, self._callback_args) diff --git a/feedindicator/utils/__init__.py b/feedindicator/utils/__init__.py new file mode 100644 index 0000000..66d30e0 --- /dev/null +++ b/feedindicator/utils/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + +from feedindicator.utils.core import get_hash +from feedindicator.utils.db import init as db_init +from feedindicator.utils.sqlite import SQLite +from feedindicator.utils.version import app_version + + +__all__ = ('app_version', 'db_init', 'get_hash', 'SQLite') diff --git a/feedindicator/utils/autostart.py b/feedindicator/utils/autostart.py new file mode 100644 index 0000000..ccdab72 --- /dev/null +++ b/feedindicator/utils/autostart.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import feedindicator +import os + +from feedindicator import config + + +def create(): + """Create autostart file.""" + if not os.path.exists(config.app_autostart_dir): + os.makedirs(config.app_autostart_dir, 0o700) + with open(config.app_autostart_file, 'w', encoding='utf-8') as f: + f.write('[Desktop Entry]\n') + f.write('Type=Application\n') + f.write('Exec=%s --autostarted\n' % feedindicator.__app_name__) + f.write('X-GNOME-Autostart-enabled=true\n') + f.write('Icon=%s\n' % feedindicator.__app_name__) + f.write('Name=%s\n' % feedindicator.__app_name__) + f.write('Comment=%s' % feedindicator.__description__) + + +def delete(): + """Delete autostart file.""" + if os.path.exists(config.app_autostart_file): + os.remove(config.app_autostart_file) diff --git a/feedindicator/utils/core.py b/feedindicator/utils/core.py new file mode 100644 index 0000000..9a5b6b0 --- /dev/null +++ b/feedindicator/utils/core.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import hashlib + + +def get_hash(*args): + """Calculate hash for combined string of arguments. + + Args: + args: values to hash + """ + return hashlib.sha512('@'.join(a for a in args if a). \ + encode('utf-8')).hexdigest() diff --git a/feedindicator/utils/db.py b/feedindicator/utils/db.py new file mode 100644 index 0000000..1bd22c2 --- /dev/null +++ b/feedindicator/utils/db.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import os + +from feedindicator import config +from feedindicator.utils.sqlite import SQLite + + +def init(): + """Init SQLite database, creates tables.""" + if not os.path.exists(config.app_database): + print(_('No database found, creating it.')) + + with SQLite() as db: + db.s('CREATE TABLE feeds (id integer NOT NULL PRIMARY KEY ' + + 'AUTOINCREMENT, created_at datetime NOT NULL DEFAULT ' + + 'CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT ' + + 'CURRENT_TIMESTAMP, feed_url TEXT NOT NULL UNIQUE, url ' + + 'TEXT, title TEXT, img TEXT)') + db.s('CREATE TRIGGER feeds_updated_at_trigger AFTER UPDATE ON ' + + 'feeds FOR EACH ROW BEGIN UPDATE feeds SET ' + + 'updated_at=CURRENT_TIMESTAMP WHERE id=old.id; END') + db.s('CREATE TABLE posts (id integer NOT NULL PRIMARY KEY ' + + 'AUTOINCREMENT, created_at datetime NOT NULL DEFAULT ' + + 'CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT ' + + 'CURRENT_TIMESTAMP, hash TEXT NOT NULL UNIQUE, url TEXT, ' + + 'title TEXT, raw TEXT, read BOOLEAN DEFAULT false, feed_id ' + + 'integer NOT NULL, FOREIGN KEY(feed_id) REFERENCES ' + + 'feeds(id))') + db.s('CREATE INDEX posts_feed_id_idx ON posts(feed_id)') + db.s('CREATE TRIGGER posts_updated_at_trigger AFTER UPDATE ON ' + + 'posts FOR EACH ROW BEGIN UPDATE posts SET ' + + 'updated_at=CURRENT_TIMESTAMP WHERE id=old.id; END') + print(_('Database created.')) diff --git a/feedindicator/utils/sqlite.py b/feedindicator/utils/sqlite.py new file mode 100644 index 0000000..b4a43a8 --- /dev/null +++ b/feedindicator/utils/sqlite.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import sqlite3 + +from feedindicator import config + + +class SQLite: + """SQlite database wrapper. + + Attributes: + _con: database connection + """ + def __init__(self): + self._con = None + + def get_con(self): + """Get database connection.""" + return self._con + + con = property(get_con) + + def __enter__(self): + self.open(config.app_database) + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def open(self, path): + """Opens a new connection. + + Args: + path: path to sqlite database file + """ + self._con = sqlite3.connect(path) + + def close(self): + """Closes the current connection.""" + if self._con: + self._con.close() + + def s(self, query, data=(), auto_commit=True): + """Executes the given query and returns the results. + + Args: + query: SQL query + data: optinal additional data + auto_commit: if true commits after query execution + """ + if not self._con: + self.open(config.app_database) + cur = self._con.cursor() + if data == (): + cur.execute(query) + else: + cur.execute(query, data) + rows = cur.fetchall() + if auto_commit: + self.commit() + return rows + + def many(self, query, data, auto_commit=True): + """Executes the given query with executemany() function. + + Args: + query: SQL query + data: additional data + auto_commit: if true commits after query execution + """ + if not self._con: + self.open(config.app_database) + cur = self._con.cursor() + cur.executemany(query, data) + if auto_commit: + self.commit() + + def commit(self): + """Commit changes to the database. When an error occurs a roll back + is done. + """ + if not self._con: + self.open(config.app_database) + try: + self._con.commit() + except sqlite3.Error as e: + self._con.rollback() + raise e diff --git a/feedindicator/utils/version.py b/feedindicator/utils/version.py new file mode 100644 index 0000000..a998320 --- /dev/null +++ b/feedindicator/utils/version.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 Dave Gardner , +# Michael Judge , +# Nicolas Raoul , +# Nathanael Philipp (jnphilipp) +# +# This file is part of feedindicator. +# +# Foobar is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# feedindicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with feedindicator. If not, see . + + +import feedindicator + +from math import ceil, floor + + +def app_version(): + """Generate command line version text.""" + def line(text): + """Make single line with padding left and right.""" + return '#%s%s%s#\n' % (' ' * floor((47 - len(text)) / 2), + text, + ' ' * ceil((47 - len(text)) / 2)) + + return (('#################################################\n' + + line(feedindicator.__app_name__) + + line(feedindicator.__description__) + + line('') + + line(feedindicator.__version__) + + line(feedindicator.__license__) + + line(feedindicator.__author__) + + line(feedindicator.__email__) + + line('') + + line(feedindicator.__github__) + + '#################################################')) diff --git a/hicolor/indicator-feedindicator-attention.png b/hicolor/indicator-feedindicator-attention.png deleted file mode 100644 index 2cee60d7a4afce2f31271b010feef10d51537eda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 654 zcmV;90&)F`P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2ipP? z1uX-M6+?yq00It4L_t(I%k7juXcJ)=#(&r5(o5peO0X1bprS)!Kud)q9lSQE2n7d= zRBQ*eh>KkWQ51&`A|iq!f~b>&LxV;^2pW)v36%b#l;QQCN4L}f30rwgiKY=(f+U#l1E=}52YzkuNeQ04n zse}(hmn$MAS!e6+BuZ}!k__;CX{m0-RuwJmCmBtjBfgOP(2=yYF*N;ix?j!SR1i{w8hMu!H0!OQY zZB`3Tb+Gtg=9wS8$6Vw(xydma2X5lpKSb_he05u@DV}?F6=OUC2)Op0rs>38a+70Z zv=D+QAs-s9Xo)2_qlM5PT_Sy_AM-~Fv8@B=p5x3%ZjgtpzN}I)IUQ z%EIh4*@-9sg)i@lDmQy6l)$#u9m{$uh0qn^5B8FbDK$lzuUkVs_OjX2e$nc8i=*_m ol&|U9L+E`e;r@SYu2;GK0L>}0iviR;kN^Mx07*qoM6N<$f>JCSK>z>% diff --git a/hicolor/indicator-feedindicator.png b/hicolor/indicator-feedindicator.png deleted file mode 100644 index 4d5185da8a7a247abdffbb6f4bc6140fda41e99b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 693 zcmV;m0!safP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2ipP? z1uQDiyy~j}00K2hL_t(I%k7k3NRv?*ho4VAC&e}@LA)6$8;do{LALBdWk{5NV68-x z21O&JVi#d07*s@blOQ1!qYQ-A5TY1Jb1t>HO3hXl4J#UC7T8dNZ8wo~U9?4t-*gw< z`MW#kJ@0eg^PY2lu%?uRe+QB2i9EmrQ~>#_WXu7>z)*q8@H0IrE?H!X#bXUlms>wF z7Su(faYZJgQYnw^-nC=W+GH+N$Q1y6=~&K^wG~d6TkrMxjVnO`qS3g*>+>6(F1J2) zN~FN~BV$1wz#Ydm+RmM#;Xo~h?OPE9L2`ps&Z5?d%T)oeY2yZrr3Q?p1^_~nAL+c& z&CK@*QaKsHX&q}mi$-09TD=)lc^P`0mh0^oak?JT>lq-KOlE9Tj#Qv_p9xVE>G%1u zpSi@NzCmQNTx>@hsjIG9@u^5we9%$@z_0mvdhYkq)!oCZiFcfDwPS6vFdlrnY+F{W z*m3(V{=g`LAYeVTpUW3oc>N}X$2&w`o(%iRBN;DL671=JLPuv8Ev;9W{x*YNr^RBf zru*IlB9U42IxQvJblHPhZZew8~L3@XT`9y-o`a0z6*D(|r1wgAQ&K}%sJ4|JH z8TFPLroMdT+4FI7MUgF94dL(<02)ov>cL8-JO=Q1Fu?5G51x*^1mOLLFaYXe6`#VN z|A5s+i*hoV(AAr_oK;?*-2<5K!M88CiUEgtxe{_^f%4IghDBY`~SAN broZ|P!aB#&T}GX700000NkvXXu0mjfx3oC` diff --git a/icons/active.png b/icons/active.png new file mode 100644 index 0000000000000000000000000000000000000000..674576135f99e804a96f928a263b8e57f85991f6 GIT binary patch literal 6816 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 literal 0 HcmV?d00001 diff --git a/icons/attention.png b/icons/attention.png new file mode 100644 index 0000000000000000000000000000000000000000..e1849cb0d8fea170bd09eb9cff2bda64dd229d1d GIT binary patch literal 6781 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 diff --git a/light/indicator-feedindicator.png b/light/indicator-feedindicator.png deleted file mode 100644 index 4d5185da8a7a247abdffbb6f4bc6140fda41e99b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 693 zcmV;m0!safP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2ipP? z1uQDiyy~j}00K2hL_t(I%k7k3NRv?*ho4VAC&e}@LA)6$8;do{LALBdWk{5NV68-x z21O&JVi#d07*s@blOQ1!qYQ-A5TY1Jb1t>HO3hXl4J#UC7T8dNZ8wo~U9?4t-*gw< z`MW#kJ@0eg^PY2lu%?uRe+QB2i9EmrQ~>#_WXu7>z)*q8@H0IrE?H!X#bXUlms>wF z7Su(faYZJgQYnw^-nC=W+GH+N$Q1y6=~&K^wG~d6TkrMxjVnO`qS3g*>+>6(F1J2) zN~FN~BV$1wz#Ydm+RmM#;Xo~h?OPE9L2`ps&Z5?d%T)oeY2yZrr3Q?p1^_~nAL+c& z&CK@*QaKsHX&q}mi$-09TD=)lc^P`0mh0^oak?JT>lq-KOlE9Tj#Qv_p9xVE>G%1u zpSi@NzCmQNTx>@hsjIG9@u^5we9%$@z_0mvdhYkq)!oCZiFcfDwPS6vFdlrnY+F{W z*m3(V{=g`LAYeVTpUW3oc>N}X$2&w`o(%iRBN;DL671=JLPuv8Ev;9W{x*YNr^RBf zru*IlB9U42IxQvJblHPhZZew8~L3@XT`9y-o`a0z6*D(|r1wgAQ&K}%sJ4|JH z8TFPLroMdT+4FI7MUgF94dL(<02)ov>cL8-JO=Q1Fu?5G51x*^1mOLLFaYXe6`#VN z|A5s+i*hoV(AAr_oK;?*-2<5K!M88CiUEgtxe{_^f%4IghDBY`~SAN broZ|P!aB#&T}GX700000NkvXXu0mjfx3oC`