├── .gitattributes ├── AcceptView.qml ├── ChatView.qml ├── Contacts.qml ├── FlatButton.qml ├── ImageButton.qml ├── LVChat.pro ├── LVChat.pro.user ├── LVChat.qm ├── LVChat.ts ├── LoginView.qml ├── accessPointModel.cpp ├── accessPointModel.h ├── android ├── AndroidManifest.xml └── res │ ├── drawable-hdpi │ └── icon.png │ ├── drawable-ldpi │ └── icon.png │ ├── drawable-mdpi │ └── icon.png │ └── values-zh-rCN │ └── strings.xml ├── audioplayer.cpp ├── audioplayer.h ├── audiorecorder.cpp ├── audiorecorder.h ├── deployment.pri ├── main.cpp ├── main.qml ├── messengerConnection.cpp ├── messengerConnection.h ├── messengerManager.cpp ├── messengerManager.h ├── pointSizeToPixelSize.cpp ├── pointSizeToPixelSize.h ├── protocol.h ├── qml.qrc ├── res ├── appicon.png ├── chat_from_bg_normal.9.png ├── chat_from_bg_pressed.9.png ├── chat_to_bg_normal.9.png ├── chat_to_bg_pressed.9.png ├── head_0.png ├── head_1.png ├── head_2.png ├── head_3.png ├── head_4.png ├── head_5.png ├── head_6.png ├── head_7.png ├── head_8.png ├── head_9.png ├── ic_accept.png ├── ic_back - 副本.png ├── ic_back.png ├── ic_back_pressed.png ├── ic_chat.png ├── ic_chat_pressed.png ├── ic_microphone.png ├── ic_ok.png ├── ic_ok_pressed.png ├── ic_reject.png ├── ic_search.png ├── ic_search_pressed.png ├── ic_voice.png └── liao.tsp ├── scaleableBorder.qml ├── scanner.cpp ├── scanner.h ├── screenshots ├── pc_init.png ├── pc_select_head.png ├── scaleableBorder.png ├── 豌豆荚截图20140923214402.png ├── 豌豆荚截图20140923214424.png ├── 豌豆荚截图20140923214450.png ├── 豌豆荚截图20140923214619.png ├── 豌豆荚截图20140923214651.png ├── 豌豆荚截图20140923214816.png ├── 豌豆荚截图20140923214838.png ├── 豌豆荚截图20140923214854.png ├── 豌豆荚截图20140923222600.png └── 豌豆荚截图20140923222705.png └── voiceMessage.h /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /AcceptView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | 3 | Rectangle { 4 | id: acceptView; 5 | anchors.fill: parent; 6 | implicitWidth: 320; 7 | implicitHeight: 240; 8 | color: "darkgray"; 9 | property alias text: content.text; 10 | signal accepted(); 11 | signal rejected(); 12 | 13 | Text { 14 | id: content; 15 | anchors.centerIn: parent; 16 | verticalAlignment: Text.AlignVCenter; 17 | horizontalAlignment: Text.AlignHCenter; 18 | font.pointSize: 24; 19 | height: 10 + fontUtil.height(24); 20 | wrapMode: Text.WordWrap; 21 | } 22 | 23 | Row { 24 | anchors.horizontalCenter: parent.horizontalCenter; 25 | anchors.bottom: parent.bottom; 26 | anchors.bottomMargin: 12; 27 | spacing: 8; 28 | 29 | FlatButton { 30 | iconWidth: 48; 31 | iconHeight: 48; 32 | width: 60 + fontUtil.width(20, text); 33 | height: Math.max(48, 8 + fontUtil.height(20)); 34 | iconSource: "res/ic_accept.png"; 35 | font.pointSize: 20; 36 | text: qsTr("Accept"); 37 | onClicked: acceptView.accepted(); 38 | } 39 | FlatButton{ 40 | iconWidth: 48; 41 | iconHeight: 48; 42 | width: 60 + fontUtil.width(20, text); 43 | height: Math.max(48, 8 + fontUtil.height(20)); 44 | iconSource: "res/ic_reject.png"; 45 | font.pointSize: 20; 46 | text: qsTr("Reject"); 47 | onClicked: acceptView.rejected(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ChatView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import QtQuick.Controls 1.2 3 | import QtQuick.Layouts 1.1 4 | import QtQuick.Controls.Styles 1.2 5 | import QtMultimedia 5.0 6 | 7 | Item { 8 | id: chat; 9 | anchors.fill: parent; 10 | signal finished(); 11 | objectName: "chat"; 12 | property alias nick: nickName.text; 13 | property int peerPortraitIndex: 9; 14 | property int portraitIndex: 9; 15 | Rectangle { 16 | id: title; 17 | width: parent.width; 18 | height: Math.max(48, 8 + fontUtil.height(20)); 19 | color: "#7378ba"; //"#203050"; 20 | ImageButton { 21 | id: back; 22 | anchors.left: parent.left; 23 | anchors.verticalCenter: parent.verticalCenter; 24 | anchors.margins: 4; 25 | width: 48; 26 | height: 48; 27 | z: 2; 28 | iconNormal: "res/ic_back.png"; 29 | iconPressed: "res/ic_back_pressed.png"; 30 | onClicked: chat.finished(); 31 | } 32 | Text { 33 | id: nickName; 34 | anchors.left: back.right; 35 | anchors.leftMargin: 6; 36 | anchors.right: parent.right; 37 | anchors.verticalCenter: back.verticalCenter; 38 | verticalAlignment: Text.AlignVCenter; 39 | font.pointSize: 20; 40 | color: "white"; 41 | } 42 | } 43 | 44 | TextField { 45 | id: recordState; 46 | width: parent.width - 40; 47 | height: 20 + fontUtil.height(24); 48 | anchors.centerIn: parent; 49 | z: 10; 50 | readOnly: true; 51 | visible: false; 52 | horizontalAlignment: TextInput.AlignHCenter; 53 | verticalAlignment: TextInput.AlignVCenter; 54 | style: TextFieldStyle { 55 | textColor: "red"; 56 | background: Rectangle { 57 | color: "lightgray"; 58 | radius: 2 59 | border.color: "#444444"; 60 | border.width: 1 61 | } 62 | } 63 | 64 | function onDurationChanged(duration){ 65 | text = "%1\"".arg(Math.ceil(duration/1000)); 66 | } 67 | states:[ 68 | State { 69 | name: "hide"; 70 | PropertyChanges{ 71 | target: recordState; 72 | restoreEntryValues: false; 73 | opacity: 0; 74 | } 75 | }, 76 | State { 77 | name: "show"; 78 | PropertyChanges{ 79 | target: recordState; 80 | restoreEntryValues: false; 81 | opacity: 1; 82 | visible: true; 83 | } 84 | } 85 | ] 86 | state: "hide"; 87 | transitions: [ 88 | Transition { 89 | from: "hide"; 90 | to: "show"; 91 | NumberAnimation { 92 | property: "opacity"; 93 | duration: 800; 94 | } 95 | }, 96 | Transition { 97 | from: "show"; 98 | to: "hide"; 99 | SequentialAnimation { 100 | NumberAnimation { 101 | properties: "opacity"; 102 | duration: 1500; 103 | } 104 | 105 | PropertyAction { 106 | target: recordState; 107 | property: "visible"; 108 | value: false; 109 | } 110 | } 111 | } 112 | ] 113 | } 114 | 115 | Connections { 116 | target: audioRecorder; 117 | onDurationChanged: recordState.onDurationChanged(duration); 118 | } 119 | 120 | Rectangle { 121 | id: pressTalk; 122 | anchors.horizontalCenter: parent.horizontalCenter; 123 | anchors.bottom: parent.bottom; 124 | anchors.bottomMargin: 4; 125 | property string text: qsTr("Press & Talk"); 126 | property bool pressed: false; 127 | height: Math.max(8 + fontUtil.height(20), 50); 128 | width: 64 + fontUtil.width(20, text); 129 | border.color: "gray"; 130 | border.width: 1; 131 | radius: 4; 132 | color: pressTalk.pressed ? "lightgray" : "gray"; 133 | Image { 134 | id: microPhone; 135 | anchors.left: parent.left; 136 | anchors.leftMargin: 4; 137 | anchors.verticalCenter: parent.verticalCenter; 138 | width: 48; 139 | height: 48; 140 | source: "res/ic_microphone.png"; 141 | } 142 | Text{ 143 | anchors.left: microPhone.right; 144 | anchors.leftMargin: 2; 145 | anchors.right: parent.right; 146 | height: parent.height; 147 | font.pointSize: 20; 148 | text: parent.text; 149 | color: parent.pressed ? "blue" : "black"; 150 | verticalAlignment: Text.AlignVCenter; 151 | horizontalAlignment: Text.AlignHCenter; 152 | } 153 | 154 | MouseArea { 155 | anchors.fill: parent; 156 | onPressed: { 157 | pressTalk.pressed = true; 158 | audioRecorder.record(); 159 | recordState.visible = true; 160 | recordState.text = qsTr("Recording..."); 161 | recordState.state = "show"; 162 | } 163 | onReleased: { 164 | pressTalk.pressed = false; 165 | audioRecorder.stop(); 166 | if(audioRecorder.success()){ 167 | chatList.model.append( 168 | { 169 | "direction": 0, 170 | "audioFile": audioRecorder.file, 171 | "duration": Math.round(audioRecorder.duration/1000) 172 | }); 173 | console.log("recorded, ", audioRecorder.file); 174 | msgManager.sendVoiceMessage(audioRecorder.file, audioRecorder.duration); 175 | } 176 | recordState.state = "hide"; 177 | } 178 | } 179 | 180 | z: 2; 181 | } 182 | 183 | MediaPlayer { 184 | id: player; 185 | } 186 | 187 | ListView { 188 | id: chatList; 189 | anchors.left: parent.left; 190 | anchors.right: parent.right; 191 | anchors.top: title.bottom; 192 | anchors.bottom: pressTalk.top; 193 | anchors.margins: 4; 194 | clip: true; 195 | spacing: 6; 196 | model: ListModel {} 197 | delegate: Item{ 198 | width: parent.width; 199 | height: Math.max(52, 8 + fontUtil.height(20)); 200 | Image { 201 | id: headPortrait; 202 | source: "res/head_%1.png".arg(direction == 0 ? chat.portraitIndex : chat.peerPortraitIndex); 203 | width: 48; 204 | height: 48; 205 | x: direction == 0 ? parent.width - width -2 : 2; 206 | y: 2; 207 | } 208 | 209 | BorderImage { 210 | id: msgWrapper; 211 | source: direction == 0 ? (pressed ? "res/chat_to_bg_pressed.9.png":"res/chat_to_bg_normal.9.png") : (pressed ? "res/chat_from_bg_pressed.9.png":"res/chat_from_bg_normal.9.png"); 212 | width: 140; //TODO: caculate via PPI 213 | height: parent.height; 214 | x: direction == 0 ? headPortrait.x - 2 - width : headPortrait.x + headPortrait.width + 2; 215 | border.left: direction == 0 ? 6 : 16; 216 | border.top: 38; 217 | border.right: direction == 0 ? 16 : 6; 218 | border.bottom: 6; 219 | property bool pressed: false; 220 | property string filePath: audioFile; 221 | Image { 222 | id: voice; 223 | source: "res/ic_voice.png"; 224 | mirror: direction == 0; 225 | width: 45; 226 | height: 45; 227 | x: direction == 0 ? parent.width - width - 2 - msgWrapper.border.right : 2 + msgWrapper.border.left; 228 | y: (parent.height - height)/2; 229 | } 230 | 231 | Text { 232 | x: direction == 0 ? 2 : voice.x + voice.width + 4; 233 | y: voice.y; 234 | height: voice.height; 235 | width: parent.width - 24 - voice.width; 236 | text: "%1\"".arg(duration); 237 | color: "black"; 238 | horizontalAlignment: direction == 0 ? Text.AlignRight : Text.AlignLeft; 239 | verticalAlignment: Text.AlignVCenter; 240 | font.pointSize: 16; 241 | } 242 | 243 | MouseArea { 244 | anchors.fill: parent; 245 | preventStealing: true; 246 | onPressed: msgWrapper.pressed = true; 247 | onReleased: msgWrapper.pressed = false; 248 | onClicked: { 249 | console.log("audioFile-", msgWrapper.filePath); 250 | player.source = (Qt.platform.os === "windows" ? "file:///" : "file://") + msgWrapper.filePath; 251 | player.play(); 252 | } 253 | } 254 | } 255 | } 256 | } 257 | 258 | Connections { 259 | target: msgManager; 260 | onVoiceMessageArrived:{ 261 | chatList.model.append( 262 | { 263 | "direction": 1, 264 | "audioFile": fileName, 265 | "duration": Math.round(duration / 1000) 266 | }); 267 | } 268 | } 269 | 270 | function changePeer(nickName, portraitIndex){ 271 | nick = nickName; 272 | peerPortraitIndex = portraitIndex; 273 | chatList.model.clear(); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /Contacts.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import QtQuick.Controls 1.2 3 | import QtQuick.Layouts 1.1 4 | 5 | Item { 6 | id: contacts; 7 | anchors.fill: parent; 8 | signal talkTo(int peerIndex); 9 | signal startScan(); 10 | objectName: "contacts"; 11 | function scanAccessPoint(){ 12 | startScan(); 13 | scanButton.enabled = false; 14 | apmodel.scan(); 15 | } 16 | 17 | Rectangle { 18 | id: title; 19 | width: parent.width; 20 | height: 48; 21 | color: "#7378ba"; 22 | ImageButton { 23 | id: scanButton; 24 | anchors.centerIn: parent; 25 | width: 48; 26 | height: 48; 27 | z: 2; 28 | iconNormal: "res/ic_search.png"; 29 | iconPressed: "res/ic_search_pressed.png"; 30 | onClicked: scanAccessPoint(); 31 | } 32 | } 33 | 34 | ListView { 35 | id: peoples; 36 | anchors.left: parent.left; 37 | anchors.right: parent.right; 38 | anchors.top: title.bottom; 39 | anchors.bottom: requestTalk.top; 40 | anchors.margins: 4; 41 | clip: true; 42 | spacing: 6; 43 | highlight: Rectangle { 44 | width: peoples.width; 45 | color: "lightsteelblue"; 46 | } 47 | 48 | model: apmodel; 49 | delegate: Item{ 50 | id: wrapper; 51 | width: parent.width; 52 | height: Math.max(52, 8 + fontUtil.height(20)); 53 | Image { 54 | id: headPortrait; 55 | source: "res/head_%1.png".arg(portraitIndex); 56 | width: 48; 57 | height: 48; 58 | x: 2; 59 | y: 2; 60 | } 61 | 62 | Text { 63 | id: nick; 64 | font.pointSize: 20; 65 | anchors.left: headPortrait.right; 66 | anchors.leftMargin: 4; 67 | anchors.verticalCenter: headPortrait.verticalCenter; 68 | text: nickName; 69 | } 70 | 71 | MouseArea { 72 | anchors.fill: parent; 73 | onClicked: { 74 | wrapper.ListView.view.currentIndex = index; 75 | } 76 | } 77 | } 78 | } 79 | 80 | Rectangle { 81 | id: requestTalk; 82 | width: parent.width; 83 | height: 64; 84 | anchors.bottom: parent.bottom; 85 | color: "gray"; //"#605550"; 86 | ImageButton { 87 | anchors.centerIn: parent; 88 | width: 60; 89 | height: 64; 90 | z: 2; 91 | iconNormal: "res/ic_chat.png"; 92 | iconPressed: "res/ic_chat_pressed.png"; 93 | onClicked: { 94 | var cur = peoples.currentIndex; 95 | if(cur >= 0){ 96 | contacts.talkTo(cur); 97 | } 98 | } 99 | } 100 | } 101 | 102 | Connections { 103 | target: apmodel; 104 | onScanFinished:{ 105 | console.log("scanFinished"); 106 | scanButton.enabled = true; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /FlatButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | 3 | Rectangle { 4 | id: bkgnd; 5 | implicitWidth: 120; 6 | implicitHeight: 50; 7 | color: "transparent"; 8 | property alias iconSource: icon.source; 9 | property alias iconWidth: icon.width; 10 | property alias iconHeight: icon.height; 11 | property alias textColor: btnText.color; 12 | property alias font: btnText.font; 13 | property alias text: btnText.text; 14 | radius: 6; 15 | property bool hovered: false; 16 | border.color: "lightgray"; 17 | border.width: hovered ? 2 : 1; 18 | signal clicked; 19 | 20 | Image { 21 | id: icon; 22 | anchors.left: parent.left; 23 | anchors.verticalCenter: parent.verticalCenter; 24 | } 25 | Text { 26 | id: btnText; 27 | anchors.left: icon.right; 28 | anchors.verticalCenter: icon.verticalCenter; 29 | anchors.margins: 4; 30 | color: ma.pressed ? "blue" : (parent.hovered ? "#0000a0" : "black"); 31 | } 32 | MouseArea { 33 | id: ma; 34 | anchors.fill: parent; 35 | hoverEnabled: true; 36 | onEntered: { 37 | bkgnd.hovered = true; 38 | } 39 | onExited: { 40 | bkgnd.hovered = false; 41 | } 42 | onClicked: { 43 | bkgnd.hovered = false; 44 | bkgnd.clicked(); 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /ImageButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | 3 | Rectangle { 4 | id: btn; 5 | implicitWidth: 48; 6 | implicitHeight: 48; 7 | color: "transparent"; 8 | property var iconNormal; 9 | property var iconPressed; 10 | signal clicked(); 11 | Image { 12 | id: icon; 13 | anchors.fill: parent; 14 | anchors.margins: 1; 15 | fillMode: Image.PreserveAspectFit; 16 | } 17 | 18 | onIconNormalChanged: icon.source = iconNormal; 19 | 20 | MouseArea { 21 | anchors.fill: parent; 22 | onPressed: { 23 | if(btn.iconPressed != undefined){ 24 | icon.source = iconPressed; 25 | }else{ 26 | border.width = 1; 27 | border.color = "gray"; 28 | } 29 | } 30 | onReleased: { 31 | if(btn.iconPressed == undefined){ 32 | border.width = 0; 33 | border.color = "gray"; 34 | } 35 | icon.source = iconNormal; 36 | } 37 | onClicked: btn.clicked(); 38 | } 39 | 40 | Component.onCompleted: { 41 | icon.source = iconNormal; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LVChat.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | 3 | QT += qml quick network multimedia 4 | 5 | SOURCES += main.cpp \ 6 | audiorecorder.cpp \ 7 | messengerConnection.cpp \ 8 | scanner.cpp \ 9 | accessPointModel.cpp \ 10 | pointSizeToPixelSize.cpp \ 11 | ../stockMonitor/qDebug2Logcat.cpp \ 12 | messengerManager.cpp 13 | 14 | RESOURCES += qml.qrc 15 | 16 | # Additional import path used to resolve QML modules in Qt Creator's code model 17 | QML_IMPORT_PATH = 18 | 19 | # Default rules for deployment. 20 | include(deployment.pri) 21 | 22 | OTHER_FILES += \ 23 | android/AndroidManifest.xml 24 | 25 | HEADERS += \ 26 | audiorecorder.h \ 27 | messengerConnection.h \ 28 | protocol.h \ 29 | scanner.h \ 30 | voiceMessage.h \ 31 | accessPointModel.h \ 32 | pointSizeToPixelSize.h \ 33 | ../stockMonitor/qDebug2Logcat.h \ 34 | messengerManager.h 35 | 36 | ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android 37 | -------------------------------------------------------------------------------- /LVChat.pro.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ProjectExplorer.Project.ActiveTarget 7 | 0 8 | 9 | 10 | ProjectExplorer.Project.EditorSettings 11 | 12 | true 13 | false 14 | true 15 | 16 | Cpp 17 | 18 | CppGlobal 19 | 20 | 21 | 22 | QmlJS 23 | 24 | QmlJSGlobal 25 | 26 | 27 | 2 28 | UTF-8 29 | false 30 | 4 31 | false 32 | 80 33 | true 34 | true 35 | 1 36 | true 37 | false 38 | 0 39 | true 40 | 0 41 | 8 42 | true 43 | 1 44 | true 45 | true 46 | true 47 | false 48 | 49 | 50 | 51 | ProjectExplorer.Project.PluginSettings 52 | 53 | 54 | 55 | ProjectExplorer.Project.Target.0 56 | 57 | Desktop Qt 5.3 MinGW 32bit 58 | Desktop Qt 5.3 MinGW 32bit 59 | qt.53.win32_mingw482_kit 60 | 0 61 | 0 62 | 0 63 | 64 | D:/projects/build-LVChat-Desktop_Qt_5_3_MinGW_32bit-Debug 65 | 66 | 67 | true 68 | qmake 69 | 70 | QtProjectManager.QMakeBuildStep 71 | false 72 | true 73 | 74 | false 75 | 76 | 77 | true 78 | Make 79 | 80 | Qt4ProjectManager.MakeStep 81 | 82 | false 83 | 84 | 85 | 86 | 2 87 | 构建 88 | 89 | ProjectExplorer.BuildSteps.Build 90 | 91 | 92 | 93 | true 94 | Make 95 | 96 | Qt4ProjectManager.MakeStep 97 | 98 | true 99 | clean 100 | 101 | 102 | 1 103 | 清理 104 | 105 | ProjectExplorer.BuildSteps.Clean 106 | 107 | 2 108 | false 109 | 110 | Debug 111 | 112 | Qt4ProjectManager.Qt4BuildConfiguration 113 | 2 114 | true 115 | 116 | 117 | D:/projects/build-LVChat-Desktop_Qt_5_3_MinGW_32bit-Release 118 | 119 | 120 | true 121 | qmake 122 | 123 | QtProjectManager.QMakeBuildStep 124 | false 125 | true 126 | 127 | false 128 | 129 | 130 | true 131 | Make 132 | 133 | Qt4ProjectManager.MakeStep 134 | 135 | false 136 | 137 | 138 | 139 | 2 140 | 构建 141 | 142 | ProjectExplorer.BuildSteps.Build 143 | 144 | 145 | 146 | true 147 | Make 148 | 149 | Qt4ProjectManager.MakeStep 150 | 151 | true 152 | clean 153 | 154 | 155 | 1 156 | 清理 157 | 158 | ProjectExplorer.BuildSteps.Clean 159 | 160 | 2 161 | false 162 | 163 | Release 164 | 165 | Qt4ProjectManager.Qt4BuildConfiguration 166 | 0 167 | true 168 | 169 | 2 170 | 171 | 172 | 0 173 | 部署 174 | 175 | ProjectExplorer.BuildSteps.Deploy 176 | 177 | 1 178 | 在本地部署 179 | 180 | ProjectExplorer.DefaultDeployConfiguration 181 | 182 | 1 183 | 184 | 185 | 186 | false 187 | false 188 | false 189 | false 190 | true 191 | 0.01 192 | 10 193 | true 194 | 1 195 | 25 196 | 197 | 1 198 | true 199 | false 200 | true 201 | valgrind 202 | 203 | 0 204 | 1 205 | 2 206 | 3 207 | 4 208 | 5 209 | 6 210 | 7 211 | 8 212 | 9 213 | 10 214 | 11 215 | 12 216 | 13 217 | 14 218 | 219 | 2 220 | 221 | LVChat 222 | 223 | Qt4ProjectManager.Qt4RunConfiguration:D:/projects/LVChat/LVChat.pro 224 | 225 | LVChat.pro 226 | false 227 | false 228 | 229 | 3768 230 | false 231 | true 232 | false 233 | false 234 | true 235 | 236 | 1 237 | 238 | 239 | 240 | ProjectExplorer.Project.Target.1 241 | 242 | Android for armeabi-v7a (GCC 4.7, Qt 5.3.1) 243 | Android for armeabi-v7a (GCC 4.7, Qt 5.3.1) 244 | {2a41e2ec-b5ed-4084-bb4e-fa614a1a62c4} 245 | 0 246 | 0 247 | 0 248 | 249 | D:/projects/build-LVChat-Android_for_armeabi_v7a_GCC_4_7_Qt_5_3_1-Release 250 | 251 | 252 | true 253 | qmake 254 | 255 | QtProjectManager.QMakeBuildStep 256 | false 257 | true 258 | 259 | false 260 | 261 | 262 | true 263 | Make 264 | 265 | Qt4ProjectManager.MakeStep 266 | 267 | -w 268 | -r 269 | 270 | false 271 | 272 | 273 | 274 | 2 275 | 构建 276 | 277 | ProjectExplorer.BuildSteps.Build 278 | 279 | 280 | 281 | true 282 | Make 283 | 284 | Qt4ProjectManager.MakeStep 285 | 286 | -w 287 | -r 288 | 289 | true 290 | clean 291 | 292 | 293 | 1 294 | 清理 295 | 296 | ProjectExplorer.BuildSteps.Clean 297 | 298 | 2 299 | false 300 | 301 | Release 302 | 303 | Qt4ProjectManager.Qt4BuildConfiguration 304 | 0 305 | true 306 | 307 | 1 308 | 309 | 310 | 311 | 1 312 | true 313 | 复制应用程序的数据 314 | 315 | Qt4ProjectManager.AndroidPackageInstallationStep 316 | 317 | 318 | android-17 319 | 320 | D:/projects/LVChat/LVChat.pro 321 | true 322 | Deploy to Android device 323 | 324 | Qt4ProjectManager.AndroidDeployQtStep 325 | 1 326 | false 327 | false 328 | 329 | 2 330 | 部署 331 | 332 | ProjectExplorer.BuildSteps.Deploy 333 | 334 | 1 335 | 部署到Android设备 336 | 部署到Android设备 337 | Qt4ProjectManager.AndroidDeployConfiguration2 338 | 339 | 1 340 | 341 | 342 | 343 | false 344 | false 345 | false 346 | false 347 | true 348 | 0.01 349 | 10 350 | true 351 | 1 352 | 25 353 | 354 | 1 355 | true 356 | false 357 | true 358 | valgrind 359 | 360 | 0 361 | 1 362 | 2 363 | 3 364 | 4 365 | 5 366 | 6 367 | 7 368 | 8 369 | 9 370 | 10 371 | 11 372 | 12 373 | 13 374 | 14 375 | 376 | 在Android设备上运行 377 | 378 | Qt4ProjectManager.AndroidRunConfiguration:D:/projects/LVChat/LVChat.pro 379 | LVChat.pro 380 | 3768 381 | false 382 | true 383 | false 384 | false 385 | true 386 | 387 | 1 388 | 389 | 390 | 391 | ProjectExplorer.Project.TargetCount 392 | 2 393 | 394 | 395 | ProjectExplorer.Project.Updater.EnvironmentId 396 | {6730cd59-2fb0-4d8d-9548-39afc1844ae8} 397 | 398 | 399 | ProjectExplorer.Project.Updater.FileVersion 400 | 15 401 | 402 | 403 | -------------------------------------------------------------------------------- /LVChat.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/LVChat.qm -------------------------------------------------------------------------------- /LVChat.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AcceptView 6 | 7 | 8 | Accept 9 | 接受 10 | 11 | 12 | 13 | Reject 14 | 拒绝 15 | 16 | 17 | 18 | ChatView 19 | 20 | 21 | Press & Talk 22 | 按住说话 23 | 24 | 25 | 26 | Recording... 27 | 录音中…… 28 | 29 | 30 | 31 | main 32 | 33 | 34 | LVChat 35 | 聊哈 36 | 37 | 38 | 39 | want to talk with you. 40 | want to talk with you. 41 | 想和你聊两句。 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /LoginView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import QtQuick.Controls 1.2 3 | import QtQuick.Layouts 1.1 4 | 5 | Item { 6 | id: login; 7 | anchors.fill: parent; 8 | anchors.margins: 10; 9 | implicitWidth: 300; 10 | implicitHeight: 102; 11 | signal go(string nick, int portraitIndex); 12 | property int portraitIndex: 9; 13 | RowLayout { 14 | anchors.fill: parent; 15 | spacing: 2; 16 | ImageButton { 17 | id: headPortrait; 18 | width: 48; 19 | height: 48; 20 | Layout.alignment: Qt.AlignVCenter; 21 | iconNormal: "res/head_9.png"; 22 | function onHeadPortraitSelected(index){ 23 | login.portraitIndex = index; 24 | iconNormal = "res/head_%1.png".arg(index); 25 | } 26 | onClicked: login.showHeadPortraits(onHeadPortraitSelected); 27 | } 28 | Rectangle { 29 | border.width: 1; 30 | border.color: "gray"; 31 | Layout.fillWidth: true; 32 | Layout.alignment: Qt.AlignVCenter; 33 | height: 12 + fontUtil.height(22); 34 | color: "transparent"; 35 | TextInput { 36 | anchors.margins: 2; 37 | anchors.fill: parent; 38 | id: nickEditor; 39 | text: "nickname"; 40 | font.pointSize: 22; 41 | verticalAlignment: TextInput.AlignVCenter; 42 | } 43 | } 44 | ImageButton { 45 | width: 48; 46 | height: 48; 47 | Layout.alignment: Qt.AlignVCenter; 48 | iconNormal: "res/ic_ok.png"; 49 | iconPressed: "res/ic_ok_pressed.png"; 50 | onClicked: login.go(nickEditor.text, login.portraitIndex); 51 | } 52 | } 53 | 54 | property var selector: null; 55 | function showHeadPortraits(fn){ 56 | selector = headComp.createObject(login, 57 | { 58 | "z":2, 59 | "callback":fn, 60 | "width": width 61 | }); 62 | selector.selected.connect(onHeadPanelClosed); 63 | } 64 | function onHeadPanelClosed(){ 65 | selector.destroy(); 66 | selector = null; 67 | } 68 | 69 | Component { 70 | id: headComp; 71 | Rectangle { 72 | id: wrapper; 73 | property var callback; 74 | signal selected(); 75 | anchors.centerIn: parent; 76 | implicitWidth: 260; 77 | implicitHeight: 104; 78 | color: "lightgray"; 79 | border.width: 1; 80 | border.color: "gray"; 81 | radius: 4; 82 | Grid { 83 | id: headPortraits; 84 | anchors.centerIn: parent; 85 | anchors.margins: 2; 86 | columns: 5; 87 | columnSpacing: 2; 88 | rows: 2; 89 | rowSpacing: 2; 90 | 91 | Repeater { 92 | delegate: ImageButton{ 93 | width: 48; 94 | height: 48; 95 | iconNormal: "res/head_%1.png".arg(index); 96 | onClicked: { 97 | wrapper.callback(index); 98 | wrapper.selected(); 99 | wrapper.visible = false; 100 | } 101 | } 102 | model: 10; 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /accessPointModel.cpp: -------------------------------------------------------------------------------- 1 | #include "accessPointModel.h" 2 | #include 3 | #include "scanner.h" 4 | #include "messengerConnection.h" 5 | #include 6 | #include "messengerManager.h" 7 | 8 | typedef QVector VideoData; 9 | class AccessPointModelPrivate 10 | { 11 | public: 12 | AccessPointModelPrivate() 13 | : m_current(0) 14 | { 15 | int role = Qt::UserRole; 16 | m_roleNames.insert(role++, "nickName"); 17 | m_roleNames.insert(role++, "portraitIndex"); 18 | m_roleNames.insert(role++, "address"); 19 | } 20 | 21 | ~AccessPointModelPrivate() 22 | { 23 | clear(); 24 | } 25 | 26 | void clear() 27 | { 28 | int count = m_accessPoints.size(); 29 | if(count > 0) 30 | { 31 | for(int i = 0; i < count; i++) 32 | { 33 | delete m_accessPoints.at(i); 34 | } 35 | m_accessPoints.clear(); 36 | } 37 | } 38 | 39 | QHash m_roleNames; 40 | QVector m_accessPoints; 41 | AccessPointScanner m_scanner; 42 | int m_current; 43 | }; 44 | 45 | AccessPointModel::AccessPointModel(QObject *parent) 46 | : QAbstractListModel(parent) 47 | , m_dptr(new AccessPointModelPrivate) 48 | { 49 | connect(&(m_dptr->m_scanner), SIGNAL(scanFinished()), 50 | this, SIGNAL(scanFinished())); 51 | connect(&(m_dptr->m_scanner), SIGNAL(newAccessPoint(AccessPoint*)), 52 | this, SLOT(onNewAccessPoint(AccessPoint*))); 53 | } 54 | 55 | AccessPointModel::~AccessPointModel() 56 | { 57 | delete m_dptr; 58 | } 59 | 60 | int AccessPointModel::rowCount(const QModelIndex &parent) const 61 | { 62 | return m_dptr->m_accessPoints.size(); 63 | } 64 | 65 | QVariant AccessPointModel::data(const QModelIndex &index, int role) const 66 | { 67 | AccessPoint *d = m_dptr->m_accessPoints[index.row()]; 68 | switch(role - Qt::UserRole) 69 | { 70 | case 0: 71 | return d->m_nickName; 72 | case 1: 73 | return d->m_portraitIndex; 74 | case 2: 75 | return d->m_address.toString(); 76 | default: 77 | return QVariant(); 78 | } 79 | } 80 | 81 | QHash AccessPointModel::roleNames() const 82 | { 83 | return m_dptr->m_roleNames; 84 | } 85 | 86 | void AccessPointModel::scan() 87 | { 88 | beginResetModel(); 89 | 90 | m_dptr->clear(); 91 | m_dptr->m_scanner.startScan(); 92 | 93 | endResetModel(); 94 | } 95 | 96 | void AccessPointModel::talkTo(int index) 97 | { 98 | m_dptr->m_current = index; 99 | MessengerManager::instance()->talkTo(m_dptr->m_accessPoints.at(index)); 100 | } 101 | 102 | QString AccessPointModel::currentNick() 103 | { 104 | AccessPoint *ap = m_dptr->m_accessPoints.at(m_dptr->m_current); 105 | return ap->m_nickName; 106 | } 107 | 108 | int AccessPointModel::currentPortraitIndex() 109 | { 110 | AccessPoint *ap = m_dptr->m_accessPoints.at(m_dptr->m_current); 111 | return ap->m_portraitIndex; 112 | } 113 | 114 | void AccessPointModel::onNewAccessPoint(AccessPoint *ap) 115 | { 116 | int count = m_dptr->m_accessPoints.size(); 117 | beginInsertRows(QModelIndex(), count, count); 118 | m_dptr->m_accessPoints.append(ap); 119 | endInsertRows(); 120 | } 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /accessPointModel.h: -------------------------------------------------------------------------------- 1 | #ifndef AccessPointModel_H 2 | #define AccessPointModel_H 3 | #include 4 | 5 | class AccessPoint; 6 | class AccessPointModelPrivate; 7 | class AccessPointModel : public QAbstractListModel 8 | { 9 | Q_OBJECT 10 | Q_PROPERTY(QString currentNick READ currentNick) 11 | Q_PROPERTY(int currentPortraitIndex READ currentPortraitIndex) 12 | public: 13 | AccessPointModel(QObject *parent = 0); 14 | ~AccessPointModel(); 15 | 16 | int rowCount(const QModelIndex &parent) const; 17 | QVariant data(const QModelIndex &index, int role) const; 18 | QHash roleNames() const; 19 | 20 | Q_INVOKABLE void scan(); 21 | Q_INVOKABLE void talkTo(int index); 22 | QString currentNick(); 23 | int currentPortraitIndex(); 24 | 25 | signals: 26 | void scanFinished(); 27 | 28 | private slots: 29 | void onNewAccessPoint(AccessPoint *ap); 30 | 31 | private: 32 | AccessPointModelPrivate *m_dptr; 33 | }; 34 | 35 | #endif // AccessPointModel_H 36 | -------------------------------------------------------------------------------- /android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /android/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/android/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /android/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/android/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /android/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/android/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /android/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 聊哈 4 | 无法找到Ministro服务。\n应用程序无法启动。 5 | 此应用程序需要Ministro服务。您想安装它吗? 6 | 您的应用程序遇到一个致命错误导致它无法继续。 7 | 8 | -------------------------------------------------------------------------------- /audioplayer.cpp: -------------------------------------------------------------------------------- 1 | #include "audioplayer.h" 2 | #include 3 | 4 | SimpleAudioPlayer::SimpleAudioPlayer() 5 | { 6 | m_player = new QMediaPlayer(this); 7 | connect(m_player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus)) 8 | ,this, SLOT(onMediaStatusChanged(QMediaPlayer::MediaStatus))); 9 | connect(m_player, SIGNAL(stateChanged(QMediaPlayer::State)), 10 | this, SLOT(onStateChanged(QMediaPlayer::State))); 11 | connect(m_player, SIGNAL(error(QMediaPlayer::Error)), 12 | this, SLOT(onError(QMediaPlayer::Error))); 13 | 14 | m_playlist = new QMediaPlaylist(this); 15 | m_player->setPlaylist(m_playlist); 16 | } 17 | 18 | SimpleAudioPlayer::~SimpleAudioPlayer() 19 | { 20 | 21 | } 22 | 23 | void SimpleAudioPlayer::play(const QString &filePath) 24 | { 25 | qDebug() << "try to play - " << filePath; 26 | stop(); 27 | m_playlist->addMedia(QUrl::fromLocalFile(filePath)); 28 | m_playlist->setCurrentIndex(0); 29 | m_player->play(); 30 | } 31 | 32 | void SimpleAudioPlayer::stop() 33 | { 34 | m_player->stop(); 35 | m_playlist->clear(); 36 | } 37 | 38 | void SimpleAudioPlayer::onMediaStatusChanged(QMediaPlayer::MediaStatus status) 39 | { 40 | switch(status) 41 | { 42 | case QMediaPlayer::EndOfMedia: 43 | qDebug() << "EndOfMedia"; 44 | emit playFinished(); 45 | break; 46 | case QMediaPlayer::InvalidMedia: 47 | qDebug() << "invalidMedia"; 48 | emit playError(); 49 | break; 50 | case QMediaPlayer::BufferingMedia: 51 | break; 52 | case QMediaPlayer::BufferedMedia: 53 | break; 54 | case QMediaPlayer::LoadingMedia: 55 | break; 56 | case QMediaPlayer::LoadedMedia: 57 | break; 58 | } 59 | } 60 | 61 | void SimpleAudioPlayer::onError(QMediaPlayer::Error error) 62 | { 63 | qDebug() << "play error " << error; 64 | emit playError(); 65 | } 66 | 67 | void SimpleAudioPlayer::onStateChanged(QMediaPlayer::State state) 68 | { 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /audioplayer.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIOPLAYER_H 2 | #define AUDIOPLAYER_H 3 | 4 | #include 5 | #include 6 | 7 | class SimpleAudioPlayer : public QObject 8 | { 9 | Q_OBJECT 10 | public: 11 | SimpleAudioPlayer(); 12 | ~SimpleAudioPlayer(); 13 | 14 | Q_INVOKABLE void play(const QString &filePath); 15 | Q_INVOKABLE void stop(); 16 | 17 | signals: 18 | void playError(); 19 | void playFinished(); 20 | 21 | protected slots: 22 | void onMediaStatusChanged(QMediaPlayer::MediaStatus status); 23 | void onError(QMediaPlayer::Error error); 24 | void onStateChanged(QMediaPlayer::State state); 25 | 26 | private: 27 | QMediaPlayer *m_player; 28 | QMediaPlaylist *m_playlist; 29 | }; 30 | 31 | #endif // AUDIOPLAYER_H 32 | -------------------------------------------------------------------------------- /audiorecorder.cpp: -------------------------------------------------------------------------------- 1 | #include "audiorecorder.h" 2 | #include 3 | #include 4 | #include 5 | /* 6 | * 05-28 13:54:39.281 27785 27847 D LanVoiceMessenger: inputs: ("default", "mic", "voice_uplink", "voi 7 | ce_downlink", "voice_call", "voice_recognition") 8 | 05-28 13:54:39.281 27785 27847 D LanVoiceMessenger: codecs: ("amr-nb", "amr-wb", "aac") 9 | 05-28 13:54:39.281 27785 27847 D LanVoiceMessenger: containers: ("mp4", "3gp", "amr", "awb") 10 | 05-28 13:54:39.291 27785 27847 D LanVoiceMessenger: samplerates: (8000, 11025, 12000, 16000, 22050, 11 | 24000, 32000, 44100, 48000, 96000) 12 | */ 13 | SimpleAudioRecorder::SimpleAudioRecorder(const QString &saveDir) 14 | : m_baseDir(saveDir), m_recorder(this), m_bValid(true) 15 | , m_duration(0), m_bSuccess(true) 16 | { 17 | if(!m_baseDir.endsWith('/')) m_baseDir.append('/'); 18 | m_bValid = setupRecorder(); 19 | m_bSuccess = m_bValid; 20 | } 21 | 22 | SimpleAudioRecorder::~SimpleAudioRecorder() 23 | { 24 | 25 | } 26 | 27 | void SimpleAudioRecorder::record() 28 | { 29 | if(m_bValid && m_recorder.state() == QMediaRecorder::StoppedState) 30 | { 31 | m_duration = 0; 32 | m_bSuccess = true; 33 | 34 | m_filePath = QString("%1%2.%3") 35 | .arg(m_baseDir) 36 | .arg(QDateTime::currentDateTime().toString("yyMMdd-hhmmss")) 37 | .arg(m_recorder.containerFormat()); 38 | m_recorder.setOutputLocation(QUrl::fromLocalFile(m_filePath)); 39 | m_recorder.record(); 40 | } 41 | } 42 | 43 | void SimpleAudioRecorder::stop() 44 | { 45 | if(m_bValid) 46 | { 47 | m_recorder.stop(); 48 | QFile::setPermissions(m_filePath, QFile::ReadOwner | QFile::WriteOwner | QFile::ReadOther | QFile::ReadUser | QFile::ReadGroup); 49 | } 50 | } 51 | 52 | QString SimpleAudioRecorder::file() 53 | { 54 | return m_filePath; 55 | } 56 | 57 | qint64 SimpleAudioRecorder::duration() 58 | { 59 | return m_duration; 60 | } 61 | 62 | bool SimpleAudioRecorder::success() 63 | { 64 | return m_bSuccess; 65 | } 66 | 67 | void SimpleAudioRecorder::onDurationChanged(qint64 duration) 68 | { 69 | m_duration = duration; 70 | emit durationChanged(duration); 71 | } 72 | 73 | void SimpleAudioRecorder::onStatusChanged(QMediaRecorder::Status) 74 | { 75 | 76 | } 77 | 78 | void SimpleAudioRecorder::onError(QMediaRecorder::Error e) 79 | { 80 | if(e != QMediaRecorder::NoError) m_bSuccess = false; 81 | } 82 | 83 | bool SimpleAudioRecorder::setupRecorder() 84 | { 85 | //inputs 86 | QStringList inputs = m_recorder.audioInputs(); 87 | if(inputs.size() == 0) return false; 88 | qDebug() << "inputs: " << inputs; 89 | m_recorder.setAudioInput("default"); 90 | 91 | //audio codecs; 92 | QStringList codecs = m_recorder.supportedAudioCodecs(); 93 | if(codecs.size() == 0) return false; 94 | qDebug() << "codecs: " << codecs; 95 | int sampleRate = 16000; 96 | if(codecs.contains("aac")) 97 | { 98 | m_settings.setCodec("aac"); 99 | } 100 | else if(codecs.contains("amr-wb")) 101 | { 102 | m_settings.setCodec("amr-wb"); 103 | } 104 | else 105 | { 106 | m_settings.setCodec(codecs.at(0)); 107 | sampleRate = 8000; 108 | } 109 | qDebug() << "set codec: " << m_settings.codec(); 110 | 111 | //containers 112 | QStringList containers = m_recorder.supportedContainers(); 113 | if(containers.size() == 0) return false; 114 | qDebug() << "containers: " << containers; 115 | if(containers.contains("3gp")) 116 | { 117 | m_container = "3gp"; 118 | } 119 | else if(containers.contains("mp4")) 120 | { 121 | m_container = "mp4"; 122 | } 123 | else 124 | { 125 | m_container = containers.at(0); 126 | } 127 | 128 | //sample rate 129 | QList sampleRates = m_recorder.supportedAudioSampleRates(); 130 | if(sampleRates.size() == 0) return false; 131 | qDebug() << "samplerates: " << sampleRates; 132 | if(sampleRates.size() && !sampleRates.contains(sampleRate)) 133 | { 134 | sampleRate = sampleRates.at(0); 135 | } 136 | 137 | m_settings.setChannelCount(2); 138 | m_settings.setSampleRate(sampleRate); 139 | m_settings.setQuality(QMultimedia::NormalQuality); 140 | m_settings.setBitRate(64000); 141 | m_settings.setEncodingMode(QMultimedia::AverageBitRateEncoding); 142 | m_recorder.setEncodingSettings(m_settings 143 | , QVideoEncoderSettings() 144 | , m_container); 145 | 146 | connect(&m_recorder, SIGNAL(durationChanged(qint64)), this, 147 | SLOT(onDurationChanged(qint64))); 148 | connect(&m_recorder, SIGNAL(statusChanged(QMediaRecorder::Status)), this, 149 | SLOT(onStatusChanged(QMediaRecorder::Status))); 150 | connect(&m_recorder, SIGNAL(error(QMediaRecorder::Error)), this, 151 | SLOT(onError(QMediaRecorder::Error))); 152 | 153 | return true; 154 | } 155 | 156 | -------------------------------------------------------------------------------- /audiorecorder.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIORECORDER_H 2 | #define AUDIORECORDER_H 3 | #include 4 | #include 5 | #include 6 | 7 | class SimpleAudioRecorder : public QObject 8 | { 9 | Q_OBJECT 10 | Q_PROPERTY(qint64 duration READ duration NOTIFY durationChanged) 11 | Q_PROPERTY(QString file READ file) 12 | public: 13 | SimpleAudioRecorder(const QString &saveDir); 14 | ~SimpleAudioRecorder(); 15 | 16 | Q_INVOKABLE bool isValid() 17 | { 18 | return m_bValid; 19 | } 20 | 21 | QString device() 22 | { 23 | return m_recorder.audioInput(); 24 | } 25 | 26 | QString codec() 27 | { 28 | return m_settings.codec(); 29 | } 30 | 31 | int sampleRate() 32 | { 33 | return m_settings.sampleRate(); 34 | } 35 | 36 | QString container() 37 | { 38 | return m_recorder.containerFormat(); 39 | } 40 | 41 | int bitrate() 42 | { 43 | return m_settings.bitRate(); 44 | } 45 | 46 | int channels() 47 | { 48 | return m_settings.channelCount(); 49 | } 50 | 51 | QMultimedia::EncodingQuality quality() 52 | { 53 | return m_settings.quality(); 54 | } 55 | 56 | /* 57 | * current record 58 | */ 59 | Q_INVOKABLE void record(); 60 | Q_INVOKABLE void stop(); 61 | /* return the full path of the current record */ 62 | QString file(); 63 | qint64 duration(); 64 | Q_INVOKABLE bool success(); 65 | signals: 66 | void durationChanged(qint64 duration); 67 | 68 | protected slots: 69 | void onDurationChanged(qint64 duration); 70 | void onStatusChanged(QMediaRecorder::Status); 71 | void onError(QMediaRecorder::Error); 72 | 73 | private: 74 | bool setupRecorder(); 75 | 76 | private: 77 | QString m_baseDir; 78 | QAudioRecorder m_recorder; 79 | QAudioEncoderSettings m_settings; 80 | bool m_bValid; 81 | QString m_device; 82 | QString m_container; 83 | qint64 m_duration; 84 | bool m_bSuccess; 85 | QString m_filePath; 86 | }; 87 | 88 | #endif // AUDIORECORDER_H 89 | -------------------------------------------------------------------------------- /deployment.pri: -------------------------------------------------------------------------------- 1 | android-no-sdk { 2 | target.path = /data/user/qt 3 | export(target.path) 4 | INSTALLS += target 5 | } else:android { 6 | x86 { 7 | target.path = /libs/x86 8 | } else: armeabi-v7a { 9 | target.path = /libs/armeabi-v7a 10 | } else { 11 | target.path = /libs/armeabi 12 | } 13 | export(target.path) 14 | INSTALLS += target 15 | } else:unix { 16 | isEmpty(target.path) { 17 | qnx { 18 | target.path = /tmp/$${TARGET}/bin 19 | } else { 20 | target.path = /opt/$${TARGET}/bin 21 | } 22 | export(target.path) 23 | } 24 | INSTALLS += target 25 | } 26 | 27 | export(INSTALLS) 28 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "messengerManager.h" 7 | #include "accessPointModel.h" 8 | #include "pointSizeToPixelSize.h" 9 | #include "../stockMonitor/qDebug2Logcat.h" 10 | #include "audiorecorder.h" 11 | #include 12 | #include 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | installLogcatMessageHandler("LVChat"); 17 | QGuiApplication app(argc, argv); 18 | QIcon icon(":/res/appicon.png"); 19 | app.setWindowIcon(icon); 20 | QTranslator ts; 21 | ts.load(":/LVChat.qm"); 22 | app.installTranslator(&ts); 23 | MessengerManager *mgr = MessengerManager::instance(); 24 | SimpleAudioRecorder *recorder = new SimpleAudioRecorder(QDir::currentPath()); 25 | QQmlApplicationEngine engine; 26 | QQmlContext *ctx = engine.rootContext(); 27 | ctx->setContextProperty("msgManager", mgr); 28 | ctx->setContextProperty("apmodel", new AccessPointModel); 29 | ctx->setContextProperty("fontUtil", new PointSizeToPixelSize); 30 | ctx->setContextProperty("audioRecorder", recorder); 31 | engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); 32 | 33 | return app.exec(); 34 | } 35 | -------------------------------------------------------------------------------- /main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import QtQuick.Window 2.1 3 | import QtQuick.Controls 1.2 4 | 5 | Window { 6 | id: root; 7 | visible: true; 8 | width: 320; 9 | height: 480; 10 | title: qsTr("LVChat"); 11 | color: "darkgray"; 12 | property var currentView: null; 13 | property string peerNickName; 14 | property int portraitIndex: 9; 15 | property int peerPortraitIndex: 9; 16 | property string queuedPeerNickName; 17 | property int queuedPeerPortraitIndex: 9; 18 | function onBack(){ 19 | if(currentView.objectName == "talk") { 20 | currentView.back(); 21 | }else{ 22 | Qt.quit(); 23 | } 24 | } 25 | 26 | function populateContacts(){ 27 | currentView.destroy(); 28 | var contactComp = Qt.createComponent("Contacts.qml"); 29 | currentView = contactComp.createObject(root); 30 | currentView.talkTo.connect(onTalkTo); 31 | currentView.startScan.connect(onStartScan); 32 | } 33 | 34 | function onLogin(nickName, index){ 35 | portraitIndex = index; 36 | msgManager.start(nickName, index); 37 | populateContacts(); 38 | currentView.scanAccessPoint(); 39 | } 40 | 41 | function onTalkTo(peerIndex){ 42 | peerNickName = apmodel.currentNick; 43 | peerPortraitIndex = apmodel.currentPortraitIndex; 44 | apmodel.talkTo(peerIndex); 45 | currentView.enabled = false; 46 | waiting.running = true; 47 | } 48 | 49 | /* relative to talkTo 50 | * 51 | */ 52 | function onAccepted(){ 53 | waiting.running = false; 54 | currentView.destroy(); 55 | var chatComp = Qt.createComponent("ChatView.qml"); 56 | currentView = chatComp.createObject(root, 57 | { 58 | "nick": root.peerNickName, 59 | "peerPortraitIndex": root.peerPortraitIndex, 60 | "portraitIndex": root.portraitIndex 61 | }); 62 | currentView.finished.connect(onChatFinished); 63 | } 64 | 65 | function onRejected(){ 66 | if(currentView.objectName == "contacts"){ 67 | currentView.enabled = true; 68 | waiting.running = false; 69 | //TODO: show sorry 70 | }else if(currentView.objectName == "chat"){ 71 | onChatFinished(); 72 | } 73 | } 74 | 75 | function onChatFinished(){ 76 | msgManager.closeCurrentSession(); 77 | populateContacts(); 78 | } 79 | 80 | function onStartScan(){ 81 | waiting.running = true; 82 | } 83 | 84 | BusyIndicator { 85 | id: waiting; 86 | anchors.centerIn: parent; 87 | z: 10; 88 | running: false; 89 | } 90 | 91 | property var questionView; 92 | function onAcceptPeer(){ 93 | questionView.destroy(); 94 | msgManager.acceptNewMessenger(); 95 | peerNickName = queuedPeerNickName; 96 | peerPortraitIndex = queuedPeerPortraitIndex; 97 | if(currentView.objectName == "chat"){ 98 | currentView.changePeer(root.peerNickName, root.peerPortraitIndex); 99 | }else if(currentView.objectName == "contacts"){ 100 | onAccepted(); 101 | } 102 | } 103 | function onRejectPeer(){ 104 | questionView.destroy(); 105 | msgManager.rejectNewMessenger(); 106 | } 107 | 108 | /* 109 | * a Peer want to chat with me! 110 | */ 111 | function onChatRequest(name, portraitIndex, address){ 112 | queuedPeerNickName = name; 113 | queuedPeerPortraitIndex = portraitIndex; 114 | var comp = Qt.createComponent("AcceptView.qml"); 115 | questionView = comp.createObject(root, 116 | { 117 | "text": "[%1]%2".arg(name).arg(qsTr("want to talk with you.")) 118 | }); 119 | questionView.accepted.connect(onAcceptPeer); 120 | questionView.rejected.connect(onRejectPeer); 121 | } 122 | 123 | function onPeerGone(){ 124 | if(currentView.objectName == "chat"){ 125 | populateContacts(); 126 | currentView.scanAccessPoint(); 127 | } 128 | } 129 | 130 | Component.onCompleted: { 131 | var loginComp = Qt.createComponent("LoginView.qml", Component.PreferSynchronous); 132 | currentView = loginComp.createObject(root); 133 | currentView.go.connect(onLogin); 134 | msgManager.chatAccepted.connect(onAccepted); 135 | msgManager.chatRejected.connect(onRejected); 136 | msgManager.newMessenger.connect(onChatRequest); 137 | msgManager.peerGone.connect(onPeerGone); 138 | } 139 | 140 | Connections { 141 | target: apmodel; 142 | onScanFinished: waiting.running = false; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /messengerConnection.cpp: -------------------------------------------------------------------------------- 1 | #include "messengerConnection.h" 2 | #include 3 | #ifdef WIN32 4 | #include 5 | #else 6 | #include 7 | #endif 8 | #include 9 | #include "messengerManager.h" 10 | #include 11 | #include 12 | #include 13 | #include "protocol.h" 14 | #include 15 | 16 | DiscoverConnection::DiscoverConnection(QObject *parent) 17 | : QTcpSocket(parent), m_nTimeout(0) 18 | { 19 | connect(this, SIGNAL(connected()), this, SLOT(onConnected())); 20 | connect(this, SIGNAL(readyRead()), this, SLOT(onReadyRead())); 21 | connect(this, SIGNAL(error(QAbstractSocket::SocketError)), 22 | this, SLOT(deleteLater())); 23 | } 24 | 25 | DiscoverConnection::~DiscoverConnection() 26 | {} 27 | 28 | void DiscoverConnection::connectAp(quint32 addr, quint16 port) 29 | { 30 | QHostAddress address(addr); 31 | connectToHost(address, port); 32 | m_nTimeout = startTimer(500); 33 | } 34 | 35 | void DiscoverConnection::timerEvent(QTimerEvent *e) 36 | { 37 | if(e->timerId() == m_nTimeout) 38 | { 39 | if(state() != QAbstractSocket::ConnectedState) 40 | { 41 | disconnect(this); 42 | abort(); 43 | deleteLater(); 44 | } 45 | killTimer(m_nTimeout); 46 | m_nTimeout = 0; 47 | return; 48 | } 49 | return QTcpSocket::timerEvent(e); 50 | } 51 | 52 | void DiscoverConnection::onConnected() 53 | { 54 | killTimer(m_nTimeout); 55 | m_nTimeout = 0; 56 | write(DISCOVER_PACKET_FULL); 57 | flush(); 58 | disconnect(this, SIGNAL(error(QAbstractSocket::SocketError)), 59 | this, SLOT(deleteLater())); 60 | } 61 | 62 | void DiscoverConnection::onReadyRead() 63 | { 64 | m_data += readAll(); 65 | int index = m_data.indexOf(PACKET_LINE_MARK); 66 | if(index != -1) 67 | { 68 | QString apInfo = QString::fromUtf8(m_data.left(index)); 69 | QStringList infoList = apInfo.split(','); 70 | if(infoList.size() == 3 && infoList.at(0) == "discover") 71 | { 72 | AccessPoint *ap = new AccessPoint(infoList.at(1), infoList.at(2).toInt(), peerAddress()); 73 | emit newAccessPoint(ap); 74 | qDebug() << "got an access point " << ap->m_address.toString() << " name " << ap->m_nickName << " portraitIndex " << ap->m_portraitIndex; 75 | } 76 | 77 | disconnect(this); 78 | close(); 79 | deleteLater(); 80 | } 81 | } 82 | 83 | TalkingConnection::TalkingConnection(QTcpSocket *sock, QObject *parent) 84 | : QObject(parent), m_socket(sock), m_bReady(false) 85 | , m_packetType(0), m_messageLength(0) 86 | , m_duration(0), m_peerAddress(0) 87 | { 88 | if(sock) 89 | { 90 | QHostAddress host = sock->peerAddress(); 91 | m_peerAddress = host.toIPv4Address(); 92 | qDebug() << "accept a connection " << host.toString(); 93 | setupSignalSlots(); 94 | } 95 | } 96 | 97 | TalkingConnection::~TalkingConnection() 98 | { 99 | if(m_socket) 100 | { 101 | m_socket->deleteLater(); 102 | m_socket = 0; 103 | } 104 | } 105 | 106 | void TalkingConnection::accept() 107 | { 108 | if(m_socket) 109 | { 110 | replyRequest(true); 111 | m_bReady = true; 112 | m_packetType = 0; 113 | } 114 | } 115 | 116 | void TalkingConnection::reject() 117 | { 118 | if(m_socket) 119 | { 120 | if(m_socket->state() == QAbstractSocket::ConnectedState) 121 | { 122 | replyRequest(false); 123 | } 124 | closeConnection(); 125 | } 126 | else 127 | { 128 | deleteLater(); 129 | } 130 | } 131 | 132 | void TalkingConnection::talkTo(quint32 ip, quint16 port) 133 | { 134 | QHostAddress address(ip); 135 | m_socket = new QTcpSocket(this); 136 | setupSignalSlots(); 137 | m_socket->connectToHost(address, port); 138 | m_nTimeout = startTimer(2000); 139 | m_peerAddress = ip; 140 | } 141 | 142 | void TalkingConnection::sendVoice(QByteArray &data, int duration, char *format) 143 | { 144 | QString header = QString("message,%1,%2,%3,%4\r\n") 145 | .arg(PACKET_MESSAGE_VOICE) 146 | .arg(data.length()) 147 | .arg(duration).arg(format); 148 | m_socket->write(header.toUtf8()); 149 | m_socket->write(data); 150 | m_socket->flush(); 151 | qDebug() << "sendVoice"; 152 | } 153 | 154 | QString TalkingConnection::peerAddress() 155 | { 156 | if(m_socket) 157 | { 158 | QHostAddress addr = m_socket->peerAddress(); 159 | if(addr != QHostAddress::Null) 160 | { 161 | return addr.toString(); 162 | } 163 | } 164 | return QString(); 165 | } 166 | 167 | void TalkingConnection::onConnected() 168 | { 169 | killTimer(m_nTimeout); 170 | m_nTimeout = 0; 171 | QString strReq = QString(TALK_REQ_PACKET_MARK) 172 | .arg(MessengerManager::s_contact->m_nickName) 173 | .arg(MessengerManager::s_contact->m_portraitIndex); 174 | m_socket->write(strReq.toUtf8()); 175 | m_socket->flush(); 176 | } 177 | 178 | void TalkingConnection::onReadyRead() 179 | { 180 | m_data += m_socket->readAll(); 181 | processPacket(); 182 | } 183 | 184 | void TalkingConnection::onError(QAbstractSocket::SocketError code) 185 | { 186 | m_socket->deleteLater(); 187 | m_socket = 0; 188 | emit peerGone(m_peerAddress); 189 | deleteLater(); 190 | } 191 | 192 | void TalkingConnection::timerEvent(QTimerEvent *e) 193 | { 194 | if(e->timerId() == m_nTimeout) 195 | { 196 | if(m_socket->state() != QAbstractSocket::ConnectedState) 197 | { 198 | m_socket->disconnect(this); 199 | m_socket->abort(); 200 | m_socket->deleteLater(); 201 | m_socket = 0; 202 | deleteLater(); 203 | } 204 | killTimer(m_nTimeout); 205 | m_nTimeout = 0; 206 | return; 207 | } 208 | return QObject::timerEvent(e); 209 | } 210 | 211 | void TalkingConnection::processPacket() 212 | { 213 | switch(m_packetType) 214 | { 215 | case PACKET_MESSAGE: 216 | if( processMessage() && m_data.length() ) processPacket(); 217 | return; 218 | case 0: 219 | break; 220 | default: 221 | return; 222 | } 223 | 224 | int index = m_data.indexOf(PACKET_LINE_MARK); 225 | if(index == -1) return; 226 | 227 | if(m_data.startsWith(DISCOVER_PACKET)) 228 | { 229 | m_packetType = PACKET_DISCOVER; 230 | discoverReply(); 231 | } 232 | else if(m_data.startsWith(TALK_REQ_TOKEN)) 233 | { 234 | m_packetType = PACKET_TALK_REQ; 235 | int start = m_data.indexOf(','); 236 | start++; 237 | QString peerInfo = QString::fromUtf8(m_data.mid(start, index - start)); 238 | QStringList infoList = peerInfo.split(','); 239 | if(infoList.size() == 2) 240 | { 241 | Peer *peer = new Peer(infoList.at(0), infoList.at(1).toInt(), this); 242 | emit incomingMessenger(peer); 243 | } 244 | m_data = m_data.mid(index + 2); 245 | } 246 | else if(m_data.startsWith(TALK_ACCEPT_PACKET)) 247 | { 248 | emit talkingAccepted(); 249 | m_data = m_data.mid(index + 2); 250 | if(m_data.length()) processPacket(); 251 | } 252 | else if(m_data.startsWith(TALK_REJECT_PACKET)) 253 | { 254 | m_packetType = PACKET_TALK_REJECT; 255 | closeConnection(); 256 | emit talkingRejected(); 257 | } 258 | else if(m_data.startsWith(MESSAGE_PACKET_MARK)) 259 | { 260 | QByteArray header = m_data.left(index); 261 | m_data = m_data.mid(index+2); 262 | QList headers = header.split(','); 263 | if(headers.size() != 5) 264 | { 265 | closeConnection(); 266 | return; 267 | } 268 | bool ok = false; 269 | m_messageType = headers.at(1).toUInt(&ok); 270 | if(!ok || m_messageType != PACKET_MESSAGE_VOICE) 271 | { 272 | closeConnection(); 273 | return; 274 | } 275 | m_messageLength = headers.at(2).toUInt(&ok); 276 | if(!ok) 277 | { 278 | closeConnection(); 279 | return; 280 | } 281 | m_duration = headers.at(3).toInt(&ok); 282 | m_format = headers.at(4); 283 | m_packetType = PACKET_MESSAGE; 284 | if( processMessage() && m_data.length() ) processPacket(); 285 | } 286 | } 287 | 288 | 289 | void TalkingConnection::replyRequest(bool agree) 290 | { 291 | const char *packet = agree ? TALK_ACCEPT_PACKET_FULL : TALK_REJECT_PACKET_FULL; 292 | m_socket->write(packet); 293 | m_socket->flush(); 294 | } 295 | 296 | void TalkingConnection::discoverReply() 297 | { 298 | QString strReply = QString(DISCOVER_REPLY_MARK) 299 | .arg(MessengerManager::s_contact->m_nickName) 300 | .arg(MessengerManager::s_contact->m_portraitIndex); 301 | m_socket->write(strReply.toUtf8()); 302 | //qDebug() << "after write name to client"; 303 | m_socket->flush(); 304 | closeConnection(); 305 | //qDebug() << "TalkingConnection, discoverReply"; 306 | } 307 | 308 | void TalkingConnection::closeConnection() 309 | { 310 | m_socket->disconnect(this); 311 | m_socket->close(); 312 | m_socket->deleteLater(); 313 | m_socket = 0; 314 | deleteLater(); 315 | } 316 | 317 | void TalkingConnection::setupSignalSlots() 318 | { 319 | connect(m_socket, SIGNAL(connected()),this, SLOT(onConnected())); 320 | connect(m_socket, SIGNAL(readyRead()), this, SLOT(onReadyRead())); 321 | connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), 322 | this, SLOT(onError(QAbstractSocket::SocketError))); 323 | } 324 | 325 | int TalkingConnection::processMessage() 326 | { 327 | if(m_data.length() < m_messageLength) return 0; 328 | QString fileName = QString("%1/%2.%3") 329 | .arg(QDir::currentPath()) 330 | .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm-ss")) 331 | .arg(m_format.data()); 332 | QFile file(fileName); 333 | if(file.open(QFile::WriteOnly)) 334 | { 335 | file.write(m_data.data(), m_messageLength); 336 | file.close(); 337 | emit incomingVoice(fileName, m_duration); 338 | } 339 | 340 | if(m_data.length() > m_messageLength) 341 | { 342 | m_data = m_data.mid(m_messageLength); 343 | } 344 | else 345 | { 346 | m_data.clear(); 347 | } 348 | m_packetType = 0; 349 | m_messageLength = 0; 350 | m_duration = 0; 351 | m_format.clear(); 352 | return 1; 353 | } 354 | -------------------------------------------------------------------------------- /messengerConnection.h: -------------------------------------------------------------------------------- 1 | #ifndef ServerConnection_H 2 | #define ServerConnection_H 3 | #include 4 | #include 5 | #include "protocol.h" 6 | #include 7 | #include 8 | #include 9 | 10 | class Contact; 11 | class Peer; 12 | class AccessPoint; 13 | 14 | class DiscoverConnection : public QTcpSocket 15 | { 16 | Q_OBJECT 17 | public: 18 | DiscoverConnection(QObject *parent = 0); 19 | ~DiscoverConnection(); 20 | 21 | void connectAp(quint32 addr, quint16 port); 22 | 23 | signals: 24 | void newAccessPoint(AccessPoint *ap); 25 | 26 | protected: 27 | void timerEvent(QTimerEvent *e); 28 | 29 | protected slots: 30 | void onConnected(); 31 | void onReadyRead(); 32 | 33 | private: 34 | int m_nTimeout; 35 | QByteArray m_data; 36 | }; 37 | 38 | class TalkingConnection : public QObject 39 | { 40 | Q_OBJECT 41 | public: 42 | TalkingConnection(QTcpSocket *sock = 0, QObject *parent = 0); 43 | ~TalkingConnection(); 44 | 45 | void accept(); 46 | void reject(); 47 | void talkTo(quint32 ip, quint16 port); 48 | void sendVoice(QByteArray &data, int duration, char *format); 49 | QString peerAddress(); 50 | 51 | signals: 52 | void talkingAccepted(); 53 | void talkingRejected(); 54 | void incomingMessenger(Peer *peer); 55 | void incomingVoice(QString fileName, int duration); 56 | void peerGone(quint32 peerAddr); //error or peer quit app 57 | 58 | protected slots: 59 | void onConnected(); 60 | void onReadyRead(); 61 | void onError(QAbstractSocket::SocketError code); 62 | 63 | protected: 64 | void timerEvent(QTimerEvent *e); 65 | 66 | private: 67 | void processPacket(); 68 | void replyRequest(bool agree); 69 | void discoverReply(); 70 | int processMessage(); 71 | void closeConnection(); 72 | void setupSignalSlots(); 73 | 74 | private: 75 | QTcpSocket *m_socket; 76 | bool m_bReady; 77 | int m_nTimeout; 78 | int m_packetType; 79 | quint32 m_messageType; 80 | quint32 m_messageLength; 81 | QByteArray m_data; 82 | qint32 m_duration; 83 | QByteArray m_format; 84 | quint32 m_peerAddress; 85 | }; 86 | 87 | class Contact 88 | { 89 | public: 90 | Contact(): m_portraitIndex(0) 91 | { 92 | } 93 | Contact(const QString &nickName, int portraitIndex) 94 | : m_nickName(nickName), m_portraitIndex(portraitIndex) 95 | {} 96 | 97 | virtual ~Contact(){} 98 | 99 | QString m_nickName; 100 | int m_portraitIndex; 101 | }; 102 | 103 | class AccessPoint : public Contact 104 | { 105 | public: 106 | AccessPoint(const QString &nickName, int portraitIndex, const QHostAddress &addr) 107 | : Contact(nickName, portraitIndex) 108 | ,m_address(addr) 109 | { 110 | } 111 | ~AccessPoint() 112 | { 113 | } 114 | 115 | QHostAddress m_address; 116 | }; 117 | 118 | class Peer : public Contact 119 | { 120 | public: 121 | Peer(const QString &nickName, int portraitIndex, TalkingConnection *conn) 122 | : Contact(nickName, portraitIndex) 123 | ,m_connection(conn) 124 | { 125 | 126 | } 127 | ~Peer() 128 | { 129 | if(m_connection)m_connection->reject(); 130 | } 131 | 132 | QPointer m_connection; 133 | }; 134 | 135 | #endif // ServerConnection_H 136 | -------------------------------------------------------------------------------- /messengerManager.cpp: -------------------------------------------------------------------------------- 1 | #include "messengerManager.h" 2 | #include 3 | #include 4 | 5 | Contact *MessengerManager::s_contact = 0; 6 | MessengerManager::MessengerManager(QObject *parent) 7 | : QTcpServer(parent), m_currentPeer(0), m_pendingPeer(0) 8 | { 9 | if(!s_contact) 10 | { 11 | s_contact = new Contact(); 12 | } 13 | connect(this, SIGNAL(newConnection()), 14 | this, SLOT(onNewConnection())); 15 | } 16 | 17 | MessengerManager::~MessengerManager() 18 | { 19 | 20 | } 21 | 22 | void MessengerManager::acceptNewMessenger() 23 | { 24 | if(m_currentPeer) 25 | { 26 | delete m_currentPeer; 27 | } 28 | m_currentPeer = m_pendingPeer; 29 | m_currentPeer->m_connection->accept(); 30 | m_pendingPeer = 0; 31 | setupSignalSlots(m_currentPeer->m_connection); 32 | } 33 | 34 | void MessengerManager::rejectNewMessenger() 35 | { 36 | if(m_pendingPeer) 37 | { 38 | delete m_pendingPeer; 39 | m_pendingPeer = 0; 40 | } 41 | } 42 | 43 | void MessengerManager::closeCurrentSession() 44 | { 45 | if(m_currentPeer) 46 | { 47 | delete m_currentPeer; 48 | m_currentPeer = 0; 49 | } 50 | } 51 | 52 | void MessengerManager::start(QString nickName, int portraitIndex) 53 | { 54 | s_contact->m_nickName = nickName; 55 | s_contact->m_portraitIndex = portraitIndex; 56 | listen(QHostAddress::AnyIPv4, SERVER_PORT); 57 | } 58 | 59 | void MessengerManager::talkTo(AccessPoint *ap) 60 | { 61 | rejectNewMessenger(); 62 | closeCurrentSession(); 63 | 64 | TalkingConnection *conn = new TalkingConnection(); 65 | m_currentPeer = new Peer(ap->m_nickName, ap->m_portraitIndex, conn); 66 | conn->talkTo(ap->m_address.toIPv4Address(), SERVER_PORT); 67 | setupSignalSlots(conn); 68 | } 69 | 70 | MessengerManager *MessengerManager::instance() 71 | { 72 | static MessengerManager *server = new MessengerManager(); 73 | return server; 74 | } 75 | 76 | void MessengerManager::sendVoiceMessage(QString fileName, qint64 duration) 77 | { 78 | if(m_currentPeer) 79 | { 80 | QFile file(fileName); 81 | if( file.open(QFile::ReadOnly) ) 82 | { 83 | int sep = fileName.lastIndexOf('.'); 84 | QString format = fileName.mid(sep+1); 85 | QByteArray data = file.readAll(); 86 | file.close(); 87 | m_currentPeer->m_connection->sendVoice(data, duration, format.toLatin1().data()); 88 | } 89 | else 90 | { 91 | qDebug() << "could not open - " << fileName << " error - " << file.errorString(); 92 | } 93 | } 94 | } 95 | 96 | void MessengerManager::onNewConnection() 97 | { 98 | while(hasPendingConnections()) 99 | { 100 | TalkingConnection *m = new TalkingConnection(nextPendingConnection()); 101 | connect(m, SIGNAL(incomingMessenger(Peer*)), 102 | this, SLOT(onIncomingMessenger(Peer*))); 103 | } 104 | } 105 | 106 | void MessengerManager::onIncomingMessenger(Peer *peer) 107 | { 108 | if(m_pendingPeer) 109 | { 110 | //sorry...we just allow one pending peer 111 | delete peer; 112 | } 113 | else 114 | { 115 | m_pendingPeer = peer; 116 | emit newMessenger(peer->m_nickName, 117 | peer->m_portraitIndex, 118 | peer->m_connection->peerAddress()); 119 | } 120 | } 121 | 122 | void MessengerManager::setupSignalSlots(TalkingConnection *conn) 123 | { 124 | connect(conn, SIGNAL(talkingAccepted()), this, SIGNAL(chatAccepted())); 125 | connect(conn, SIGNAL(talkingRejected()), this, SIGNAL(chatRejected())); 126 | connect(conn, SIGNAL(incomingVoice(QString,int)), this, SIGNAL(voiceMessageArrived(QString,int))); 127 | connect(conn, SIGNAL(peerGone(quint32)), this, SIGNAL(peerGone())); 128 | } 129 | -------------------------------------------------------------------------------- /messengerManager.h: -------------------------------------------------------------------------------- 1 | #ifndef MESSENGER_MANAGER_H 2 | #define MESSENGER_MANAGER_H 3 | #include 4 | #include "messengerConnection.h" 5 | 6 | class MessengerManager : public QTcpServer 7 | { 8 | Q_OBJECT 9 | MessengerManager(QObject *parent = 0); 10 | public: 11 | ~MessengerManager(); 12 | 13 | Q_INVOKABLE void acceptNewMessenger(); 14 | Q_INVOKABLE void rejectNewMessenger(); 15 | Q_INVOKABLE void closeCurrentSession(); 16 | 17 | static Contact * s_contact; 18 | Q_INVOKABLE void start(QString nickName, int portraitIndex); 19 | 20 | void talkTo(AccessPoint *ap); 21 | 22 | static MessengerManager *instance(); 23 | Q_INVOKABLE void sendVoiceMessage(QString fileName, qint64 duration); 24 | 25 | signals: 26 | void newMessenger(QString name, int portraitIndex, QString address); 27 | void chatAccepted(); 28 | void chatRejected(); 29 | void voiceMessageArrived(QString fileName, int duration); 30 | void peerGone(); 31 | 32 | protected slots: 33 | void onNewConnection(); 34 | void onIncomingMessenger(Peer *peer); 35 | 36 | private: 37 | void setupSignalSlots(TalkingConnection *conn); 38 | 39 | private: 40 | Peer * m_currentPeer; 41 | Peer * m_pendingPeer; 42 | }; 43 | 44 | #endif // MessengerManager_H 45 | -------------------------------------------------------------------------------- /pointSizeToPixelSize.cpp: -------------------------------------------------------------------------------- 1 | #include "pointSizeToPixelSize.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int PointSizeToPixelSize::pixelSize(int pointSize) 9 | { 10 | QScreen *screen = qApp->primaryScreen(); 11 | int ps = (pointSize * screen->logicalDotsPerInch()) / 72; 12 | qDebug() << "pointSize " << pointSize << " - pixelSize " << ps; 13 | return ps; 14 | } 15 | 16 | int PointSizeToPixelSize::height(int pointSize) 17 | { 18 | QFont font = qApp->font(); 19 | font.setPointSize(pointSize); 20 | QFontMetrics fm(font); 21 | return fm.height(); 22 | } 23 | 24 | int PointSizeToPixelSize::width(int pointSize, QString text) 25 | { 26 | QFont font = qApp->font(); 27 | font.setPointSize(pointSize); 28 | QFontMetrics fm(font); 29 | return fm.width(text); 30 | } 31 | -------------------------------------------------------------------------------- /pointSizeToPixelSize.h: -------------------------------------------------------------------------------- 1 | #ifndef POINTSIZETOPIXELSIZE_H 2 | #define POINTSIZETOPIXELSIZE_H 3 | #include 4 | 5 | class PointSizeToPixelSize : public QObject 6 | { 7 | Q_OBJECT 8 | public: 9 | PointSizeToPixelSize(QObject *parent = 0){} 10 | ~PointSizeToPixelSize(){} 11 | 12 | Q_INVOKABLE int pixelSize(int pointSize); 13 | Q_INVOKABLE int height(int pointSize); 14 | Q_INVOKABLE int width(int pointSize, QString text); 15 | }; 16 | 17 | #endif // POINTSIZETOPIXELSIZE_H 18 | -------------------------------------------------------------------------------- /protocol.h: -------------------------------------------------------------------------------- 1 | #ifndef PROTOCOL_H 2 | #define PROTOCOL_H 3 | 4 | #define SERVER_PORT 5661 5 | #define PACKET_LINE_MARK "\r\n" 6 | #define DISCOVER_PACKET "discover" 7 | #define DISCOVER_PACKET_FULL "discover\r\n" 8 | #define DISCOVER_REPLY_FORMAT "discover,${name},${portraitIndex}\r\n" 9 | #define DISCOVER_REPLY_MARK "discover,%1,%2\r\n" 10 | #define TALK_REQ_PACKET_FORMAT "talking,${name},${portraitIndex}\r\n" 11 | #define TALK_REQ_PACKET_MARK "talking,%1,%2\r\n" 12 | #define TALK_REQ_TOKEN "talking" 13 | #define TALK_REJECT_PACKET "reject" 14 | #define TALK_REJECT_PACKET_FULL "reject\r\n" 15 | #define TALK_ACCEPT_PACKET "accept" 16 | #define TALK_ACCEPT_PACKET_FULL "accept\r\n" 17 | 18 | #define MESSAGE_PACKET_MARK "message" 19 | #define MESSAGE_PACKET_FORMAT "message,${type},${lengthOfBody},${duration},${format}\r\n${BODY DATA}" 20 | 21 | #define PACKET_DISCOVER 1 22 | #define PACKET_DISCOVER_REPLY 2 23 | #define PACKET_TALK_REQ 3 24 | #define PACKET_TALK_ACCEPT 4 25 | #define PACKET_TALK_REJECT 5 26 | #define PACKET_MESSAGE 6 27 | #define PACKET_MESSAGE_TEXT 1 28 | #define PACKET_MESSAGE_VOICE 2 29 | 30 | #endif // PROTOCOL_H 31 | -------------------------------------------------------------------------------- /qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | res/head_0.png 4 | res/head_1.png 5 | res/head_2.png 6 | res/head_3.png 7 | res/head_4.png 8 | res/head_5.png 9 | res/head_6.png 10 | res/head_7.png 11 | res/head_8.png 12 | res/head_9.png 13 | res/ic_ok.png 14 | res/ic_ok_pressed.png 15 | LoginView.qml 16 | main.qml 17 | ImageButton.qml 18 | ChatView.qml 19 | Contacts.qml 20 | res/ic_microphone.png 21 | res/chat_from_bg_normal.9.png 22 | res/chat_from_bg_pressed.9.png 23 | res/chat_to_bg_normal.9.png 24 | res/chat_to_bg_pressed.9.png 25 | res/ic_back.png 26 | res/ic_back_pressed.png 27 | res/ic_voice.png 28 | res/ic_search.png 29 | res/ic_search_pressed.png 30 | res/ic_chat.png 31 | res/ic_chat_pressed.png 32 | AcceptView.qml 33 | res/ic_accept.png 34 | res/ic_reject.png 35 | LVChat.qm 36 | FlatButton.qml 37 | res/appicon.png 38 | 39 | 40 | -------------------------------------------------------------------------------- /res/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/appicon.png -------------------------------------------------------------------------------- /res/chat_from_bg_normal.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/chat_from_bg_normal.9.png -------------------------------------------------------------------------------- /res/chat_from_bg_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/chat_from_bg_pressed.9.png -------------------------------------------------------------------------------- /res/chat_to_bg_normal.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/chat_to_bg_normal.9.png -------------------------------------------------------------------------------- /res/chat_to_bg_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/chat_to_bg_pressed.9.png -------------------------------------------------------------------------------- /res/head_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/head_0.png -------------------------------------------------------------------------------- /res/head_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/head_1.png -------------------------------------------------------------------------------- /res/head_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/head_2.png -------------------------------------------------------------------------------- /res/head_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/head_3.png -------------------------------------------------------------------------------- /res/head_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/head_4.png -------------------------------------------------------------------------------- /res/head_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/head_5.png -------------------------------------------------------------------------------- /res/head_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/head_6.png -------------------------------------------------------------------------------- /res/head_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/head_7.png -------------------------------------------------------------------------------- /res/head_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/head_8.png -------------------------------------------------------------------------------- /res/head_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/head_9.png -------------------------------------------------------------------------------- /res/ic_accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_accept.png -------------------------------------------------------------------------------- /res/ic_back - 副本.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_back - 副本.png -------------------------------------------------------------------------------- /res/ic_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_back.png -------------------------------------------------------------------------------- /res/ic_back_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_back_pressed.png -------------------------------------------------------------------------------- /res/ic_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_chat.png -------------------------------------------------------------------------------- /res/ic_chat_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_chat_pressed.png -------------------------------------------------------------------------------- /res/ic_microphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_microphone.png -------------------------------------------------------------------------------- /res/ic_ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_ok.png -------------------------------------------------------------------------------- /res/ic_ok_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_ok_pressed.png -------------------------------------------------------------------------------- /res/ic_reject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_reject.png -------------------------------------------------------------------------------- /res/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_search.png -------------------------------------------------------------------------------- /res/ic_search_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_search_pressed.png -------------------------------------------------------------------------------- /res/ic_voice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/ic_voice.png -------------------------------------------------------------------------------- /res/liao.tsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/res/liao.tsp -------------------------------------------------------------------------------- /scaleableBorder.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | 3 | Rectangle { 4 | width: 450; 5 | height: 300; 6 | 7 | Row{ 8 | anchors.centerIn: parent; 9 | BorderImage{ 10 | width: 130; 11 | height: 200; 12 | border.left: 16; 13 | border.top: 38; 14 | border.right: 6; 15 | border.bottom: 6; 16 | source: "res/chat_from_bg_normal.9.png"; 17 | } 18 | BorderImage{ 19 | width: 200; 20 | height: 150; 21 | border.left: 16; 22 | border.top: 38; 23 | border.right: 6; 24 | border.bottom: 6; 25 | source: "res/chat_from_bg_normal.9.png"; 26 | } 27 | BorderImage{ 28 | width: 100; 29 | height: 280; 30 | border.left: 16; 31 | border.top: 38; 32 | border.right: 6; 33 | border.bottom: 6; 34 | source: "res/chat_from_bg_normal.9.png"; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /scanner.cpp: -------------------------------------------------------------------------------- 1 | #include "scanner.h" 2 | #include 3 | #include 4 | #include 5 | #include "messengerConnection.h" 6 | #include 7 | 8 | AccessPointScanner::AccessPointScanner(QObject *parent) 9 | : QObject(parent) 10 | ,m_index(0), m_finished(0) 11 | { 12 | initializeScanList(); 13 | } 14 | 15 | AccessPointScanner::~AccessPointScanner() 16 | { 17 | 18 | } 19 | 20 | void AccessPointScanner::startScan() 21 | { 22 | if(m_scanList.size() > 0) 23 | { 24 | scanAps(); 25 | } 26 | else 27 | { 28 | qDebug() << "none scan list"; 29 | emit scanFinished(); 30 | } 31 | } 32 | 33 | void AccessPointScanner::onDiscoverSocketDestroyed() 34 | { 35 | m_finished++; 36 | if(++m_index < m_scanList.size()) 37 | { 38 | scanOneAp(); 39 | return; 40 | } 41 | if(m_finished == m_scanList.size()) 42 | { 43 | m_index = 0; 44 | m_finished = 0; 45 | emit scanFinished(); 46 | qDebug() << "emit scanFinished"; 47 | } 48 | } 49 | 50 | void AccessPointScanner::scanOneAp() 51 | { 52 | DiscoverConnection *conn = new DiscoverConnection(); 53 | connect(conn, SIGNAL(newAccessPoint(AccessPoint *)), 54 | this, SIGNAL(newAccessPoint(AccessPoint *))); 55 | connect(conn, SIGNAL(destroyed()), this, SLOT(onDiscoverSocketDestroyed())); 56 | conn->connectAp(m_scanList.at(m_index), SERVER_PORT); 57 | //qDebug() << "scaning " << QHostAddress(m_scanList.at(m_index)); 58 | } 59 | 60 | void AccessPointScanner::scanAps() 61 | { 62 | int limit = qMin(m_scanList.size(), 10); 63 | for(; m_index < limit; ++m_index) 64 | { 65 | scanOneAp(); 66 | } 67 | --m_index; 68 | } 69 | 70 | void AccessPointScanner::initializeScanList() 71 | { 72 | QList interfaces = QNetworkInterface::allInterfaces(); 73 | foreach(QNetworkInterface interface, interfaces) 74 | { 75 | int flags = interface.flags(); 76 | if(flags & QNetworkInterface::IsLoopBack) continue; 77 | if(!(flags &(QNetworkInterface::IsUp | QNetworkInterface::IsRunning))) continue; 78 | 79 | QList entries = interface.addressEntries(); 80 | int count = entries.size(); 81 | for(int i = 0; i < count; i++) 82 | { 83 | const QNetworkAddressEntry &entry = entries.at(i); 84 | QHostAddress ip = entry.ip(); 85 | if(!ip.isLoopback() && ip.protocol() == QAbstractSocket::IPv4Protocol) 86 | { 87 | quint32 myself = ip.toIPv4Address(); 88 | int numberOfIps = ipCount(entry.prefixLength()); 89 | quint32 startIp = (ip.toIPv4Address() & entry.netmask().toIPv4Address())+1; 90 | for(int j = 1; j < numberOfIps; j++, startIp++) 91 | { 92 | if(myself != startIp) 93 | { 94 | m_scanList.append(startIp); 95 | //QHostAddress address(startIp); 96 | //qDebug() << address; 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | int AccessPointScanner::ipCount(int prefixLength) 105 | { 106 | int count = 0; 107 | int unmask = 32 - prefixLength; 108 | for(int i = 0; i < unmask; i++) 109 | { 110 | count += pow(2, i); 111 | } 112 | return count; 113 | } 114 | -------------------------------------------------------------------------------- /scanner.h: -------------------------------------------------------------------------------- 1 | #ifndef SCANNER_H 2 | #define SCANNER_H 3 | #include 4 | #include 5 | #include 6 | 7 | class AccessPoint; 8 | class AccessPointScanner : public QObject 9 | { 10 | Q_OBJECT 11 | public: 12 | AccessPointScanner(QObject *parent = 0); 13 | ~AccessPointScanner(); 14 | 15 | void startScan(); 16 | 17 | signals: 18 | void newAccessPoint(AccessPoint *ap); 19 | void scanFinished(); 20 | 21 | protected slots: 22 | void onDiscoverSocketDestroyed(); 23 | 24 | private: 25 | void scanAps(); 26 | void initializeScanList(); 27 | int ipCount(int prefixLength); 28 | bool compareAccessPoints(); 29 | void scanOneAp(); 30 | 31 | private: 32 | QList m_scanList; 33 | int m_index; 34 | int m_finished; 35 | }; 36 | 37 | #endif // SCANNER_H 38 | -------------------------------------------------------------------------------- /screenshots/pc_init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/pc_init.png -------------------------------------------------------------------------------- /screenshots/pc_select_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/pc_select_head.png -------------------------------------------------------------------------------- /screenshots/scaleableBorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/scaleableBorder.png -------------------------------------------------------------------------------- /screenshots/豌豆荚截图20140923214402.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/豌豆荚截图20140923214402.png -------------------------------------------------------------------------------- /screenshots/豌豆荚截图20140923214424.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/豌豆荚截图20140923214424.png -------------------------------------------------------------------------------- /screenshots/豌豆荚截图20140923214450.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/豌豆荚截图20140923214450.png -------------------------------------------------------------------------------- /screenshots/豌豆荚截图20140923214619.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/豌豆荚截图20140923214619.png -------------------------------------------------------------------------------- /screenshots/豌豆荚截图20140923214651.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/豌豆荚截图20140923214651.png -------------------------------------------------------------------------------- /screenshots/豌豆荚截图20140923214816.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/豌豆荚截图20140923214816.png -------------------------------------------------------------------------------- /screenshots/豌豆荚截图20140923214838.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/豌豆荚截图20140923214838.png -------------------------------------------------------------------------------- /screenshots/豌豆荚截图20140923214854.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/豌豆荚截图20140923214854.png -------------------------------------------------------------------------------- /screenshots/豌豆荚截图20140923222600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/豌豆荚截图20140923222600.png -------------------------------------------------------------------------------- /screenshots/豌豆荚截图20140923222705.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foruok/LVChat/40412e8683220e07f69d298ac71fc9aa91f36304/screenshots/豌豆荚截图20140923222705.png -------------------------------------------------------------------------------- /voiceMessage.h: -------------------------------------------------------------------------------- 1 | #ifndef VOICEMESSAGE_H 2 | #define VOICEMESSAGE_H 3 | #include 4 | #include 5 | class VoiceMessage 6 | { 7 | public: 8 | VoiceMessage(const QString &fileName, int duration) 9 | : m_fileName(fileName), m_duration(duration) 10 | , m_time(QDateTime::currentDateTime()) 11 | { 12 | } 13 | 14 | QString m_fileName; 15 | int m_duration; 16 | QDateTime m_time; 17 | }; 18 | 19 | #endif // VOICEMESSAGE_H 20 | --------------------------------------------------------------------------------