diff --git a/README.md b/README.md index 5921838..49fc115 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,6 @@ The Linux Sina Weibo Client This project is under heavy development. -**特别说明:在使用 l10n 框架之前,严禁任何人以任何理由翻译界面!** - -**WARNING: Nobody is allowed to translate the UI until we use l10n framework!!** - IRC Channel ------ \#wecase @ freenode diff --git a/ROADMAP b/ROADMAP index 0419ee4..1387fef 100644 --- a/ROADMAP +++ b/ROADMAP @@ -1 +1,3 @@ -复制粘贴支持 +词语过滤支持,使用一个代理 Model 实现 +Context Menu 支持 +仅仅在双击后进入 TextEdit 状态,平时处于 Text,提高性能 diff --git a/locale/WeCase_zh_CN.qm b/locale/WeCase_zh_CN.qm index 31fa38c..669a960 100644 Binary files a/locale/WeCase_zh_CN.qm and b/locale/WeCase_zh_CN.qm differ diff --git a/locale/WeCase_zh_CN.ts b/locale/WeCase_zh_CN.ts index 75a52cd..ea311c2 100644 --- a/locale/WeCase_zh_CN.ts +++ b/locale/WeCase_zh_CN.ts @@ -3,9 +3,9 @@ @default - + WeCase - 微盒 + 微盒 @@ -87,25 +87,30 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY. LoginWindow - + Authorize Failed! 认证失败! - + Check your account and password! - 检查您的账号和密码! + 检查您的账号和密码! - + GO! 走起! - + Login, waiting... 登录中,请稍候…… + + + Check your account, password and Internet Connection! + 检查您的账号,密码和网络连接! + NewPostWindow @@ -115,22 +120,22 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY. 新消息 - + 140 140 - + &Picture 图片(&P) - + &Cancel 取消(&C) - + &Send 发送(&S) @@ -140,35 +145,55 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY. 表情 - + S&miley 表情(&M) + + + Also: + 同时: + + + + Comment + 评论 + + + + Repost + 转发 + + + + Commmet to Original + 评论给原微博 + NewpostWindow - + WeCase 微盒 - + Retweet Success! 转发成功! - + Comment Success! 评论成功! - + Reply Success! 回复成功! - + Tweet Success! 发送成功! @@ -178,37 +203,37 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY. 图片 (*.png *.jpg *.bmp *.gif) - + Picture 图片 - + Choose a image 选择一张图片 - + Remove the picture 移除图片 - + Text too long! 内容过长! - + Please remove some text. 请删除一些文字。 - + Unknown error! 未知错误! - + Images 图片 @@ -256,30 +281,38 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY. 评论 + + SmileyWindow + + + Choose a smiley + 选择一个表情 + + TweetItem - + Time travel! 穿越时空! - + %.0f seconds ago %.0f 秒前 - + %.0f minutes ago %.0f 分钟前 - + %.0f hours ago %.0f 小时前 - + %.0f days ago %.0f 天前 @@ -287,52 +320,72 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY. WeCaseWindow - + Weibo(%d) 微博(%d) - + @Me(%d) @我(%d) - + Comments(%d) 评论(%d) - + WeCase 微盒 - + Weibo 微博 - + @ME - @我 + @我 - + Comments 评论 + + + You have: + 您有: + + + + %d unread @ME + %d 条未读 @提醒 + + + + %d unread comment(s) + %d 条未读评论 + + + + @Me + @我 + WeSettingsWindow - + %i min %i sec - %i 分 %i 秒 + %i 分 %i 秒 - + %i sec - %i 秒 + %i 秒 @@ -391,72 +444,72 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY. 微博 - + @ME @我 - + Comments 评论 - + My tweet 我的微博 - + &Me 我(&M) - + &Refresh 刷新(&R) - + &New Weibo 新微博(&N) - + &WeCase 微盒(&W) - + &Help 帮助(&H) - + &Options 选项(&H) - + &About... 关于(&A)... - + &Log out 注销(&L) - + &Exit 退出(&E) - + &Settings 设置(&S) - + &Update 升级(&U) diff --git a/res/img/smiley/L6/clock_thumb b/res/img/smiley/L6/clock_thumb index 9c5dd46..92f0862 100644 --- a/res/img/smiley/L6/clock_thumb +++ b/res/img/smiley/L6/clock_thumb @@ -1 +1 @@ -种 +钟 diff --git a/res/ui/MainWindow.ui b/res/ui/MainWindow.ui index dbbcb0b..e595a9b 100644 --- a/res/ui/MainWindow.ui +++ b/res/ui/MainWindow.ui @@ -30,7 +30,14 @@ - + + + QFrame::StyledPanel + + + QFrame::Sunken + + @@ -40,7 +47,14 @@ - + + + QFrame::StyledPanel + + + QFrame::Sunken + + @@ -50,7 +64,14 @@ - + + + QFrame::StyledPanel + + + QFrame::Sunken + + @@ -60,7 +81,14 @@ - + + + QFrame::StyledPanel + + + QFrame::Sunken + + diff --git a/res/ui/NewpostWindow.ui b/res/ui/NewpostWindow.ui index 2b89a44..3a2fd5a 100644 --- a/res/ui/NewpostWindow.ui +++ b/res/ui/NewpostWindow.ui @@ -7,7 +7,7 @@ 0 0 562 - 292 + 306 @@ -19,129 +19,160 @@ false - - - - - - - - 0 - 200 - - - - - - - - Arial Black - 14 - - - - Qt::LeftToRight - - - 140 - - - Qt::PlainText - - - Qt::AlignCenter - - - - - - - true - - - QFrame::Sunken - - - - - - + + + + + + 0 + 200 + + + + + + + + Arial Black + 14 + + + + Qt::LeftToRight + + + 140 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + true + + + QFrame::Sunken + + + + + + + + + + QLayout::SetFixedSize + + + 0 + - + - Qt::Vertical - - - QSizePolicy::Fixed + Qt::Horizontal - 20 + 40 20 - - - - 0 - 40 - + + + Also: + + + + + + + Comment + + + + + + + Repost + + + + + + + Commmet to Original - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - &Picture - - - - - - - &Cancel - - - - - - - &Send - - - - - - - S&miley - - - - - pushButton_picture - pushButton_cancel - pushButton_send - horizontalSpacer - pushButton + + + + + 0 + 40 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Picture + + + + + + + &Cancel + + + + + + + &Send + + + + + + + S&miley + + + + + pushButton_picture + pushButton_cancel + pushButton_send + horizontalSpacer + pushButton + + diff --git a/res/ui/TweetDelegate.qml b/res/ui/TweetDelegate.qml index 882f8ed..bb34328 100644 --- a/res/ui/TweetDelegate.qml +++ b/res/ui/TweetDelegate.qml @@ -1,4 +1,4 @@ -import QtQuick 1.0 +import QtQuick 1.1 Item { id: container @@ -27,7 +27,11 @@ Item { signal mentionLinkClicked(string screenname) width: ListView.view.width; - height: { + height: getHeight(); + + /*Component.onCompleted: height = getHeight()*/ + + function getHeight() { var tweetImageHeight = tweetImage.paintedHeight if (statusText.paintedHeight < 80) { return 90 + tweetImageHeight; @@ -72,6 +76,19 @@ Item { } } + function get_thumbnail_pic() { + if (thumbnail_pic) { + return thumbnail_pic + } + if (original && original.thumbnail_pic) { + return original.thumbnail_pic + } + else { + return "" + } + } + + Rectangle { id: avatarBackground width: 60 @@ -104,7 +121,7 @@ Item { ButtonImage { id: comment - visible: tweetType != "comment"; + visible: tweetType != 2; buttonImageUrl: "img/small_comment_button.png" pressedButtonImageUrl: "img/small_comment_button_pressed.png" @@ -118,7 +135,7 @@ Item { ButtonImage { id: retweet - visible: tweetType != "comment"; + visible: tweetType != 2; buttonImageUrl: "img/small_retweet_button.png" pressedButtonImageUrl: "img/small_retweet_button_pressed.png" @@ -134,7 +151,7 @@ Item { ButtonImage { id: reply - visible: tweetType == "comment"; + visible: tweetType == 2; buttonImageUrl: "img/small_reply_button.png" pressedButtonImageUrl: "img/small_reply_button_pressed.png" @@ -148,7 +165,7 @@ Item { ButtonImage { id: favorite - visible: tweetType != "comment"; + visible: tweetType != 2; buttonImageUrl: { if (isFavorite) { @@ -178,44 +195,47 @@ Item { } Text { + //TextEdit { id: statusText color: "#333333" text: { - if (tweetType == "tweet" || tweetType == "comment") { + if (tweetType == 0) { return '' + tweetScreenName + ':<\/b>
' + addTags(tweetText) } - else if (tweetType == "retweet") { + else if (tweetType == 1 || tweetType == 2) { return '' + tweetScreenName + ':<\/b>
' + addTags(tweetText) + '
' + '    ' + tweetOriginalName + '<\/b>: ' + addTags(tweetOriginalText) } } - anchors.topMargin: 4 - anchors.top: parent.top; + y: 4; // anchors.topMargin: 4; anchors.top: parent.top; + // Binding loop detected for property "height", do not use it anchors.right: retweet.left; anchors.rightMargin: 0 anchors.left: avatarBackground.right; anchors.leftMargin: 10 textFormat: Text.RichText wrapMode: "Wrap" font.family: "Segoe UI" font.pointSize: 9 + //selectByMouse: true + //readOnly: true onLinkActivated: container.handleLink(link); } Image { id: tweetImage - visible: thumbnail_pic + visible: get_thumbnail_pic() anchors.top: statusText.bottom - anchors.topMargin: thumbnail_pic ? 10 : 0; + anchors.topMargin: get_thumbnail_pic() ? 10 : 0; anchors.horizontalCenter: parent.horizontalCenter - source: thumbnail_pic + source: get_thumbnail_pic() MouseArea { anchors.fill: parent onClicked: { busy.on = true; - mainWindow.look_orignal_pic(thumbnail_pic, tweetid); + mainWindow.look_orignal_pic(get_thumbnail_pic(), tweetid); } } @@ -229,12 +249,24 @@ Item { Text { id: sinceText - text: tweetSinceTime + text: { + if (tweetType != 2) { + return "" + tweetSinceTime + "" + } + else if (tweetType == 2) { + return "" + tweetSinceTime + "" + } + else { + return tweetSinceTime + } + } anchors.top: avatarBackground.bottom anchors.leftMargin: 11 anchors.topMargin: 5 anchors.left: parent.left font.family: "Segoe UI" font.pointSize: 7 + + onLinkActivated: container.handleLink(link); } } diff --git a/res/ui/TweetList.qml b/res/ui/TweetList.qml index 0718520..28f49e0 100644 --- a/res/ui/TweetList.qml +++ b/res/ui/TweetList.qml @@ -30,11 +30,12 @@ Rectangle { id: tweetListView anchors.fill: parent; clip: true + cacheBuffer: 500 model: mymodel delegate: TweetDelegate { function retweet_string() { - if (tweetType == "retweet") { + if (tweetType == 1) { return "//@" + tweetScreenName + ":" + tweetText; } else { @@ -56,12 +57,14 @@ Rectangle { } tweetType: type - tweetScreenName: author - tweetOriginalId: original_id - tweetOriginalName: original_author - tweetOriginalText: original_content - tweetText: content - tweetAvatar: avatar + tweetScreenName: author.name + tweetOriginalId: type != 0 && original.id + + // 不是单条微博,有作者信息(原微博不被删),返回作者姓名;否则返回空字符串 + tweetOriginalName: (type != 0 && original.author && original.author.name) || "" + tweetOriginalText: type != 0 && original.text + tweetText: text + tweetAvatar: author.avatar tweetid: id isOwnTweet: true isNewTweet: true @@ -71,7 +74,7 @@ Rectangle { onFavoriteButtonClicked: favorite() onRetweetButtonClicked: mainWindow.repost(tweetid, retweet_string()) onCommentButtonClicked: mainWindow.comment(tweetid) - onReplyButtonClicked: mainWindow.reply(original_id, tweetid) + onReplyButtonClicked: mainWindow.reply(original.id, tweetid) // not implemented onMoreButtonClicked: console.log("Clicked a user: " + tweetScreenName) @@ -91,7 +94,7 @@ Rectangle { id: scrollbar anchors.right: tweetListView.right y: tweetListView.visibleArea.yPosition * tweetListView.height - width: 2 + width: 8 height: tweetListView.visibleArea.heightRatio * tweetListView.height color: "black" } diff --git a/src/AboutWindow.py b/src/AboutWindow.py new file mode 100644 index 0000000..fda94db --- /dev/null +++ b/src/AboutWindow.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +# WeCase -- This file implemented AboutWindow. +# Copyright (C) 2013 Tom Li +# License: GPL v3 or later. + + +from PyQt4 import QtGui +from AboutWindow_ui import Ui_About_Dialog + + +class AboutWindow(QtGui.QDialog, Ui_About_Dialog): + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.setupUi(self) diff --git a/src/AboutWindow_ui.py b/src/AboutWindow_ui.py index e482018..fc1c493 100644 --- a/src/AboutWindow_ui.py +++ b/src/AboutWindow_ui.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'ui/AboutWindow.ui' +# Form implementation generated from reading ui file './ui/AboutWindow.ui' # -# Created: Sat Apr 13 20:51:20 2013 +# Created: Sun Apr 14 21:55:20 2013 # by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! diff --git a/src/LoginWindow.py b/src/LoginWindow.py new file mode 100644 index 0000000..05b609e --- /dev/null +++ b/src/LoginWindow.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +# WeCase -- This file implemented LoginWindow. +# Copyright (C) 2013 Tom Li +# License: GPL v3 or later. + + +import webbrowser +import urllib.request +import urllib.parse +import urllib.error +import http.client +import ssl +import socket +from configparser import ConfigParser +import threading +from weibo import APIClient +from PyQt4 import QtCore, QtGui +from LoginWindow_ui import Ui_frm_Login +from WeCaseWindow import WeCaseWindow +import const + + +class LoginWindow(QtGui.QDialog, Ui_frm_Login): + loginReturn = QtCore.pyqtSignal(object) + + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.loadConfig() + self.setupUi(self) + self.setupMyUi() + self.setupSignals() + + def setupSignals(self): + # Other singals defined in Desinger. + self.loginReturn.connect(self.checkLogin) + self.chk_Remember.clicked.connect(self.uncheckAutoLogin) + + def accept(self, client): + if self.chk_Remember.isChecked(): + self.passwd[str(self.username)] = str(self.password) + self.last_login = str(self.username) + # Because this is a model dislog, + # closeEvent won't emit when we accept() the window, but will + # emit when we reject() the window. + self.saveConfig() + wecase_main = WeCaseWindow(client) + wecase_main.show() + # Maybe users will logout, so reset the status + self.pushButton_log.setText(self.tr("GO!")) + self.pushButton_log.setEnabled(True) + self.done(True) + + def reject(self): + QtGui.QMessageBox.critical(None, self.tr("Authorize Failed!"), + self.tr("Check your account, " + "password and Internet Connection!") + ) + self.pushButton_log.setText(self.tr("GO!")) + self.pushButton_log.setEnabled(True) + + def checkLogin(self, client): + if client: + self.accept(client) + else: + self.reject() + + def setupMyUi(self): + self.show() + self.txt_Password.setEchoMode(QtGui.QLineEdit.Password) + self.cmb_Users.addItem(self.last_login) + + for username in list(self.passwd.keys()): + if username == self.last_login: + continue + self.cmb_Users.addItem(username) + + if self.cmb_Users.currentText(): + self.chk_Remember.setChecked(True) + self.setPassword(self.cmb_Users.currentText()) + + if self.auto_login: + self.chk_AutoLogin.setChecked(self.auto_login) + self.login() + + def loadConfig(self): + self.config = ConfigParser() + self.config.read(const.config_path) + + if not self.config.has_section('login'): + self.config['login'] = {} + + self.login_config = self.config['login'] + self.passwd = eval(self.login_config.get('passwd', "{}")) + self.last_login = str(self.login_config.get('last_login', "")) + self.auto_login = self.login_config.getboolean('auto_login', 0) + + def saveConfig(self): + self.login_config['passwd'] = str(self.passwd) + self.login_config['last_login'] = self.last_login + self.login_config['auto_login'] = str(self.chk_AutoLogin.isChecked()) + + with open(const.config_path, "w+") as config_file: + self.config.write(config_file) + + def login(self): + self.pushButton_log.setText(self.tr("Login, waiting...")) + self.pushButton_log.setEnabled(False) + self.ui_authorize() + + def ui_authorize(self): + self.username = self.cmb_Users.currentText() + self.password = self.txt_Password.text() + threading.Thread(group=None, target=self.authorize, + args=(self.username, self.password)).start() + + def authorize(self, username, password): + # TODO: This method is very messy, maybe do some cleanup? + + client = APIClient(app_key=const.APP_KEY, app_secret=const.APP_SECRET, + redirect_uri=const.CALLBACK_URL) + + # Step 1: Get the authorize url from Sina + authorize_url = client.get_authorize_url() + + # Step 2: Send the authorize info to Sina and get the authorize_code + # TODO: Rewrite them with urllib/urllib2 + oauth2 = const.OAUTH2_PARAMETER + oauth2['userId'] = username + oauth2['passwd'] = password + postdata = urllib.parse.urlencode(oauth2) + + conn = http.client.HTTPSConnection('api.weibo.com') + sock = socket.create_connection((conn.host, conn.port), conn.timeout, conn.source_address) + conn.sock = ssl.wrap_socket(sock, conn.key_file, conn.cert_file, ssl_version=ssl.PROTOCOL_TLSv1) + + try: + conn.request('POST', '/oauth2/authorize', postdata, + {'Referer': authorize_url, + 'Content-Type': 'application/x-www-form-urlencoded'}) + except OSError: + self.loginReturn.emit(None) + return + + res = conn.getresponse() + + location = res.getheader('location') + + if not location: + return self.loginReturn.emit(None) + + authorize_code = location.split('=')[1] + conn.close() + + # Step 3: Put the authorize information into SDK + r = client.request_access_token(authorize_code) + access_token = r.access_token + expires_in = r.expires_in + + client.set_access_token(access_token, expires_in) + self.loginReturn.emit(client) + + def setPassword(self, username): + if username: + self.txt_Password.setText(self.passwd[str(username)]) + + @QtCore.pyqtSlot(bool) + def uncheckAutoLogin(self, checked): + if not checked: + self.chk_AutoLogin.setChecked(False) + + def openRegisterPage(self): + webbrowser.open("http://weibo.com/signup/signup.php") + + def closeEvent(self, event): + # HACK: When a user want to close this window, closeEvent will emit. + # But if we don't have closeEvent, Qt will call reject(). We use + # reject() to show the error message, so users will see the error and + # they can not close this window. So just do nothing there to allow + # users to close the window. + pass diff --git a/src/LoginWindow_ui.py b/src/LoginWindow_ui.py index 039c5bf..4e630b2 100644 --- a/src/LoginWindow_ui.py +++ b/src/LoginWindow_ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file './ui/LoginWindow.ui' # -# Created: Mon Apr 8 16:40:03 2013 +# Created: Sun Apr 14 21:55:20 2013 # by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! diff --git a/src/MainWindow_ui.py b/src/MainWindow_ui.py index 435c52c..4a595b8 100644 --- a/src/MainWindow_ui.py +++ b/src/MainWindow_ui.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './ui/MainWindow.ui' +# Form implementation generated from reading ui file 'ui/MainWindow.ui' # -# Created: Mon Apr 8 16:40:03 2013 -# by: PyQt4 UI code generator 4.10 +# Created: Sat Apr 27 15:00:44 2013 +# by: PyQt4 UI code generator 4.10.1 # # WARNING! All changes made in this file will be lost! @@ -41,6 +41,8 @@ def setupUi(self, frm_MainWindow): self.verticalLayout_2 = QtGui.QVBoxLayout(self.tab) self.verticalLayout_2.setObjectName(_fromUtf8("verticalLayout_2")) self.homeView = QtDeclarative.QDeclarativeView(self.tab) + self.homeView.setFrameShape(QtGui.QFrame.StyledPanel) + self.homeView.setFrameShadow(QtGui.QFrame.Sunken) self.homeView.setObjectName(_fromUtf8("homeView")) self.verticalLayout_2.addWidget(self.homeView) self.tabWidget.addTab(self.tab, _fromUtf8("")) @@ -49,6 +51,8 @@ def setupUi(self, frm_MainWindow): self.verticalLayout_3 = QtGui.QVBoxLayout(self.tab_2) self.verticalLayout_3.setObjectName(_fromUtf8("verticalLayout_3")) self.mentionsView = QtDeclarative.QDeclarativeView(self.tab_2) + self.mentionsView.setFrameShape(QtGui.QFrame.StyledPanel) + self.mentionsView.setFrameShadow(QtGui.QFrame.Sunken) self.mentionsView.setObjectName(_fromUtf8("mentionsView")) self.verticalLayout_3.addWidget(self.mentionsView) self.tabWidget.addTab(self.tab_2, _fromUtf8("")) @@ -57,6 +61,8 @@ def setupUi(self, frm_MainWindow): self.verticalLayout_4 = QtGui.QVBoxLayout(self.tab_3) self.verticalLayout_4.setObjectName(_fromUtf8("verticalLayout_4")) self.commentsView = QtDeclarative.QDeclarativeView(self.tab_3) + self.commentsView.setFrameShape(QtGui.QFrame.StyledPanel) + self.commentsView.setFrameShadow(QtGui.QFrame.Sunken) self.commentsView.setObjectName(_fromUtf8("commentsView")) self.verticalLayout_4.addWidget(self.commentsView) self.tabWidget.addTab(self.tab_3, _fromUtf8("")) @@ -65,6 +71,8 @@ def setupUi(self, frm_MainWindow): self.verticalLayout_5 = QtGui.QVBoxLayout(self.tab_4) self.verticalLayout_5.setObjectName(_fromUtf8("verticalLayout_5")) self.myView = QtDeclarative.QDeclarativeView(self.tab_4) + self.myView.setFrameShape(QtGui.QFrame.StyledPanel) + self.myView.setFrameShadow(QtGui.QFrame.Sunken) self.myView.setObjectName(_fromUtf8("myView")) self.verticalLayout_5.addWidget(self.myView) self.tabWidget.addTab(self.tab_4, _fromUtf8("")) diff --git a/src/NewpostWindow.py b/src/NewpostWindow.py new file mode 100644 index 0000000..d886a0e --- /dev/null +++ b/src/NewpostWindow.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +# WeCase -- This file implemented NewpostWindow. +# Copyright (C) 2013 Tom Li +# License: GPL v3 or later. + + +import re +import threading +from PyQt4 import QtCore, QtGui +from weibo import APIError +from Notify import Notify +from TweetUtils import tweetLength +from NewpostWindow_ui import Ui_NewPostWindow +from SmileyWindow import SmileyWindow + + +class NewpostWindow(QtGui.QDialog, Ui_NewPostWindow): + client = None + image = None + apiError = QtCore.pyqtSignal(str) + sendSuccessful = QtCore.pyqtSignal() + + def __init__(self, parent=None, action="new", id=None, cid=None, text=""): + QtGui.QDialog.__init__(self, parent) + self.action = action + self.id = id + self.cid = cid + self.setupUi(self) + self.setupMyUi() + self.textEdit.setText(text) + self.textEdit.callback = self.mentions_suggest + self.textEdit.mention_flag = "@" + self.notify = Notify(timeout=1) + + def setupMyUi(self): + self.checkChars() + if self.action == "new": + self.chk_repost.setEnabled(False) + self.chk_comment.setEnabled(False) + self.chk_comment_original.setEnabled(False) + elif self.action == "retweet": + self.chk_repost.setEnabled(False) + self.pushButton_picture.setEnabled(False) + elif self.action == "comment": + self.chk_comment.setEnabled(False) + self.pushButton_picture.setEnabled(False) + elif self.action == "reply": + self.chk_repost.setEnabled(False) + self.chk_comment.setEnabled(False) + self.pushButton_picture.setEnabled(False) + + def mentions_suggest(self, text): + ret_users = [] + try: + word = re.findall('@[-a-zA-Z0-9_\u4e00-\u9fa5]+', text)[-1] + word = word.replace('@', '') + except IndexError: + return [] + if not word.strip(): + return [] + users = self.client.search.suggestions.at_users.get(q=word, type=0) + for user in users: + ret_users.append("@" + user['nickname']) + return ret_users + + def send(self): + self.pushButton_send.setEnabled(False) + if self.action == "new": + threading.Thread(group=None, target=self.new).start() + elif self.action == "retweet": + threading.Thread(group=None, target=self.retweet).start() + elif self.action == "comment": + threading.Thread(group=None, target=self.comment).start() + elif self.action == "reply": + threading.Thread(group=None, target=self.reply).start() + + def retweet(self): + text = str(self.textEdit.toPlainText()) + try: + self.client.statuses.repost.post(id=int(self.id), status=text, + is_comment=int((self.chk_comment.isChecked() + + self.chk_comment_original.isChecked() * 2))) + self.notify.showMessage(self.tr("WeCase"), + self.tr("Retweet Success!")) + self.sendSuccessful.emit() + except APIError as e: + self.apiError.emit(str(e)) + return + + def comment(self): + text = str(self.textEdit.toPlainText()) + try: + self.client.comments.create.post(id=int(self.id), comment=text, + comment_ori=int(self.chk_comment_original.isChecked())) + if self.chk_repost.isChecked(): + self.client.statuses.repost.post(id=int(self.id), status=text) + self.notify.showMessage(self.tr("WeCase"), + self.tr("Comment Success!")) + self.sendSuccessful.emit() + except APIError as e: + self.apiError.emit(str(e)) + return + + def reply(self): + text = str(self.textEdit.toPlainText()) + try: + self.client.comments.reply.post(id=int(self.id), cid=int(self.cid), + comment=text, + comment_ori=int(self.chk_comment_original.isChecked())) + if self.chk_repost.isChecked(): + self.client.statuses.repost.post(id=int(self.id), status=text) + self.notify.showMessage(self.tr("WeCase"), + self.tr("Reply Success!")) + self.sendSuccessful.emit() + except APIError as e: + self.apiError.emit(str(e)) + return + + def new(self): + text = str(self.textEdit.toPlainText()) + + try: + if self.image: + self.client.statuses.upload.post(status=text, + pic=open(self.image, "rb")) + else: + self.client.statuses.update.post(status=text) + + self.notify.showMessage(self.tr("WeCase"), + self.tr("Tweet Success!")) + self.sendSuccessful.emit() + except APIError as e: + self.apiError.emit(str(e)) + return + + self.image = None + + def addImage(self): + ACCEPT_TYPE = self.tr("Images") + "(*.png *.jpg *.bmp *.gif)" + if self.image: + self.image = None + self.pushButton_picture.setText(self.tr("Picture")) + else: + self.image = QtGui.QFileDialog.getOpenFileName(self, + self.tr("Choose a" + " image"), + filter=ACCEPT_TYPE) + # user may cancel the dialog, so check again + if self.image: + self.pushButton_picture.setText(self.tr("Remove the picture")) + + def showError(self, e): + if "Text too long" in e: + QtGui.QMessageBox.warning(None, self.tr("Text too long!"), + self.tr("Please remove some text.")) + else: + QtGui.QMessageBox.warning(None, self.tr("Unknown error!"), e) + self.pushButton_send.setEnabled(True) + + def showSmiley(self): + wecase_smiley = SmileyWindow() + if wecase_smiley.exec_(): + self.textEdit.textCursor().insertText(wecase_smiley.smileyName) + + def checkChars(self): + '''Check textEdit's characters. + If it larger than 140, Send Button will be disabled + and label will show red chars.''' + + text = self.textEdit.toPlainText() + numLens = 140 - tweetLength(text) + if numLens == 140 and (not self.action == "retweet"): + # you can not send empty tweet, except retweet + self.pushButton_send.setEnabled(False) + elif numLens >= 0: + # length is okay + self.label.setStyleSheet("color:black;") + self.pushButton_send.setEnabled(True) + else: + # text is too long + self.label.setStyleSheet("color:red;") + self.pushButton_send.setEnabled(False) + self.label.setText(str(numLens)) diff --git a/src/NewpostWindow_ui.py b/src/NewpostWindow_ui.py index 234ef75..18c5a33 100644 --- a/src/NewpostWindow_ui.py +++ b/src/NewpostWindow_ui.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'ui/NewpostWindow.ui' +# Form implementation generated from reading ui file './ui/NewpostWindow.ui' # -# Created: Sat Apr 13 20:36:28 2013 +# Created: Sun Apr 14 21:55:20 2013 # by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -26,12 +26,10 @@ def _translate(context, text, disambig): class Ui_NewPostWindow(object): def setupUi(self, NewPostWindow): NewPostWindow.setObjectName(_fromUtf8("NewPostWindow")) - NewPostWindow.resize(562, 292) + NewPostWindow.resize(562, 306) NewPostWindow.setAutoFillBackground(False) NewPostWindow.setProperty("unifiedTitleAndToolBarOnMac", False) - self.gridLayout_3 = QtGui.QGridLayout(NewPostWindow) - self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) - self.verticalLayout = QtGui.QVBoxLayout() + self.verticalLayout = QtGui.QVBoxLayout(NewPostWindow) self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) self.widget = QtGui.QWidget(NewPostWindow) self.widget.setMinimumSize(QtCore.QSize(0, 200)) @@ -55,8 +53,25 @@ def setupUi(self, NewPostWindow): self.textEdit.setObjectName(_fromUtf8("textEdit")) self.gridLayout.addWidget(self.textEdit, 0, 0, 1, 1) self.verticalLayout.addWidget(self.widget) - spacerItem = QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) - self.verticalLayout.addItem(spacerItem) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setSizeConstraint(QtGui.QLayout.SetFixedSize) + self.horizontalLayout.setContentsMargins(-1, -1, 0, -1) + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.label_2 = QtGui.QLabel(NewPostWindow) + self.label_2.setObjectName(_fromUtf8("label_2")) + self.horizontalLayout.addWidget(self.label_2) + self.chk_comment = QtGui.QCheckBox(NewPostWindow) + self.chk_comment.setObjectName(_fromUtf8("chk_comment")) + self.horizontalLayout.addWidget(self.chk_comment) + self.chk_repost = QtGui.QCheckBox(NewPostWindow) + self.chk_repost.setObjectName(_fromUtf8("chk_repost")) + self.horizontalLayout.addWidget(self.chk_repost) + self.chk_comment_original = QtGui.QCheckBox(NewPostWindow) + self.chk_comment_original.setObjectName(_fromUtf8("chk_comment_original")) + self.horizontalLayout.addWidget(self.chk_comment_original) + self.verticalLayout.addLayout(self.horizontalLayout) self.widget_2 = QtGui.QWidget(NewPostWindow) self.widget_2.setMinimumSize(QtCore.QSize(0, 40)) self.widget_2.setObjectName(_fromUtf8("widget_2")) @@ -78,7 +93,7 @@ def setupUi(self, NewPostWindow): self.pushButton.setObjectName(_fromUtf8("pushButton")) self.gridLayout_2.addWidget(self.pushButton, 0, 0, 1, 1) self.verticalLayout.addWidget(self.widget_2) - self.gridLayout_3.addLayout(self.verticalLayout, 0, 0, 1, 1) + self.verticalLayout.setStretch(0, 4) self.retranslateUi(NewPostWindow) QtCore.QObject.connect(self.pushButton_cancel, QtCore.SIGNAL(_fromUtf8("clicked()")), NewPostWindow.close) @@ -93,6 +108,10 @@ def setupUi(self, NewPostWindow): def retranslateUi(self, NewPostWindow): NewPostWindow.setWindowTitle(_translate("NewPostWindow", "New Message", None)) self.label.setText(_translate("NewPostWindow", "140", None)) + self.label_2.setText(_translate("NewPostWindow", "Also:", None)) + self.chk_comment.setText(_translate("NewPostWindow", "Comment", None)) + self.chk_repost.setText(_translate("NewPostWindow", "Repost", None)) + self.chk_comment_original.setText(_translate("NewPostWindow", "Commmet to Original", None)) self.pushButton_picture.setText(_translate("NewPostWindow", "&Picture", None)) self.pushButton_cancel.setText(_translate("NewPostWindow", "&Cancel", None)) self.pushButton_send.setText(_translate("NewPostWindow", "&Send", None)) diff --git a/src/Notify.py b/src/Notify.py new file mode 100644 index 0000000..dc552ab --- /dev/null +++ b/src/Notify.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +# WeCase -- This file implemented Notify. +# Copyright (C) 2013 Tom Li +# License: GPL v3 or later. + + +from PyQt4 import QtCore +import notify2 as pynotify +import const + + +class Notify(): + image = const.myself_path + "/ui/img/WeCase 80.png" + + def __init__(self, appname=QtCore.QObject().tr("WeCase"), timeout=5): + pynotify.init(appname) + self.timeout = timeout + self.n = pynotify.Notification(appname) + + def showMessage(self, title, text): + self.n.update(title, text, self.image) + self.n.set_timeout(self.timeout * 1000) + self.n.show() diff --git a/src/SettingWindow.py b/src/SettingWindow.py new file mode 100644 index 0000000..0fb8f11 --- /dev/null +++ b/src/SettingWindow.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +# WeCase -- This file implemented SettingWindow. +# Copyright (C) 2013 Tom Li +# License: GPL v3 or later. + + +from PyQt4 import QtGui +from configparser import ConfigParser +from SettingWindow_ui import Ui_SettingWindow +import const + + +class WeSettingsWindow(QtGui.QDialog, Ui_SettingWindow): + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.setupUi(self) + self.loadConfig() + + def transformInterval(self, sliderValue): + return (sliderValue // 60, sliderValue % 60) + + def setIntervalText(self, sliderValue): + self.intervalLabel.setText(self.tr("%i min %i sec") % + (self.transformInterval(sliderValue))) + + def setTimeoutText(self, sliderValue): + self.timeoutLabel.setText(self.tr("%i sec") % sliderValue) + + def loadConfig(self): + self.config = ConfigParser() + self.config.read(const.config_path) + + if not self.config.has_section('main'): + self.config['main'] = {} + + self.main_config = self.config['main'] + self.intervalSlider.setValue(int(self.main_config.get( + 'notify_interval', "30"))) + self.setIntervalText(self.intervalSlider.value()) + self.timeoutSlider.setValue(int(self.main_config.get( + "notify_timeout", "5"))) + self.setTimeoutText(self.timeoutSlider.value()) + self.commentsChk.setChecked(self.main_config.getboolean( + "remind_comments", True)) + self.mentionsChk.setChecked(self.main_config.getboolean( + "remind_mentions", True)) + + def saveConfig(self): + self.config = ConfigParser() + self.config.read(const.config_path) + + if not self.config.has_section('main'): + self.config['main'] = {} + + self.main_config = self.config['main'] + self.main_config['notify_interval'] = str(self.intervalSlider.value()) + self.main_config['notify_timeout'] = str(self.timeoutSlider.value()) + self.main_config['remind_comments'] = str(self.commentsChk.isChecked()) + self.main_config['remind_mentions'] = str(self.mentionsChk.isChecked()) + + with open(const.config_path, "w+") as config_file: + self.config.write(config_file) + + def accept(self): + self.saveConfig() + self.done(True) + + def reject(self): + self.done(False) diff --git a/src/SettingWindow_ui.py b/src/SettingWindow_ui.py index 4016a24..8f8c238 100644 --- a/src/SettingWindow_ui.py +++ b/src/SettingWindow_ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file './ui/SettingWindow.ui' # -# Created: Mon Apr 8 16:40:03 2013 +# Created: Sun Apr 14 21:55:20 2013 # by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! diff --git a/src/Smiley.py b/src/Smiley.py index a9b1d51..5ede37d 100644 --- a/src/Smiley.py +++ b/src/Smiley.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# WeCase -- This model implemented a model for smileies +# WeCase -- This model implemented a model for smileies. # Copyright: GPL v3 or later. + import sys import os from PyQt4 import QtCore diff --git a/src/SmileyWindow.py b/src/SmileyWindow.py new file mode 100644 index 0000000..4218400 --- /dev/null +++ b/src/SmileyWindow.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +# WeCase -- This file implemented SmileyWindow. +# Copyright (C) 2013 Tom Li +# License: GPL v3 or later. + + +from PyQt4 import QtCore, QtGui +from Smiley import SmileyModel, SmileyItem +from SmileyWindow_ui import Ui_SmileyWindow +import const + + +class SmileyWindow(QtGui.QDialog, Ui_SmileyWindow): + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.setupUi(self) + self.setupMyUi() + self.setupModels() + self.smileyName = "" + + def setupMyUi(self): + self.smileyView.setResizeMode(self.smileyView.SizeRootObjectToView) + + def setupModels(self): + self.smileyModel = SmileyModel(self) + self.smileyModel.init_smileies(const.myself_path + "./ui/img/smiley", + self.smileyModel, SmileyItem) + self.smileyView.rootContext().setContextProperty("SmileyModel", + self.smileyModel) + self.smileyView.rootContext().setContextProperty("parentWindow", self) + self.smileyView.setSource(QtCore.QUrl.fromLocalFile( + const.myself_path + "/ui/SmileyView.qml")) + + @QtCore.pyqtSlot(str) + def returnSmileyName(self, smileyName): + self.smileyName = smileyName + self.done(True) diff --git a/src/SmileyWindow_ui.py b/src/SmileyWindow_ui.py index 45ea1a5..b03845b 100644 --- a/src/SmileyWindow_ui.py +++ b/src/SmileyWindow_ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file './ui/SmileyWindow.ui' # -# Created: Mon Apr 8 16:40:03 2013 +# Created: Sun Apr 14 21:55:20 2013 # by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! diff --git a/src/Tweet.py b/src/Tweet.py index 8f287ad..9c4d11f 100644 --- a/src/Tweet.py +++ b/src/Tweet.py @@ -4,68 +4,289 @@ # WeCase -- This model implemented Model and Item for tweets # Copyright: GPL v3 or later. +import threading from PyQt4 import QtCore from datetime import datetime +from TweetUtils import get_mid from WTimeParser import WTimeParser as time_parser -class TweetModel(QtCore.QAbstractListModel): +class TweetAbstractModel(QtCore.QAbstractListModel): def __init__(self, prototype, parent=None): - QtCore.QAbstractListModel.__init__(self, parent) - self.setRoleNames(prototype.roleNames()) - self.tweets = [] + super(TweetAbstractModel, self).__init__() + self.setRoleNames(prototype.roles) + self._tweets = [] def appendRow(self, item): self.insertRow(self.rowCount(), item) + def appendRows(self, items): + for item in items: + self.appendRow(TweetItem(item)) + def clear(self): - del self.tweets - self.tweets = [] + self._tweets = [] def data(self, index, role): - return self.tweets[index.row()].data(role) + return self._tweets[index.row()].data(role) def insertRow(self, row, item): self.beginInsertRows(QtCore.QModelIndex(), row, row) - self.tweets.insert(row, item) + self._tweets.insert(row, item) + self.endInsertRows() + + def insertRows(self, row, items): + self.beginInsertRows(QtCore.QModelIndex(), row, row + len(items) - 1) + for item in items: + self._tweets.insert(row, TweetItem(item)) self.endInsertRows() def rowCount(self, parent=QtCore.QModelIndex()): - return len(self.tweets) - - -class TweetItem(QtCore.QAbstractItemModel): - typeRole = QtCore.Qt.UserRole + 1 - idRole = QtCore.Qt.UserRole + 2 - authorRole = QtCore.Qt.UserRole + 3 - avatarRole = QtCore.Qt.UserRole + 4 - contentRole = QtCore.Qt.UserRole + 5 - timeRole = QtCore.Qt.UserRole + 6 - originalIdRole = QtCore.Qt.UserRole + 7 - originalContentRole = QtCore.Qt.UserRole + 8 - originalAuthorRole = QtCore.Qt.UserRole + 9 - originalTimeRole = QtCore.Qt.UserRole + 10 - thumbnailPicRole = QtCore.Qt.UserRole + 11 - - def __init__(self, type="", id="", author="", avatar="", content="", - time="", original_id="", original_content="", - original_author="", original_time="", thumbnail_pic="", - parent=None): - QtCore.QAbstractItemModel.__init__(self, parent) - - self.type = type - self.id = id - self.author = author - self.avatar = avatar - self.content = content - self.time = time - self.original_id = original_id - self.original_content = original_content - self.original_author = original_author - self.original_time = original_time - self.thumbnail_pic = thumbnail_pic - - def sinceTimeString(self, createTime): + return len(self._tweets) + + +class TweetCommonModel(TweetAbstractModel): + timelineLoaded = QtCore.pyqtSignal() + + def __init__(self, prototype, timeline=None, parent=None): + super(TweetCommonModel, self).__init__(prototype, parent) + self.timeline = timeline + self.lock = False + + def _get_thread(self, page): + if self.lock: + return + self.lock = True + timeline = self.timeline.get(page=page).statuses + self.appendRows(timeline) + + self.since = int(self._tweets[0].id) + self.max = int(self._tweets[-1].id) + self.lock = False + + def _get(self, page): + threading.Thread(group=None, target=self._get_thread, + args=(page,)).start() + + def _new_thread(self, since): + if self.lock: + return + self.lock = True + timeline = self.timeline.get(since_id=since).statuses[::-1] + self.insertRows(0, timeline) + + self.since = int(self._tweets[0].id) + self.lock = False + self.timelineLoaded.emit() + + def _new(self, since): + threading.Thread(group=None, target=self._new_thread, + args=(since,)).start() + + def _old_thread(self, max): + if self.lock: + return + self.lock = True + timeline = self.timeline.get(max_id=max).statuses + + # Remove the first same tweet + self.appendRows(timeline[1::]) + self.max = int(self._tweets[-1].id) + self.lock = False + + def _old(self, max): + threading.Thread(group=None, target=self._old_thread, + args=(max,)).start() + + + # Public + def load(self): + self._get(1) + self.page = 1 + + def next(self): + self._old(self.max) + + def new(self): + self.page = 1 + self._new(self.since) + + +class TweetCommentModel(TweetCommonModel): + def __init__(self, prototype, timeline=None, parent=None): + super(TweetCommentModel, self).__init__(prototype, timeline, parent) + + def _get_thread(self, page): + if self.lock: + return + self.lock = True + timeline = self.timeline.get(page=page).comments + self.appendRows(timeline) + + self.since = int(self._tweets[0].id) + self.max = int(self._tweets[-1].id) + self.lock = False + + def _get(self, page): + threading.Thread(group=None, target=self._get_thread, + args=(page,)).start() + + def _new_thread(self, since): + if self.lock: + return + timeline = self.timeline.get(since_id=since).comments[::-1] + self.insertRows(0, timeline) + self.since = int(self._tweets[0].id) + self.lock = False + self.timelineLoaded.emit() + + def _new(self, since): + threading.Thread(group=None, target=self._new_thread, + args=(since,)).start() + + def _old_thread(self, max): + if self.lock: + return + self.lock = True + timeline = self.timeline.get(max_id=max).statuses + + # Remove the first same tweet + self.appendRows(timeline[1::]) + self.max = int(self._tweets[-1].id) + self.lock = False + + def _old(self, max): + threading.Thread(group=None, target=self._old_thread, + args=(max,)).start() + + +class UserItem(QtCore.QObject): + def __init__(self, item, parent=None): + super(UserItem, self).__init__() + self._data = item + + @QtCore.pyqtProperty(str, constant=True) + def id(self): + return self._data.get('idstr') + + @QtCore.pyqtProperty(str, constant=True) + def name(self): + return self._data.get('name') + + @QtCore.pyqtProperty(str, constant=True) + def avatar(self): + return self._data.get('profile_image_url') + + +class TweetItem(QtCore.QObject): + TWEET = 0 + RETWEET = 1 + COMMENT = 2 + roles = { + QtCore.Qt.UserRole + 1: "type", + QtCore.Qt.UserRole + 2: "id", + QtCore.Qt.UserRole + 3: "mid", + QtCore.Qt.UserRole + 4: "url", + QtCore.Qt.UserRole + 5: "author", + QtCore.Qt.UserRole + 6: "time", + QtCore.Qt.UserRole + 7: "text", + QtCore.Qt.UserRole + 8: "original", + QtCore.Qt.UserRole + 9: "thumbnail_pic", + QtCore.Qt.UserRole + 10: "original_pic" + } + + def __init__(self, item={}, parent=None): + super(TweetItem, self).__init__() + self._data = item + + if not item: + return + + self._roleData = { + "type": self.type, + "id": self.id, + "mid": self.mid, + "url": self.url, + "author": self.author, + "time": self.time, + "text": self.text, + "original": self.original, + "thumbnail_pic": self.thumbnail_pic, + "original_pic": self.original_pic, + } + + def data(self, key): + return self._roleData[self.roles[key]] + + @QtCore.pyqtProperty(int, constant=True) + def type(self): + if "retweeted_status" in self._data: + return self.RETWEET + elif "status" in self._data: + return self.COMMENT + else: + return self.TWEET + + @QtCore.pyqtProperty(str, constant=True) + def id(self): + return self._data.get('idstr') + + @QtCore.pyqtProperty(str, constant=True) + def mid(self): + decimal_mid = str(self._data.get('mid')) + encode_mid = get_mid(decimal_mid) + return encode_mid + + @QtCore.pyqtProperty(str, constant=True) + def url(self): + try: + uid = self._data['user']['id'] + mid = get_mid(self._data['mid']) + except KeyError: + # Sometimes Sina's API doesn't return user + # when our tweet is deeply nested. Just forgot it. + return "" + return 'http://weibo.com/%s/%s' % (uid, mid) + + @QtCore.pyqtProperty(QtCore.QObject, constant=True) + def author(self): + if "user" in self._data: + self._user = UserItem(self._data.get('user'), self) + return self._user + else: + return None + + @QtCore.pyqtProperty(str, constant=True) + def time(self): + return self._sinceTimeString(self._data.get('created_at')) + + @QtCore.pyqtProperty(str, constant=True) + def text(self): + return self._data.get('text') + + @QtCore.pyqtProperty(QtCore.QObject, constant=True) + def original(self): + if self.type == self.RETWEET: + self._original = TweetItem(self._data.get('retweeted_status')) + return self._original + elif self.type == self.COMMENT: + self._original = TweetItem(self._data.get('status')) + return self._original + else: + return None + + @QtCore.pyqtProperty(str, constant=True) + def thumbnail_pic(self): + return self._data.get('thumbnail_pic', "") + + @QtCore.pyqtProperty(str, constant=True) + def original_pic(self): + return self._data.get('original_pic') + + def _sinceTimeString(self, createTime): + if not createTime: + return + create = time_parser().parse(createTime) create_utc = (create - create.utcoffset()).replace(tzinfo=None) now_utc = datetime.utcnow() @@ -85,44 +306,3 @@ def sinceTimeString(self, createTime): return self.tr("%.0f hours ago") % (passedSeconds / 3600) return self.tr("%.0f days ago") % (passedSeconds / 86400) - - def roleNames(self): - names = {} - names[self.typeRole] = "type" - names[self.idRole] = "id" - names[self.authorRole] = "author" - names[self.avatarRole] = "avatar" - names[self.contentRole] = "content" - names[self.timeRole] = "time" - names[self.originalIdRole] = "original_id" - names[self.originalContentRole] = "original_content" - names[self.originalAuthorRole] = "original_author" - names[self.originalTimeRole] = "original_time" - names[self.thumbnailPicRole] = "thumbnail_pic" - return names - - def data(self, role): - if role == self.typeRole: - return self.type - elif role == self.idRole: - return self.id - elif role == self.authorRole: - return self.author - elif role == self.avatarRole: - return self.avatar - elif role == self.contentRole: - return self.content - elif role == self.timeRole: - return self.sinceTimeString(self.time) - elif role == self.originalIdRole: - return self.original_id - elif role == self.originalContentRole: - return self.original_content - elif role == self.originalAuthorRole: - return self.original_author - elif role == self.originalTimeRole: - return self.original_time - elif role == self.thumbnailPicRole: - return self.thumbnail_pic - else: - return None diff --git a/src/TweetUtils.py b/src/TweetUtils.py index 2561f48..96ad268 100644 --- a/src/TweetUtils.py +++ b/src/TweetUtils.py @@ -2,7 +2,7 @@ # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 # WeCase -- This model implemented a bug-for-bug compatible -# strings' length counter with Sina's +# strings' length counter with Sina's # Copyright (C) 2013 Tom Li # License: GPL v3 or later. @@ -51,3 +51,32 @@ def findall(regex, text): (byteLen - TWEET_MAX + TWEET_URL_LEN)) n = n.replace(url, "") return ceil((total + len(n) + len(re.findall(r"[^\x00-\x80]", n))) / 2) + + +def get_mid(mid): + """Convert a id of a tweet to a mid.""" + + def baseN(num, base): + """Convert the base of a decimal.""" + CHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + return ((num == 0) and "0") or (baseN(num // base, base).lstrip("0") + + CHAR[num % base]) + + url = "" + + i = len(mid) - 7 + while i > -7: + offset_1 = 0 if i < 0 else i + offset_2 = i + 7 + num = mid[offset_1:offset_2] + num = baseN(int(num), 62) + + if not len(num) == 1: + # if it isn't the first char of the mid, and it's length less than + # four chars, add zero at left for spacing + num = num.rjust(4, "0") + + url = num + url + + i -= 7 + return url diff --git a/src/WTimer.py b/src/WTimer.py index 67b5264..cb03959 100644 --- a/src/WTimer.py +++ b/src/WTimer.py @@ -7,7 +7,6 @@ # License: GPL v3 or later. -import time import threading diff --git a/src/WeCaseWindow.py b/src/WeCaseWindow.py new file mode 100644 index 0000000..16422cf --- /dev/null +++ b/src/WeCaseWindow.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +# WeCase -- This file implemented WeCaseWindow, the mainWindow of WeCase. +# Copyright (C) 2013 Tom Li +# License: GPL v3 or later. + + +import os +import urllib.request +import urllib.parse +import urllib.error +from configparser import ConfigParser +import threading +from WTimer import WTimer +from PyQt4 import QtCore, QtGui +from Tweet import TweetCommonModel, TweetCommentModel, TweetItem +from MainWindow_ui import Ui_frm_MainWindow +from Notify import Notify +from NewpostWindow import NewpostWindow +from SettingWindow import WeSettingsWindow +from AboutWindow import AboutWindow +import const + + +class WeCaseWindow(QtGui.QMainWindow, Ui_frm_MainWindow): + client = None + uid = None + imageLoaded = QtCore.pyqtSignal(str) + tabTextChanged = QtCore.pyqtSignal(int, str) + + def __init__(self, client, parent=None): + QtGui.QMainWindow.__init__(self, parent) + self.setupUi(self) + self.tweetViews = [self.homeView, self.mentionsView, self.commentsView, + self.myView] + self.client = client + self.setupModels() + self.init_account() + self.setupMyUi() + self.loadConfig() + self.IMG_AVATAR = -2 + self.IMG_THUMB = -1 + self.notify = Notify(timeout=self.notify_timeout) + self.applyConfig() + self.download_lock = [] + + def init_account(self): + self.get_uid() + + def loadConfig(self): + self.config = ConfigParser() + self.config.read(const.config_path) + + if not self.config.has_section('main'): + self.config['main'] = {} + + self.main_config = self.config['main'] + self.timer_interval = int(self.main_config.get('notify_interval', 30)) + self.notify_timeout = int(self.main_config.get('notify_timeout', 5)) + self.remindMentions = self.main_config.getboolean('remind_mentions', 1) + self.remindComments = self.main_config.getboolean('remind_comments', 1) + + def applyConfig(self): + try: + self.timer.stop_event.set() + except AttributeError: + pass + + self.timer = WTimer(self.timer_interval, self.show_notify) + self.timer.start() + self.notify.timeout = self.notify_timeout + + def setupMyUi(self): + for tweetView in self.tweetViews: + tweetView.setResizeMode(tweetView.SizeRootObjectToView) + tweetView.setSource( + QtCore.QUrl.fromLocalFile(const.myself_path + + "/ui/TweetList.qml")) + tweetView.rootContext().setContextProperty("mainWindow", self) + + @QtCore.pyqtSlot() + def load_more(self): + model = self.get_current_model() + model.next() + + def setupModels(self): + self.all_timeline = TweetCommonModel(TweetItem(), + self.client.statuses.home_timeline, + self) + self.all_timeline.load() + self.homeView.rootContext().setContextProperty("mymodel", + self.all_timeline) + self.mentions = TweetCommonModel(TweetItem(), + self.client.statuses.mentions, + self) + self.mentions.load() + self.mentionsView.rootContext().setContextProperty("mymodel", + self.mentions) + self.comment_to_me = TweetCommentModel(TweetItem(), + self.client.comments.to_me, + self) + self.comment_to_me.load() + self.commentsView.rootContext().setContextProperty("mymodel", + self.comment_to_me) + self.my_timeline = TweetCommonModel(TweetItem(), + self.client.statuses.user_timeline, + self) + self.my_timeline.load() + self.myView.rootContext().setContextProperty("mymodel", + self.my_timeline) + + def reset_remind(self): + if self.tabWidget.currentIndex() == 0: + self.tabWidget.setTabText(0, self.tr("Weibo")) + elif self.tabWidget.currentIndex() == 1: + self.client.remind.set_count.post(type="mention_status") + self.tabWidget.setTabText(1, self.tr("@Me")) + elif self.tabWidget.currentIndex() == 2: + self.client.remind.set_count.post(type="cmt") + self.tabWidget.setTabText(2, self.tr("Comments")) + + def get_remind(self, uid): + '''this function is used to get unread_count + from Weibo API. uid is necessary.''' + + reminds = self.client.remind.unread_count.get(uid=uid) + return reminds + + def get_uid(self): + '''How can I get my uid? here it is''' + try: + self.uid = self.client.account.get_uid.get().uid + except AttributeError: + return None + + def show_notify(self): + # This function is run in another thread by WTimer. + # Do not modify UI directly. Send signal and react it in a slot only. + # We use SIGNAL self.tabTextChanged and SLOT self.setTabText() + # to display unread count + + reminds = self.get_remind(self.uid) + msg = self.tr("You have:") + "\n" + num_msg = 0 + + if reminds['status'] != 0: + # Note: do NOT send notify here, or users will crazy. + self.tabTextChanged.emit(0, self.tr("Weibo(%d)") + % reminds['status']) + + if reminds['mention_status'] and self.remindMentions: + msg += self.tr("%d unread @ME") % reminds['mention_status'] + "\n" + self.tabTextChanged.emit(1, self.tr("@Me(%d)") + % reminds['mention_status']) + num_msg += 1 + + if reminds['cmt'] and self.remindComments: + msg += self.tr("%d unread comment(s)") % reminds['cmt'] + "\n" + self.tabTextChanged.emit(2, self.tr("Comments(%d)") + % reminds['cmt']) + num_msg += 1 + + if num_msg: + return + self.notify.showMessage(self.tr("WeCase"), msg) + + def setTabText(self, index, string): + self.tabWidget.setTabText(index, string) + + def moveToTop(self): + self.get_current_tweetView().rootObject().positionViewAtBeginning() + + def setLoaded(self, tweetid): + self.get_current_tweetView().rootObject().imageLoaded(tweetid) + + def showSettings(self): + wecase_settings = WeSettingsWindow() + if wecase_settings.exec_(): + self.loadConfig() + self.applyConfig() + + def showAbout(self): + wecase_about = AboutWindow() + wecase_about.exec_() + + def logout(self): + self.close() + # This is a model dialog, if we exec it before we close MainWindow + # MainWindow won't close + from LoginWindow import LoginWindow + wecase_login = LoginWindow() + wecase_login.exec_() + + def postTweet(self): + wecase_new = NewpostWindow() + wecase_new.client = self.client + wecase_new.exec_() + + @QtCore.pyqtSlot(str) + def comment(self, idstr): + wecase_new = NewpostWindow(action="comment", id=int(idstr)) + wecase_new.client = self.client + wecase_new.exec_() + + @QtCore.pyqtSlot(str, str) + def repost(self, idstr, text): + wecase_new = NewpostWindow(action="retweet", id=int(idstr), text=text) + wecase_new.client = self.client + wecase_new.exec_() + + @QtCore.pyqtSlot(str, result=int) + def favorite(self, idstr): + try: + self.client.favorites.create.post(id=int(idstr)) + return True + except: + return False + + @QtCore.pyqtSlot(str, result=bool) + def un_favorite(self, idstr): + try: + self.client.favorites.destroy.post(id=int(idstr)) + return True + except: + return False + + @QtCore.pyqtSlot(str, str) + def reply(self, idstr, cidstr): + wecase_new = NewpostWindow(action="reply", id=int(idstr), + cid=int(cidstr)) + wecase_new.client = self.client + wecase_new.exec_() + + @QtCore.pyqtSlot(str, str) + def look_orignal_pic(self, thumbnail_pic, tweetid): + threading.Thread(group=None, target=self.fetch_open_original_pic, + args=(thumbnail_pic, tweetid)).start() + + def fetch_open_original_pic(self, thumbnail_pic, tweetid): + """Fetch and open original pic from thumbnail pic url. + Pictures will stored in cache directory. If we already have a same + name in cache directory, just open it. If we don't, then download it + first.""" + + if tweetid in self.download_lock: + return + self.download_lock.append(tweetid) + original_pic = thumbnail_pic.replace("thumbnail", + "large") # A simple trick ... ^_^ + localfile = const.cache_path + original_pic.split("/")[-1] + if not os.path.exists(localfile): + urllib.request.urlretrieve(original_pic, localfile) + + self.download_lock.remove(tweetid) + os.popen("xdg-open " + localfile) # xdg-open is common? + self.imageLoaded.emit(tweetid) + + def refresh(self): + model = self.get_current_model() + model.timelineLoaded.connect(self.moveToTop) + #model.clear() + #model.load() + model.new() + self.reset_remind() + + def get_current_tweetView(self): + tweetViews = {0: self.homeView, 1: self.mentionsView, + 2: self.commentsView, 3: self.myView} + return tweetViews[self.tabWidget.currentIndex()] + + def get_current_model(self): + models = {0: self.all_timeline, 1: self.mentions, + 2: self.comment_to_me, + 3: self.my_timeline} + return models[self.tabWidget.currentIndex()] + + def get_current_function(self): + functions = {0: self.get_all_timeline, 1: self.get_mentions_timeline, + 2: self.get_comment_to_me, 3: self.get_my_timeline} + return functions[self.tabWidget.currentIndex()] + + def closeEvent(self, event): + self.timer.stop_event.set() diff --git a/src/const.py b/src/const.py new file mode 100644 index 0000000..f7e5fe6 --- /dev/null +++ b/src/const.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +# WeCase -- This file defined constants and runtime constants. +# Copyright (C) 2013 Tom Li +# License: GPL v3 or later. + + +import sys +import os + + +APP_KEY = "1011524190" +APP_SECRET = "1898b3f668368b9f4a6f7ac8ed4a918f" +CALLBACK_URL = 'https://api.weibo.com/oauth2/default.html' +OAUTH2_PARAMETER = {'client_id': APP_KEY, + 'response_type': 'code', + 'redirect_uri': CALLBACK_URL, + 'action': 'submit', + 'userId': '', # username + 'passwd': '', # password + 'isLoginSina': 0, + 'from': '', + 'regCallback': '', + 'state': '', + 'ticket': '', + 'withOfficalFlag': 0} +config_path = os.environ['HOME'] + '/.config/wecase/config_db' +cache_path = os.environ['HOME'] + '/.cache/wecase/' +myself_name = sys.argv[0].split('/')[-1] +myself_path = os.path.abspath(sys.argv[0]).replace(myself_name, "") diff --git a/src/wecase.pro b/src/wecase.pro new file mode 100644 index 0000000..26083e5 --- /dev/null +++ b/src/wecase.pro @@ -0,0 +1,15 @@ +SOURCES = wecase.py\ + Smiley.py\ + Tweet.py\ + NewpostWindow.py\ + WeCaseWindow.py\ + LoginWindow.py + +FORMS = ui/AboutWindow.ui\ + ui/LoginWindow.ui\ + ui/MainWindow.ui\ + ui/NewpostWindow.ui\ + ui/SettingWindow.ui\ + ui/SmileyWindow.ui + +TRANSLATIONS = locale/WeCase_zh_CN.ts diff --git a/src/wecase.py b/src/wecase.py index 28a907c..6ec40c9 100755 --- a/src/wecase.py +++ b/src/wecase.py @@ -1,806 +1,33 @@ #!/usr/bin/env python3 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# WeCase -- Linux Sina Weibo Client -# Since 4th,Feb,2013 -# This is a TEST version -# Wait for ... +# WeCase -- Linux Sina Weibo Client, Since 4th, Feb, 2013. +# This is the main file of WeCase. # Copyright: GPL v3 or later. -# Well, Let's go! - import sys import os -import re -import webbrowser -import urllib.request -import urllib.parse -import urllib.error -import http.client -from configparser import ConfigParser -import notify2 as pynotify -import threading -from WTimer import WTimer -from weibo import APIClient, APIError from PyQt4 import QtCore, QtGui -from TweetUtils import tweetLength -from Tweet import TweetModel, TweetItem -from Smiley import SmileyModel, SmileyItem -from LoginWindow_ui import Ui_frm_Login -from MainWindow_ui import Ui_frm_MainWindow -from SettingWindow_ui import Ui_SettingWindow -from NewpostWindow_ui import Ui_NewPostWindow -from AboutWindow_ui import Ui_About_Dialog -from SmileyWindow_ui import Ui_SmileyWindow - -APP_KEY = "1011524190" -APP_SECRET = "1898b3f668368b9f4a6f7ac8ed4a918f" -CALLBACK_URL = 'https://api.weibo.com/oauth2/default.html' -OAUTH2_PARAMETER = {'client_id': APP_KEY, - 'response_type': 'code', - 'redirect_uri': CALLBACK_URL, - 'action': 'submit', - 'userId': '', # username - 'passwd': '', # password - 'isLoginSina': 0, - 'from': '', - 'regCallback': '', - 'state': '', - 'ticket': '', - 'withOfficalFlag': 0} -config_path = os.environ['HOME'] + '/.config/wecase/config_db' -cache_path = os.environ['HOME'] + '/.cache/wecase/' -myself_name = sys.argv[0].split('/')[-1] -myself_path = os.path.abspath(sys.argv[0]).replace(myself_name, "") - - -class LoginWindow(QtGui.QDialog, Ui_frm_Login): - loginReturn = QtCore.pyqtSignal(object) - - def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) - self.loadConfig() - self.setupUi(self) - self.setupMyUi() - self.loginReturn.connect(self.checkLogin) - - def accept(self, client): - if self.chk_Remember.isChecked(): - self.passwd[str(self.username)] = str(self.password) - self.last_login = str(self.username) - self.saveConfig() - wecase_main = WeCaseWindow() - wecase_main.init_account(client) - wecase_main.show() - self.done(True) - - def reject(self): - QtGui.QMessageBox.critical(None, self.tr("Authorize Failed!"), - self.tr("Check your account and " - "password!")) - self.pushButton_log.setText(self.tr("GO!")) - self.pushButton_log.setEnabled(True) - - def checkLogin(self, client): - if client: - self.accept(client) - else: - self.reject() - - def setupMyUi(self): - self.show() - self.txt_Password.setEchoMode(QtGui.QLineEdit.Password) - self.cmb_Users.addItem(self.last_login) - self.chk_AutoLogin.setChecked(self.auto_login) - - for username in list(self.passwd.keys()): - if username == self.last_login: - continue - self.cmb_Users.addItem(username) - - if self.cmb_Users.currentText(): - self.setPassword(self.cmb_Users.currentText()) - - if self.auto_login: - self.login() - - def loadConfig(self): - self.config = ConfigParser() - self.config.read(config_path) - - if not self.config.has_section('login'): - self.config['login'] = {} - - self.login_config = self.config['login'] - self.passwd = eval(self.login_config.get('passwd', "{}")) - self.last_login = str(self.login_config.get('last_login', "")) - self.auto_login = self.login_config.getboolean('auto_login', 0) - - def saveConfig(self): - self.login_config['passwd'] = str(self.passwd) - self.login_config['last_login'] = self.last_login - self.login_config['auto_login'] = str(self.chk_AutoLogin.isChecked()) - - def login(self): - self.pushButton_log.setText(self.tr("Login, waiting...")) - self.pushButton_log.setEnabled(False) - self.ui_authorize() - - def ui_authorize(self): - self.username = self.cmb_Users.currentText() - self.password = self.txt_Password.text() - threading.Thread(group=None, target=self.authorize, - args=(self.username, self.password)).start() - - def authorize(self, username, password): - # TODO: This method is very messy, maybe do some cleanup? - - client = APIClient(app_key=APP_KEY, app_secret=APP_SECRET, - redirect_uri=CALLBACK_URL) - - # Step 1: Get the authorize url from Sina - authorize_url = client.get_authorize_url() - - # Step 2: Send the authorize info to Sina and get the authorize_code - # TODO: Rewrite them with urllib/urllib2 - oauth2 = OAUTH2_PARAMETER - oauth2['userId'] = username - oauth2['passwd'] = password - postdata = urllib.parse.urlencode(oauth2) - - conn = http.client.HTTPSConnection('api.weibo.com') - conn.request('POST', '/oauth2/authorize', postdata, - {'Referer': authorize_url, - 'Content-Type': 'application/x-www-form-urlencoded'}) - - res = conn.getresponse() - - location = res.getheader('location') - - if not location: - return self.loginReturn.emit(None) - - authorize_code = location.split('=')[1] - conn.close() - - # Step 3: Put the authorize information into SDK - r = client.request_access_token(authorize_code) - access_token = r.access_token - expires_in = r.expires_in - - client.set_access_token(access_token, expires_in) - self.loginReturn.emit(client) - - def setPassword(self, username): - if username: - self.txt_Password.setText(self.passwd[str(username)]) - - def openRegisterPage(self): - webbrowser.open("http://weibo.com/signup/signup.php") - - def closeEvent(self, event): - with open(config_path, "w+") as config_file: - self.config.write(config_file) - - -class WeCaseWindow(QtGui.QMainWindow, Ui_frm_MainWindow): - client = None - uid = None - timelineLoaded = QtCore.pyqtSignal(int) - imageLoaded = QtCore.pyqtSignal(str) - tabTextChanged = QtCore.pyqtSignal(int, str) - - def __init__(self, parent=None): - QtGui.QMainWindow.__init__(self, parent) - self.setupUi(self) - self.tweetViews = [self.homeView, self.mentionsView, self.commentsView, - self.myView] - self.setupModels() - self.setupMyUi() - self.loadConfig() - self.IMG_AVATAR = -2 - self.IMG_THUMB = -1 - self.notify = Notify(timeout=self.notify_timeout) - self.applyConfig() - - def init_account(self, client): - self.client = client - self.get_uid() - self.get_all_timeline() - self.get_my_timeline() - self.get_mentions_timeline() - self.get_comment_to_me() - - def loadConfig(self): - self.config = ConfigParser() - self.config.read(config_path) - - if not self.config.has_section('main'): - self.config['main'] = {} - - self.main_config = self.config['main'] - self.timer_interval = int(self.main_config.get('notify_interval', 30)) - self.notify_timeout = int(self.main_config.get('notify_timeout', 5)) - self.remindMentions = self.main_config.getboolean('remind_mentions', 1) - self.remindComments = self.main_config.getboolean('remind_comments', 1) - - def applyConfig(self): - try: - self.timer.stop_event.set() - except AttributeError: - pass - - self.timer = WTimer(self.timer_interval, self.show_notify) - self.timer.start() - self.notify.timeout = self.notify_timeout - - def setupMyUi(self): - for tweetView in self.tweetViews: - tweetView.setResizeMode(tweetView.SizeRootObjectToView) - tweetView.setSource( - QtCore.QUrl.fromLocalFile(myself_path + "/ui/TweetList.qml")) - tweetView.rootContext().setContextProperty("mainWindow", self) - - @QtCore.pyqtSlot() - def load_more(self): - if self.tabWidget.currentIndex() == 0: - self.all_timeline_page += 1 - self.get_all_timeline(self.all_timeline_page) - elif self.tabWidget.currentIndex() == 1: - self.mentions_page += 1 - self.get_mentions_timeline(self.mentions_page) - elif self.tabWidget.currentIndex() == 2: - self.comment_to_me_page += 1 - self.get_comment_to_me(self.comment_to_me_page) - elif self.tabWidget.currentIndex() == 3: - self.my_timeline_page += 1 - self.get_my_timeline(self.my_timeline_page) - - def setupModels(self): - self.all_timeline = TweetModel(TweetItem(), self) - self.homeView.rootContext().setContextProperty("mymodel", - self.all_timeline) - self.mentions = TweetModel(TweetItem(), self) - self.mentionsView.rootContext().setContextProperty("mymodel", - self.mentions) - self.comment_to_me = TweetModel(TweetItem(), self) - self.commentsView.rootContext().setContextProperty("mymodel", - self.comment_to_me) - self.my_timeline = TweetModel(TweetItem(), self) - self.myView.rootContext().setContextProperty("mymodel", - self.my_timeline) - - def get_timeline(self, timeline, model, more=False): - for count, item in enumerate(timeline): - # tweet (default), comment or retweet? - item_type = "tweet" - - # simple tweet or comment - item_id = item['idstr'] - item_author = item['user']['name'] - item_author_avatar = item['user']['profile_image_url'] - item_content = item['text'] - item_content_time = item['created_at'] - - # comment only - try: - item_comment_to_original_id = item['status']['idstr'] - item_type = "comment" - except KeyError: - # not a comment - pass - - # original tweet (if retweeted) - try: - item_original_id = item['retweeted_status']['idstr'] - item_original_content = item['retweeted_status']['text'] - item_original_author = item['retweeted_status']['user']['name'] - item_original_time = item['retweeted_status']['created_at'] - item_type = "retweet" - except KeyError: - # not retweeted - pass - - # thumb pic - try: - item_thumb_pic = None - item_thumb_pic = item['thumbnail_pic'] - except KeyError: - try: - item_thumb_pic = item['retweeted_status']['thumbnail_pic'] - except KeyError: - pass - - # tweet - tweet = TweetItem(type=item_type, id=item_id, author=item_author, - avatar=item_author_avatar, content=item_content, - time=item_content_time) - - if item_type == "comment": - # comment - tweet = TweetItem(type=item_type, id=item_id, - author=item_author, - avatar=item_author_avatar, - content=item_content, time=item_content_time, - original_id=item_comment_to_original_id) - - if item_type == "retweet": - # retweet - tweet = TweetItem(type=item_type, id=item_id, - author=item_author, - avatar=item_author_avatar, - content=item_content, time=item_content_time, - original_id=item_original_id, - original_content=item_original_content, - original_author=item_original_author, - original_time=item_original_time) - - if not item_thumb_pic is None: - # thumb pic - tweet.thumbnail_pic = item_thumb_pic - - model.appendRow(tweet) - self.timelineLoaded.emit(more) - - def get_all_timeline(self, page=1, reset_remind=False, more=False): - all_timelines = self.client.statuses.home_timeline.get( - page=page).statuses - threading.Thread(group=None, target=self.get_timeline, - args=(all_timelines, self.all_timeline, more)).start() - self.all_timeline_page = page - if reset_remind: - self.tabWidget.setTabText(0, self.tr("Weibo")) - - def get_my_timeline(self, page=1, reset_remind=False, more=False): - my_timelines = self.client.statuses.user_timeline.get( - page=page).statuses - threading.Thread(group=None, target=self.get_timeline, - args=(my_timelines, self.my_timeline, more)).start() - self.my_timeline_page = page - - def get_mentions_timeline(self, page=1, reset_remind=False, more=False): - mentions_timelines = self.client.statuses.mentions.get( - page=page).statuses - threading.Thread(group=None, target=self.get_timeline, - args=( - mentions_timelines, self.mentions, more)).start() - self.mentions_page = page - if reset_remind: - self.client.remind.set_count.post(type="mention_status") - self.tabWidget.setTabText(1, self.tr("@ME")) - - def get_comment_to_me(self, page=1, reset_remind=False, more=False): - comments_to_me = self.client.comments.to_me.get(page=page).comments - threading.Thread(group=None, target=self.get_timeline, args=( - comments_to_me, self.comment_to_me, more)).start() - self.comment_to_me_page = page - if reset_remind: - self.client.remind.set_count.post(type="cmt") - self.tabWidget.setTabText(2, self.tr("Comments")) - - def get_remind(self, uid): - '''this function is used to get unread_count - from Weibo API. uid is necessary.''' - - reminds = self.client.remind.unread_count.get(uid=uid) - return reminds - - def get_uid(self): - '''How can I get my uid? here it is''' - try: - self.uid = self.client.account.get_uid.get().uid - except AttributeError: - return None - - def show_notify(self): - # This function is run in another thread by WTimer. - # Do not modify UI directly. Send signal and react it in a slot only. - # We use SIGNAL self.tabTextChanged and SLOT self.setTabText() - # to display unread count - - reminds = self.get_remind(self.uid) - msg = "You have:\n" - num_msg = 0 - - if reminds['status'] != 0: - # Note: do NOT send notify here, or users will crazy. - self.tabTextChanged.emit(0, self.tr("Weibo(%d)") - % reminds['status']) - - if reminds['mention_status'] and self.remindMentions: - msg += "%d unread @ME\n" % reminds['mention_status'] - self.tabTextChanged.emit(1, self.tr("@Me(%d)") - % reminds['mention_status']) - num_msg += 1 - - if reminds['cmt'] and self.remindComments: - msg += "%d unread comment(s)\n" % reminds['cmt'] - self.tabTextChanged.emit(2, self.tr("Comments(%d)" ) - % reminds['cmt']) - num_msg += 1 - - if num_msg != 0: - self.notify.showMessage(self.tr("WeCase"), msg) - - def setTabText(self, index, string): - self.tabWidget.setTabText(index, string) +from LoginWindow import LoginWindow +import const - def moveToTop(self, more): - if more: - self.get_current_tweetView().rootObject().positionViewAtBeginning() - def setLoaded(self, tweetid): - self.get_current_tweetView().rootObject().imageLoaded(tweetid) - - def showSettings(self): - wecase_settings = WeSettingsWindow() - if wecase_settings.exec_(): - self.loadConfig() - self.applyConfig() - - def showAbout(self): - wecase_about = AboutWindow() - wecase_about.exec_() - - def logout(self): - wecase_login.exec_() - self.close() - - def postTweet(self): - wecase_new = NewpostWindow() - wecase_new.client = self.client - wecase_new.exec_() - - @QtCore.pyqtSlot(str) - def comment(self, idstr): - wecase_new = NewpostWindow(action="comment", id=int(idstr)) - wecase_new.client = self.client - wecase_new.exec_() - - @QtCore.pyqtSlot(str, str) - def repost(self, idstr, text): - wecase_new = NewpostWindow(action="retweet", id=int(idstr), text=text) - wecase_new.client = self.client - wecase_new.exec_() - - @QtCore.pyqtSlot(str, result=int) - def favorite(self, idstr): - try: - self.client.favorites.create.post(id=int(idstr)) - return True - except: - return False - - @QtCore.pyqtSlot(str, result=bool) - def un_favorite(self, idstr): - try: - self.client.favorites.destroy.post(id=int(idstr)) - return True - except: - return False - - @QtCore.pyqtSlot(str, str) - def reply(self, idstr, cidstr): - wecase_new = NewpostWindow(action="reply", id=int(idstr), - cid=int(cidstr)) - wecase_new.client = self.client - wecase_new.exec_() - - @QtCore.pyqtSlot(str, str) - def look_orignal_pic(self, thumbnail_pic, tweetid): - threading.Thread(group=None, target=self.fetch_open_original_pic, - args=(thumbnail_pic, tweetid)).start() - - def fetch_open_original_pic(self, thumbnail_pic, tweetid): - """Fetch and open original pic from thumbnail pic url. - Pictures will stored in cache directory. If we already have a same - name in cache directory, just open it. If we don't, then download it - first.""" - # XXX: This function is NOT thread-safe! - # Click a single picture for many time will download a image for many - # times, and the picture may be overwrite, we will get a broken image. - - original_pic = thumbnail_pic.replace("thumbnail", - "large") # A simple trick ... ^_^ - localfile = cache_path + original_pic.split("/")[-1] - if not os.path.exists(localfile): - urllib.request.urlretrieve(original_pic, localfile) - - os.popen("xdg-open " + localfile) # xdg-open is common? - self.imageLoaded.emit(tweetid) - - def refresh(self): - model = self.get_current_model() - get_timeline = self.get_current_function() - - model.clear() - threading.Thread(group=None, target=get_timeline, - args=(1, True, True)).start() - - def get_current_tweetView(self): - tweetViews = {0: self.homeView, 1: self.mentionsView, - 2: self.commentsView, 3: self.myView} - return tweetViews[self.tabWidget.currentIndex()] - - def get_current_model(self): - models = {0: self.all_timeline, 1: self.mentions, - 2: self.comment_to_me, - 3: self.my_timeline} - return models[self.tabWidget.currentIndex()] - - def get_current_function(self): - functions = {0: self.get_all_timeline, 1: self.get_mentions_timeline, - 2: self.get_comment_to_me, 3: self.get_my_timeline} - return functions[self.tabWidget.currentIndex()] - - def closeEvent(self, event): - self.timer.stop_event.set() - - -class WeSettingsWindow(QtGui.QDialog, Ui_SettingWindow): - def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) - self.setupUi(self) - self.loadConfig() - - def transformInterval(self, sliderValue): - return (sliderValue // 60, sliderValue % 60) - - def setIntervalText(self, sliderValue): - self.intervalLabel.setText(self.tr("%i min %i sec") % - (self.transformInterval(sliderValue))) - - def setTimeoutText(self, sliderValue): - self.timeoutLabel.setText(self.tr("%i sec") % sliderValue) - - def loadConfig(self): - self.config = ConfigParser() - self.config.read(config_path) - - if not self.config.has_section('main'): - self.config['main'] = {} - - self.main_config = self.config['main'] - self.intervalSlider.setValue(int(self.main_config.get( - 'notify_interval', "30"))) - self.setIntervalText(self.intervalSlider.value()) - self.timeoutSlider.setValue(int(self.main_config.get( - "notify_timeout", "5"))) - self.setTimeoutText(self.timeoutSlider.value()) - self.commentsChk.setChecked(self.main_config.getboolean( - "remind_comments", True)) - self.mentionsChk.setChecked(self.main_config.getboolean( - "remind_mentions", True)) - - def saveConfig(self): - self.config = ConfigParser() - self.config.read(config_path) - - if not self.config.has_section('main'): - self.config['main'] = {} - - self.main_config = self.config['main'] - self.main_config['notify_interval'] = str(self.intervalSlider.value()) - self.main_config['notify_timeout'] = str(self.timeoutSlider.value()) - self.main_config['remind_comments'] = str(self.commentsChk.isChecked()) - self.main_config['remind_mentions'] = str(self.mentionsChk.isChecked()) - - with open(config_path, "w+") as config_file: - self.config.write(config_file) - - def accept(self): - self.saveConfig() - self.done(True) - - def reject(self): - self.done(False) - - -class NewpostWindow(QtGui.QDialog, Ui_NewPostWindow): - client = None - image = None - apiError = QtCore.pyqtSignal(str) - sendSuccessful = QtCore.pyqtSignal() - - def __init__(self, parent=None, action="new", id=None, cid=None, text=""): - QtGui.QDialog.__init__(self, parent) - self.action = action - self.id = id - self.cid = cid - self.setupUi(self) - self.textEdit.setText(text) - self.textEdit.callback = self.mentions_suggest - self.textEdit.mention_flag = "@" - self.checkChars() - self.notify = Notify(timeout=1) - - def setupMyUi(self): - if self.action == "new": - self.pushButton_send.clicked.connect(self.send_tweet) - - def mentions_suggest(self, text): - ret_users = [] - try: - print(text) - word = re.findall(r'@[-a-zA-Z0-9_\u4e00-\u9fa5]+', text)[-1].replace('@', '') - except IndexError: - return [] - if not word.strip(): - return [] - users = self.client.search.suggestions.at_users.get(q=word, type=0) - for user in users: - ret_users.append("@" + user['nickname']) - return ret_users - - def send(self): - self.pushButton_send.setEnabled(False) - if self.action == "new": - threading.Thread(group=None, target=self.new).start() - elif self.action == "retweet": - threading.Thread(group=None, target=self.retweet).start() - elif self.action == "comment": - threading.Thread(group=None, target=self.comment).start() - elif self.action == "reply": - threading.Thread(group=None, target=self.reply).start() - - def retweet(self): - text = str(self.textEdit.toPlainText()) - try: - self.client.statuses.repost.post(id=int(self.id), status=text) - self.notify.showMessage(self.tr("WeCase"), - self.tr("Retweet Success!")) - self.sendSuccessful.emit() - except APIError as e: - self.apiError.emit(str(e)) - return - - def comment(self): - text = str(self.textEdit.toPlainText()) - try: - self.client.comments.create.post(id=int(self.id), comment=text) - self.notify.showMessage(self.tr("WeCase"), - self.tr("Comment Success!")) - self.sendSuccessful.emit() - except APIError as e: - self.apiError.emit(str(e)) - return - - def reply(self): - text = str(self.textEdit.toPlainText()) - try: - self.client.comments.reply.post(id=int(self.id), cid=int(self.cid), - comment=text) - self.notify.showMessage(self.tr("WeCase"), - self.tr("Reply Success!")) - self.sendSuccessful.emit() - except APIError as e: - self.apiError.emit(str(e)) - return - - def new(self): - text = str(self.textEdit.toPlainText()) - - try: - if self.image: - self.client.statuses.upload.post(status=text, - pic=open(self.image, "rb")) - else: - self.client.statuses.update.post(status=text) - - self.notify.showMessage(self.tr("WeCase"), - self.tr("Tweet Success!")) - self.sendSuccessful.emit() - except APIError as e: - self.apiError.emit(str(e)) - return - - self.image = None - - def addImage(self): - ACCEPT_TYPE = self.tr("Images") + "(*.png *.jpg *.bmp *.gif)" - if self.image: - self.image = None - self.pushButton_picture.setText(self.tr("Picture")) - else: - self.image = QtGui.QFileDialog.getOpenFileName(self, - self.tr("Choose a " - "image"), - filter=ACCEPT_TYPE) - self.pushButton_picture.setText(self.tr("Remove the picture")) - - def showError(self, e): - if "Text too long" in e: - QtGui.QMessageBox.warning(None, self.tr("Text too long!"), - self.tr("Please remove some text.")) - else: - QtGui.QMessageBox.warning(None, self.tr("Unknown error!"), e) - self.pushButton_send.setEnabled(True) - - def showSmiley(self): - wecase_smiley = SmileyWindow() - if wecase_smiley.exec_(): - self.textEdit.textCursor().insertText(wecase_smiley.smileyName) - - def checkChars(self): - '''Check textEdit's characters. - If it larger than 140, Send Button will be disabled - and label will show red chars.''' - - text = self.textEdit.toPlainText() - numLens = 140 - tweetLength(text) - if numLens == 140: - # you can not send empty tweet - self.pushButton_send.setEnabled(False) - elif numLens > 0: - # length is okay - self.label.setStyleSheet("color:black;") - self.pushButton_send.setEnabled(True) - else: - # text is too long - self.label.setStyleSheet("color:red;") - self.pushButton_send.setEnabled(False) - self.label.setText(str(numLens)) - - -class Notify(): - image = myself_path + "/ui/img/WeCase 80.png" - - def __init__(self, appname=QtCore.QObject().tr("WeCase"), timeout=5): - pynotify.init(appname) - self.timeout = timeout - self.n = pynotify.Notification(appname) - - def showMessage(self, title, text): - self.n.update(title, text, self.image) - self.n.set_timeout(self.timeout * 1000) - self.n.show() - - -class AboutWindow(QtGui.QDialog, Ui_About_Dialog): - def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) - self.setupUi(self) - - -class SmileyWindow(QtGui.QDialog, Ui_SmileyWindow): - def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) - self.setupUi(self) - self.setupMyUi() - self.setupModels() - self.smileyName = "" - - def setupMyUi(self): - self.smileyView.setResizeMode(self.smileyView.SizeRootObjectToView) - - def setupModels(self): - self.smileyModel = SmileyModel(self) - self.smileyModel.init_smileies(myself_path + "./ui/img/smiley", - self.smileyModel, SmileyItem) - self.smileyView.rootContext().setContextProperty("SmileyModel", - self.smileyModel) - self.smileyView.rootContext().setContextProperty("parentWindow", self) - self.smileyView.setSource( - QtCore.QUrl.fromLocalFile(myself_path + "/ui/SmileyView.qml")) - - @QtCore.pyqtSlot(str) - def returnSmileyName(self, smileyName): - self.smileyName = smileyName - self.done(True) - - -if __name__ == "__main__": +def mkconfig(): try: - os.mkdir(config_path.replace("/config_db", "")) + os.mkdir(const.config_path.replace("/config_db", "")) except OSError: pass try: - os.mkdir(cache_path) + os.mkdir(const.cache_path) except OSError: pass + +if __name__ == "__main__": + mkconfig() + app = QtGui.QApplication(sys.argv) # Qt's built-in string translator @@ -813,7 +40,7 @@ def returnSmileyName(self, smileyName): # WeCase's own string translator my_translator = QtCore.QTranslator(app) my_translator.load("WeCase_" + QtCore.QLocale.system().name(), - myself_path + "locale") + const.myself_path + "locale") app.installTranslator(my_translator) wecase_login = LoginWindow() diff --git a/src/wecase_rc.py b/src/wecase_rc.py index 3b5dc55..081c32c 100644 --- a/src/wecase_rc.py +++ b/src/wecase_rc.py @@ -2,7 +2,7 @@ # Resource object code # -# Created: 周一 4月 8 16:40:01 2013 +# Created: 周日 4月 14 21:55:18 2013 # by: The Resource Compiler for PyQt (Qt v4.8.4) # # WARNING! All changes made in this file will be lost!