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
-
+
- 微盒
+ 微盒
@@ -87,25 +87,30 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY.
LoginWindow
-
+
认证失败!
-
+
- 检查您的账号和密码!
+ 检查您的账号和密码!
-
+
走起!
-
+
登录中,请稍候……
+
+
+
+ 检查您的账号,密码和网络连接!
+
NewPostWindow
@@ -115,22 +120,22 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY.
新消息
-
+
140
-
+
图片(&P)
-
+
取消(&C)
-
+
发送(&S)
@@ -140,35 +145,55 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY.
表情
-
+
表情(&M)
+
+
+
+ 同时:
+
+
+
+
+ 评论
+
+
+
+
+ 转发
+
+
+
+
+ 评论给原微博
+
NewpostWindow
-
+
微盒
-
+
转发成功!
-
+
评论成功!
-
+
回复成功!
-
+
发送成功!
@@ -178,37 +203,37 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY.
图片 (*.png *.jpg *.bmp *.gif)
-
+
图片
-
+
选择一张图片
-
+
移除图片
-
+
内容过长!
-
+
请删除一些文字。
-
+
未知错误!
-
+
图片
@@ -256,30 +281,38 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY.
评论
+
+ SmileyWindow
+
+
+
+ 选择一个表情
+
+
TweetItem
-
+
穿越时空!
-
+
%.0f 秒前
-
+
%.0f 分钟前
-
+
%.0f 小时前
-
+
%.0f 天前
@@ -287,52 +320,72 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY.
WeCaseWindow
-
+
微博(%d)
-
+
@我(%d)
-
+
评论(%d)
-
+
微盒
-
+
微博
-
+
- @我
+ @我
-
+
评论
+
+
+
+ 您有:
+
+
+
+
+ %d 条未读 @提醒
+
+
+
+
+ %d 条未读评论
+
+
+
+
+ @我
+
WeSettingsWindow
-
+
- %i 分 %i 秒
+ %i 分 %i 秒
-
+
- %i 秒
+ %i 秒
@@ -391,72 +444,72 @@ This software is provided AS IS, and comes with ABSOLUTE NO WARRANTY.
微博
-
+
@我
-
+
评论
-
+
我的微博
-
+
我(&M)
-
+
刷新(&R)
-
+
新微博(&N)
-
+
微盒(&W)
-
+
帮助(&H)
-
+
选项(&H)
-
+
关于(&A)...
-
+
注销(&L)
-
+
退出(&E)
-
+
设置(&S)
-
+
升级(&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!