├── .gitignore ├── LICENSE ├── NeteaseMusicForLinux.desktop ├── NeteaseMusicForLinux.pro ├── README.md ├── debian ├── changelog ├── compat ├── control ├── rules └── source │ └── format ├── deployment.pri ├── icons ├── 128x128 │ └── NeteaseMusicForLinux.png ├── 256x256 │ └── NeteaseMusicForLinux.png ├── 32x32 │ └── NeteaseMusicForLinux.png └── 64x64 │ └── NeteaseMusicForLinux.png ├── images.qrc ├── images ├── cancel_fullscreen.png ├── disc_disc.png ├── disc_mask.png ├── disc_needle.png ├── headphone.png ├── left_arrow_hover.png ├── left_arrow_inactive.png ├── left_arrow_normal.png ├── muted_hover_pressed.png ├── muted_normal.png ├── next_hover_pressed.png ├── next_normal.png ├── pause_hover_pressed.png ├── pause_normal.png ├── play_hover_pressed.png ├── play_normal.png ├── playlist_hover_pressed.png ├── playlist_normal.png ├── prev_hover_pressed.png ├── prev_normal.png ├── right_arrow_hover.png ├── right_arrow_inactive.png ├── right_arrow_normal.png ├── user_icon.png ├── volume_hover_pressed.png └── volume_normal.png ├── qml.qrc └── src ├── appcontroller.cpp ├── appcontroller.h ├── database.cpp ├── database.h ├── htsettings.cpp ├── htsettings.h ├── main.cpp ├── netease_api ├── neteaseapi.cpp └── neteaseapi.h ├── playlist_model.cpp ├── playlist_model.h ├── qmls ├── Footer.qml ├── HTImageButton.qml ├── HTTabContent.qml ├── HTTabView.qml ├── HTVolumeButton.qml ├── Header.qml ├── LoginDialog.qml ├── MainController.qml ├── PlayView.qml ├── PlaylistDetialView.qml ├── PlaylistIconView.qml ├── PlaylistView.qml ├── SideBar.qml ├── SideBarPlaylists.qml ├── SongsListView.qml ├── ViewHistoryManager.qml ├── main.qml ├── utils.js └── widgets │ ├── BackForwardButton.qml │ ├── HSep.qml │ ├── HTBannersView.qml │ ├── HTLyricView.qml │ ├── HTProgressBar.qml │ ├── HTSectionTitle.qml │ ├── HTTextButton.qml │ ├── HTTextInput.qml │ ├── RunningDisc.qml │ └── VSep.qml ├── song.cpp └── song.h /.gitignore: -------------------------------------------------------------------------------- 1 | *.pro.user 2 | *.DS_Store 3 | build/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 hualet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /NeteaseMusicForLinux.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Exec=NeteaseMusicForLinux 3 | Icon=NeteaseMusicForLinux 4 | Name=网易云音乐 5 | StartupNotify=true 6 | Terminal=false 7 | Type=Application 8 | -------------------------------------------------------------------------------- /NeteaseMusicForLinux.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | 3 | QT += qml quick sql 4 | 5 | SOURCES += src/main.cpp \ 6 | src/netease_api/neteaseapi.cpp \ 7 | src/appcontroller.cpp \ 8 | src/song.cpp \ 9 | src/playlist_model.cpp \ 10 | src/htsettings.cpp \ 11 | src/database.cpp 12 | 13 | RESOURCES += qml.qrc \ 14 | images.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 | HEADERS += \ 23 | src/netease_api/neteaseapi.h \ 24 | src/appcontroller.h \ 25 | src/song.h \ 26 | src/playlist_model.h \ 27 | src/htsettings.h \ 28 | src/database.h 29 | 30 | 31 | unix { 32 | icon32.files = icons/32x32/NeteaseMusicForLinux.png 33 | icon32.path = /usr/share/icons/hicolor/32x32/apps 34 | icon64.files = icons/64x64/NeteaseMusicForLinux.png 35 | icon64.path = /usr/share/icons/hicolor/64x64/apps 36 | icon128.files = icons/128x128/NeteaseMusicForLinux.png 37 | icon128.path = /usr/share/icons/hicolor/128x128/apps 38 | icon256.files = icons/256x256/NeteaseMusicForLinux.png 39 | icon256.path = /usr/share/icons/hicolor/256x256/apps 40 | } 41 | 42 | target.path = /usr/bin/ 43 | 44 | desktop.files = NeteaseMusicForLinux.desktop 45 | desktop.path = /usr/share/applications 46 | 47 | INSTALLS += desktop \ 48 | icon32 \ 49 | icon64 \ 50 | icon128 \ 51 | icon256 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #NeteaseMusicForLinux 2 | 3 | [![Join the chat at https://gitter.im/hualet/NeteaseMusicForLinux](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hualet/NeteaseMusicForLinux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 网易云音乐的Linux版本 5 | 6 | ![Imgur](http://i.imgur.com/KDG8KVX.png =500x300) 7 | ![Imgur](http://i.imgur.com/B9CuJak.png =500x300) 8 | 9 | ##功能: 10 | 1. 热门精选 11 | 2. 排行榜 12 | 3. 网友歌单 13 | 4. 用户登录 14 | 15 | ##后期功能 16 | 1. 收藏功能 17 | 3. 新歌速递 18 | 4. 最新音乐 19 | 20 | ##安装 21 | ###手动安装 22 | mkdir build; cd build 23 | qmake ..; make; sudo make install 24 | sudo gtk-update-icon-cache /usr/share/icons/hicolor/ 25 | ###deb安装 26 | 64位 http://pan.baidu.com/s/1bn4gKwB 27 | 28 | 安装完成后,程序就会出现在你的launcher里面了,尽情享用吧 ;) 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | netease-music (0.1-1) trusty; urgency=low 2 | 3 | * Initial release. 4 | 5 | -- hualet Sun, 19 Apr 2015 18:52:34 +0800 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: netease-music 2 | Section: x11/net 3 | Priority: optional 4 | Maintainer: hualet 5 | Build-Depends: debhelper (>= 9) 6 | Standards-Version: 3.9.5 7 | Homepage: https://git.oschina.net/hualet/NeteaseMusicForLinux 8 | 9 | Package: netease-music 10 | Architecture: any 11 | Depends: ${shlibs:Depends}, ${misc:Depends} 12 | Description: 网易云音乐Linux客户端 13 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # See debhelper(7) (uncomment to enable) 3 | # output every command that modifies files on the build system. 4 | #DH_VERBOSE = 1 5 | 6 | # see EXAMPLES in dpkg-buildflags(1) and read /usr/share/dpkg/* 7 | DPKG_EXPORT_BUILDFLAGS = 1 8 | include /usr/share/dpkg/default.mk 9 | 10 | # see FEATURE AREAS in dpkg-buildflags(1) 11 | #export DEB_BUILD_MAINT_OPTIONS = hardening=+all 12 | 13 | # see ENVIRONMENT in dpkg-buildflags(1) 14 | # package maintainers to append CFLAGS 15 | #export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic 16 | # package maintainers to append LDFLAGS 17 | #export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed 18 | 19 | 20 | # main packaging script based on dh7 syntax 21 | %: 22 | dh $@ 23 | 24 | # debmake generated override targets 25 | # This is example for Cmake (See http://bugs.debian.org/641051 ) 26 | #override_dh_auto_configure: 27 | # dh_auto_configure -- \ 28 | # -DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH) 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /icons/128x128/NeteaseMusicForLinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/icons/128x128/NeteaseMusicForLinux.png -------------------------------------------------------------------------------- /icons/256x256/NeteaseMusicForLinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/icons/256x256/NeteaseMusicForLinux.png -------------------------------------------------------------------------------- /icons/32x32/NeteaseMusicForLinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/icons/32x32/NeteaseMusicForLinux.png -------------------------------------------------------------------------------- /icons/64x64/NeteaseMusicForLinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/icons/64x64/NeteaseMusicForLinux.png -------------------------------------------------------------------------------- /images.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | images/next_hover_pressed.png 4 | images/next_normal.png 5 | images/pause_hover_pressed.png 6 | images/pause_normal.png 7 | images/play_hover_pressed.png 8 | images/play_normal.png 9 | images/prev_hover_pressed.png 10 | images/prev_normal.png 11 | images/muted_hover_pressed.png 12 | images/muted_normal.png 13 | images/volume_hover_pressed.png 14 | images/volume_normal.png 15 | images/disc_disc.png 16 | images/disc_mask.png 17 | images/disc_needle.png 18 | images/cancel_fullscreen.png 19 | images/headphone.png 20 | images/user_icon.png 21 | images/left_arrow_hover.png 22 | images/left_arrow_inactive.png 23 | images/left_arrow_normal.png 24 | images/right_arrow_hover.png 25 | images/right_arrow_inactive.png 26 | images/right_arrow_normal.png 27 | images/playlist_hover_pressed.png 28 | images/playlist_normal.png 29 | 30 | 31 | -------------------------------------------------------------------------------- /images/cancel_fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/cancel_fullscreen.png -------------------------------------------------------------------------------- /images/disc_disc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/disc_disc.png -------------------------------------------------------------------------------- /images/disc_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/disc_mask.png -------------------------------------------------------------------------------- /images/disc_needle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/disc_needle.png -------------------------------------------------------------------------------- /images/headphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/headphone.png -------------------------------------------------------------------------------- /images/left_arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/left_arrow_hover.png -------------------------------------------------------------------------------- /images/left_arrow_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/left_arrow_inactive.png -------------------------------------------------------------------------------- /images/left_arrow_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/left_arrow_normal.png -------------------------------------------------------------------------------- /images/muted_hover_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/muted_hover_pressed.png -------------------------------------------------------------------------------- /images/muted_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/muted_normal.png -------------------------------------------------------------------------------- /images/next_hover_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/next_hover_pressed.png -------------------------------------------------------------------------------- /images/next_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/next_normal.png -------------------------------------------------------------------------------- /images/pause_hover_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/pause_hover_pressed.png -------------------------------------------------------------------------------- /images/pause_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/pause_normal.png -------------------------------------------------------------------------------- /images/play_hover_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/play_hover_pressed.png -------------------------------------------------------------------------------- /images/play_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/play_normal.png -------------------------------------------------------------------------------- /images/playlist_hover_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/playlist_hover_pressed.png -------------------------------------------------------------------------------- /images/playlist_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/playlist_normal.png -------------------------------------------------------------------------------- /images/prev_hover_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/prev_hover_pressed.png -------------------------------------------------------------------------------- /images/prev_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/prev_normal.png -------------------------------------------------------------------------------- /images/right_arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/right_arrow_hover.png -------------------------------------------------------------------------------- /images/right_arrow_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/right_arrow_inactive.png -------------------------------------------------------------------------------- /images/right_arrow_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/right_arrow_normal.png -------------------------------------------------------------------------------- /images/user_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/user_icon.png -------------------------------------------------------------------------------- /images/volume_hover_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/volume_hover_pressed.png -------------------------------------------------------------------------------- /images/volume_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hualet/NeteaseMusicForLinux/f0662203658add8c48f474cc67a19da88fe12339/images/volume_normal.png -------------------------------------------------------------------------------- /qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | src/qmls/main.qml 4 | src/qmls/Header.qml 5 | src/qmls/Footer.qml 6 | src/qmls/PlaylistIconView.qml 7 | src/qmls/HTTabView.qml 8 | src/qmls/widgets/HSep.qml 9 | src/qmls/widgets/VSep.qml 10 | src/qmls/HTTabContent.qml 11 | src/qmls/SongsListView.qml 12 | src/qmls/HTImageButton.qml 13 | src/qmls/utils.js 14 | src/qmls/HTVolumeButton.qml 15 | src/qmls/widgets/RunningDisc.qml 16 | src/qmls/PlayView.qml 17 | src/qmls/SideBar.qml 18 | src/qmls/MainController.qml 19 | src/qmls/widgets/HTProgressBar.qml 20 | src/qmls/widgets/HTLyricView.qml 21 | src/qmls/widgets/HTBannersView.qml 22 | src/qmls/PlaylistDetialView.qml 23 | src/qmls/widgets/HTTextButton.qml 24 | src/qmls/ViewHistoryManager.qml 25 | src/qmls/widgets/BackForwardButton.qml 26 | src/qmls/PlaylistView.qml 27 | src/qmls/LoginDialog.qml 28 | src/qmls/widgets/HTTextInput.qml 29 | src/qmls/widgets/HTSectionTitle.qml 30 | src/qmls/SideBarPlaylists.qml 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/appcontroller.cpp: -------------------------------------------------------------------------------- 1 | #include "appcontroller.h" 2 | #include 3 | 4 | AppController::AppController(QObject *parent) : 5 | QObject(parent) 6 | { 7 | m_api = new NeteaseAPI(this); 8 | m_database = new Database(this); 9 | m_playlistModel = new PlaylistModel(m_database, this); 10 | 11 | connect(m_api, &NeteaseAPI::loginSucceed, this, &AppController::loginSucceed); 12 | connect(m_api, &NeteaseAPI::loginFailed, this, &AppController::loginFailed); 13 | connect(m_api, &NeteaseAPI::userPlaylistGot, this, &AppController::userPlaylistsGot); 14 | connect(m_api, &NeteaseAPI::topPlaylistGot, this, &AppController::topPlaylistsGot); 15 | connect(m_api, &NeteaseAPI::playlistDetailGot, this, &AppController::playlistDetailGot); 16 | connect(m_api, &NeteaseAPI::rankingListsGot, this, &AppController::rankingListsGot); 17 | connect(m_api, &NeteaseAPI::lyricGot, this, &AppController::lyricGot); 18 | connect(m_api, &NeteaseAPI::hotspotGot, this, &AppController::hotspotGot); 19 | connect(m_api, &NeteaseAPI::bannersGot, this, &AppController::bannersGot); 20 | } 21 | 22 | void AppController::login(QString account, QString password) 23 | { 24 | m_api->login(account, password); 25 | } 26 | 27 | void AppController::getUserPlaylists(QString uid) { 28 | m_api->userPlaylist(uid); 29 | } 30 | 31 | void AppController::getTopPlaylists() 32 | { 33 | m_api->topPlaylist(); 34 | } 35 | 36 | void AppController::getPlaylistDetail(QString playlistId) 37 | { 38 | m_api->playlistDetail(playlistId); 39 | } 40 | 41 | void AppController::getRankingLists() 42 | { 43 | m_api->rankingLists(); 44 | } 45 | 46 | void AppController::getLyric(QString songId) 47 | { 48 | m_api->getLyric(songId); 49 | } 50 | 51 | void AppController::getHotspot() 52 | { 53 | m_api->hotspot(); 54 | } 55 | 56 | void AppController::getBanners() 57 | { 58 | m_api->getBanners(); 59 | } 60 | 61 | 62 | void AppController::addPlaylistItem(QString id, QString name, QString mp3Url, 63 | QString picUrl, QString artist, QString album, int duration) 64 | { 65 | m_playlistModel->addSong(id, name, mp3Url, picUrl, artist, album, duration); 66 | } 67 | 68 | Song* AppController::getNextPlaylistItem(QString id) 69 | { 70 | return m_playlistModel->getNextSong(id); 71 | } 72 | 73 | Song* AppController::getPlaylistItemById(QString id) 74 | { 75 | return m_playlistModel->getSongById(id); 76 | } 77 | 78 | PlaylistModel *AppController::playlistModel() const 79 | { 80 | return m_playlistModel; 81 | } 82 | 83 | void AppController::setPlaylistModel(PlaylistModel *playlistModel) 84 | { 85 | if (m_playlistModel != playlistModel) { 86 | m_playlistModel = playlistModel; 87 | emit playlistModelChanged(); 88 | } 89 | } 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/appcontroller.h: -------------------------------------------------------------------------------- 1 | #ifndef APPCONTROLLER_H 2 | #define APPCONTROLLER_H 3 | 4 | #include 5 | #include "netease_api/neteaseapi.h" 6 | #include "playlist_model.h" 7 | #include "song.h" 8 | #include "database.h" 9 | 10 | class AppController : public QObject 11 | { 12 | Q_OBJECT 13 | public: 14 | explicit AppController(QObject *parent = 0); 15 | 16 | Q_PROPERTY(PlaylistModel* playlistModel READ playlistModel WRITE setPlaylistModel NOTIFY playlistModelChanged) 17 | 18 | PlaylistModel *playlistModel() const; 19 | void setPlaylistModel(PlaylistModel *playlistModel); 20 | 21 | signals: 22 | void userIdChanged(); 23 | void playlistModelChanged(); 24 | 25 | void loginSucceed(QString info); 26 | void loginFailed(); 27 | void userPlaylistsGot(QString userPlaylists); 28 | void topPlaylistsGot(QString playlists); 29 | void playlistDetailGot(QString detail); 30 | void rankingListsGot(QString lists); 31 | void lyricGot(QString lyric); 32 | void hotspotGot(QString hotspot); 33 | void bannersGot(QString banners); 34 | 35 | public slots: 36 | void login(QString, QString); 37 | void getUserPlaylists(QString); 38 | void getTopPlaylists(); 39 | void getPlaylistDetail(QString); 40 | void getRankingLists(); 41 | void getLyric(QString); 42 | void getHotspot(); 43 | void getBanners(); 44 | 45 | void addPlaylistItem(QString id, QString name, QString mp3Url, 46 | QString picUrl, QString artist, QString album, int duration); 47 | Song* getNextPlaylistItem(QString id); 48 | Song* getPlaylistItemById(QString id); 49 | 50 | private: 51 | NeteaseAPI* m_api; 52 | PlaylistModel* m_playlistModel; 53 | Database *m_database; 54 | }; 55 | 56 | #endif // APPCONTROLLER_H 57 | -------------------------------------------------------------------------------- /src/database.cpp: -------------------------------------------------------------------------------- 1 | #include "database.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | Database::Database(QObject *parent) : 10 | QObject(parent) 11 | { 12 | QString configDir = QDir::home().filePath(".config"); 13 | QDir(configDir).mkdir("NeteaseMusicForLinux"); 14 | QString appConfigDir = QDir(configDir).filePath("NeteaseMusicForLinux"); 15 | m_fileName = QDir(appConfigDir).filePath("playlist.db"); 16 | 17 | initDatabase(); 18 | initDelayCommitTimer(); 19 | } 20 | 21 | void Database::initDatabase() 22 | { 23 | QFile file(m_fileName); 24 | if (!file.exists()) { 25 | file.open(QFile::WriteOnly); 26 | file.close(); 27 | } 28 | 29 | QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); 30 | db.setDatabaseName(m_fileName); 31 | db.open(); 32 | 33 | QSqlQuery query; 34 | query.exec("CREATE TABLE IF NOT EXISTS PLAYLIST (" 35 | "ID CHAR PRIMARY KEY," 36 | "NAME CHAR," 37 | "ARTIST CHAR," 38 | "ALBUM CHAR," 39 | "MP3URL CHAR," 40 | "PICURL CHAR," 41 | "DURATION LONG)"); 42 | } 43 | 44 | void Database::initDelayCommitTimer() 45 | { 46 | m_delayCommitTimer = new QTimer(this); 47 | m_delayCommitTimer->setInterval(500); 48 | m_delayCommitTimer->setSingleShot(true); 49 | 50 | connect(m_delayCommitTimer, &QTimer::timeout, this, &Database::delayCommitTimerTimeout); 51 | } 52 | 53 | void Database::addPlaylistItem(Song *song) 54 | { 55 | QSqlDatabase db = QSqlDatabase::database(); 56 | db.transaction(); 57 | 58 | QSqlQuery query; 59 | query.prepare("INSERT INTO PLAYLIST (ID, NAME, ARTIST, ALBUM, MP3URL, PICURL, DURATION)" 60 | "VALUES (?, ?, ?, ?, ?, ?, ?)"); 61 | query.addBindValue(song->id()); 62 | query.addBindValue(song->name()); 63 | query.addBindValue(song->artist()); 64 | query.addBindValue(song->album()); 65 | query.addBindValue(song->mp3Url()); 66 | query.addBindValue(song->picUrl()); 67 | query.addBindValue(song->duration()); 68 | query.exec(); 69 | 70 | m_delayCommitTimer->start(); 71 | } 72 | 73 | QList Database::getPlaylistItems() 74 | { 75 | QList list; 76 | 77 | QSqlQuery query("SELECT * FROM PLAYLIST"); 78 | while (query.next()) { 79 | QString id = query.value(0).toString(); 80 | QString name = query.value(1).toString(); 81 | QString artist = query.value(2).toString(); 82 | QString album = query.value(3).toString(); 83 | QString mp3Url = query.value(4).toString(); 84 | QString picUrl = query.value(5).toString(); 85 | int duration = query.value(6).toInt(); 86 | 87 | Song *song = new Song(id, name, mp3Url, picUrl, 88 | artist, album, duration, this); 89 | list << song; 90 | } 91 | 92 | return list; 93 | } 94 | 95 | void Database::delayCommitTimerTimeout() 96 | { 97 | qDebug() << "delaycommitTimerTimeout"; 98 | QSqlDatabase::database().commit(); 99 | } 100 | -------------------------------------------------------------------------------- /src/database.h: -------------------------------------------------------------------------------- 1 | #ifndef DATABASE_H 2 | #define DATABASE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "song.h" 10 | 11 | class Database : public QObject 12 | { 13 | Q_OBJECT 14 | public: 15 | explicit Database(QObject *parent = 0); 16 | 17 | void addPlaylistItem(Song* song); 18 | QList getPlaylistItems(); 19 | 20 | void delayCommitTimerTimeout(); 21 | 22 | private: 23 | QString m_fileName; 24 | QTimer *m_delayCommitTimer; 25 | 26 | void initDatabase(); 27 | void initDelayCommitTimer(); 28 | }; 29 | 30 | #endif // DATABASE_H 31 | -------------------------------------------------------------------------------- /src/htsettings.cpp: -------------------------------------------------------------------------------- 1 | #include "htsettings.h" 2 | #include 3 | 4 | HTSettings::HTSettings(QObject *parent) : 5 | QSettings(parent) 6 | { 7 | } 8 | 9 | double HTSettings::volume() 10 | { 11 | return value("General/volume").toDouble(); 12 | } 13 | 14 | void HTSettings::setVolume(double volume) 15 | { 16 | setValue("General/volume", QVariant(volume)); 17 | } 18 | 19 | QString HTSettings::lastSong() const 20 | { 21 | return value("General/last_song").toString(); 22 | } 23 | 24 | void HTSettings::setLastSong(QString& lastSong) 25 | { 26 | setValue("General/last_song", QVariant(lastSong)); 27 | } 28 | 29 | QString HTSettings::userId() const 30 | { 31 | return value("General/user_id").toString(); 32 | } 33 | 34 | void HTSettings::setUserId(QString& userId) 35 | { 36 | setValue("General/user_id", QVariant(userId)); 37 | emit userIdChanged(); 38 | } 39 | 40 | QString HTSettings::userNickname() const 41 | { 42 | return value("General/user_nickname").toString(); 43 | } 44 | 45 | void HTSettings::setUserNickname(QString& nickname) 46 | { 47 | setValue("General/user_nickname", QVariant(nickname)); 48 | emit userNicknameChanged(); 49 | } 50 | 51 | QString HTSettings::userAvatarUrl() const 52 | { 53 | return value("General/user_avatar_url").toString(); 54 | } 55 | 56 | void HTSettings::setUserAvatarUrl(QString& avatarUrl) 57 | { 58 | setValue("General/user_avatar_url", QVariant(avatarUrl)); 59 | emit userAvatarUrlChanged(); 60 | } 61 | -------------------------------------------------------------------------------- /src/htsettings.h: -------------------------------------------------------------------------------- 1 | #ifndef HTSETTINGS_H 2 | #define HTSETTINGS_H 3 | 4 | #include 5 | 6 | class HTSettings : public QSettings 7 | { 8 | Q_OBJECT 9 | public: 10 | explicit HTSettings(QObject *parent = 0); 11 | 12 | Q_PROPERTY(double volume READ volume WRITE setVolume) 13 | Q_PROPERTY(QString lastSong READ lastSong WRITE setLastSong) 14 | Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged) 15 | Q_PROPERTY(QString userNickname READ userNickname WRITE setUserNickname NOTIFY userNicknameChanged) 16 | Q_PROPERTY(QString userAvatarUrl READ userAvatarUrl WRITE setUserAvatarUrl NOTIFY userAvatarUrlChanged) 17 | 18 | double volume(); 19 | void setVolume(double volume); 20 | 21 | QString lastSong() const; 22 | void setLastSong(QString& lastSong); 23 | 24 | QString userId() const; 25 | void setUserId(QString& userId); 26 | 27 | QString userNickname() const; 28 | void setUserNickname(QString& nickname); 29 | 30 | QString userAvatarUrl() const; 31 | void setUserAvatarUrl(QString& avatarUrl); 32 | 33 | signals: 34 | void userIdChanged(); 35 | void userNicknameChanged(); 36 | void userAvatarUrlChanged(); 37 | }; 38 | 39 | #endif // HTSETTINGS_H 40 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "appcontroller.h" 7 | #include "song.h" 8 | #include "playlist_model.h" 9 | #include "htsettings.h" 10 | #include "database.h" 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | QGuiApplication app(argc, argv); 15 | app.setOrganizationName("Hualet"); 16 | app.setOrganizationDomain("hualet.org"); 17 | app.setApplicationName("NeteaseMusicForLinux"); 18 | 19 | AppController controller; 20 | HTSettings settings; 21 | Database database; 22 | 23 | qRegisterMetaType("PlaylistModel*"); 24 | qmlRegisterType("Org.Hualet.Widgets", 1, 0, "Song"); 25 | 26 | QQmlApplicationEngine engine; 27 | engine.rootContext()->setContextProperty("_controller", &controller); 28 | engine.rootContext()->setContextProperty("_settings", &settings); 29 | engine.load(QUrl(QStringLiteral("qrc:/src/qmls/main.qml"))); 30 | 31 | return app.exec(); 32 | } 33 | -------------------------------------------------------------------------------- /src/netease_api/neteaseapi.cpp: -------------------------------------------------------------------------------- 1 | #include "neteaseapi.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | NeteaseAPI::NeteaseAPI(QObject *parent): 14 | QObject(parent), 15 | m_networkManager(new QNetworkAccessManager(this)) 16 | { 17 | m_headerMap["Accept"] = "*/*"; 18 | // FixMe: I don't know how to deal with gzipped data with QNetworkReply. 19 | // m_headerMap["Accept-Encoding"] = "gzip,deflate,sdch"; 20 | m_headerMap["Accept-Language"] = "zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4"; 21 | m_headerMap["Connection"] = "keep-alive"; 22 | m_headerMap["Content-Type"] = "application/x-www-form-urlencoded"; 23 | m_headerMap["Host"] = "music.163.com"; 24 | m_headerMap["Referer"] = "http://music.163.com/search/"; 25 | m_headerMap["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36"; 26 | } 27 | 28 | NeteaseAPI::~NeteaseAPI() 29 | { 30 | } 31 | 32 | void NeteaseAPI::login(QString username, QString password) 33 | { 34 | QUrl url("http://music.163.com/api/login/"); 35 | QUrlQuery params; 36 | params.addQueryItem("username", username); 37 | params.addQueryItem("password", QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Md5).toHex()); 38 | params.addQueryItem("rememberLogin", "true"); 39 | 40 | QNetworkReply* reply = post(url, params.query(QUrl::FullyEncoded).toUtf8()); 41 | connect(reply, &QNetworkReply::finished, this, &NeteaseAPI::handleLoginFinished); 42 | } 43 | 44 | void NeteaseAPI::userPlaylist(QString uid) 45 | { 46 | QUrl url = QString("http://music.163.com/api/user/playlist"); 47 | QUrlQuery query; 48 | query.addQueryItem("offset", QString::number(0)); 49 | query.addQueryItem("limit", QString::number(100)); 50 | query.addQueryItem("uid", uid); 51 | url.setQuery(query.toString(QUrl::FullyEncoded)); 52 | 53 | QNetworkReply* reply = get(url); 54 | connect(reply, &QNetworkReply::finished, this, &NeteaseAPI::handleUserPlaylistFinished); 55 | } 56 | 57 | void NeteaseAPI::topPlaylist(QString category, QString order, quint8 offset, quint8 limit) 58 | { 59 | QUrl url = QString("http://music.163.com/api/playlist/list"); 60 | QUrlQuery query; 61 | query.addQueryItem("cat", category); 62 | query.addQueryItem("order", order); 63 | query.addQueryItem("offset", QString::number(offset)); 64 | query.addQueryItem("total", offset ? "false" : "true"); 65 | query.addQueryItem("limit", QString::number(limit)); 66 | url.setQuery(query.toString(QUrl::FullyEncoded)); 67 | 68 | QNetworkReply* reply = get(url); 69 | connect(reply, &QNetworkReply::finished, this, &NeteaseAPI::handleTopPlaylistFinished); 70 | } 71 | 72 | void NeteaseAPI::playlistDetail(QString playlistId) 73 | { 74 | QUrl url = QString("http://music.163.com/api/playlist/detail"); 75 | QUrlQuery query; 76 | query.addQueryItem("id", playlistId); 77 | url.setQuery(query.toString(QUrl::FullyEncoded)); 78 | 79 | QNetworkReply* reply = get(url); 80 | connect(reply, &QNetworkReply::finished, this, &NeteaseAPI::handlePlaylistDetailFinished); 81 | } 82 | 83 | void NeteaseAPI::rankingLists() 84 | { 85 | QUrl url = QString("http://music.163.com/api/toplist"); 86 | QUrlQuery query; 87 | url.setQuery(query.toString(QUrl::FullyEncoded)); 88 | 89 | QNetworkReply* reply = get(url); 90 | connect(reply, &QNetworkReply::finished, this, &NeteaseAPI::handleRankingListsFinished); 91 | } 92 | 93 | void NeteaseAPI::getLyric(QString songId) 94 | { 95 | QUrl url = QString("http://music.163.com/api/song/lyric"); 96 | QUrlQuery query; 97 | query.addQueryItem("os", "osx"); 98 | query.addQueryItem("id", songId); 99 | query.addQueryItem("lv", "-1"); 100 | query.addQueryItem("kv", "-1"); 101 | query.addQueryItem("tv", "-1"); 102 | url.setQuery(query.toString(QUrl::FullyEncoded)); 103 | 104 | QNetworkReply* reply = get(url); 105 | connect(reply, &QNetworkReply::finished, this, &NeteaseAPI::handleGetLyricFinished); 106 | } 107 | 108 | void NeteaseAPI::hotspot() 109 | { 110 | QUrl url = QString("http://music.163.com/api/discovery/hotspot"); 111 | QUrlQuery query; 112 | query.addQueryItem("limit", "12"); 113 | url.setQuery(query.toString(QUrl::FullyEncoded)); 114 | 115 | QNetworkReply* reply = get(url); 116 | connect(reply, &QNetworkReply::finished, this, &NeteaseAPI::handleHotspotFinished); 117 | } 118 | 119 | void NeteaseAPI::getBanners() 120 | { 121 | QUrl url = QString("http://music.163.com/api/banner/get"); 122 | QUrlQuery query; 123 | query.addQueryItem("limit", "8"); 124 | url.setQuery(query.toString(QUrl::FullyEncoded)); 125 | 126 | QNetworkReply* reply = get(url); 127 | connect(reply, &QNetworkReply::finished, this, &NeteaseAPI::handleGetBannersFinished); 128 | } 129 | 130 | // slots 131 | void NeteaseAPI::handleLoginFinished() 132 | { 133 | QNetworkReply *reply = qobject_cast(sender()); 134 | if (!reply->error()) { 135 | QByteArray array = reply->readAll(); 136 | 137 | QJsonDocument document = QJsonDocument::fromJson(array); 138 | QJsonObject object = document.object(); 139 | // TODO: save the info or something here 140 | emit loginSucceed(QString(document.toJson())); 141 | } else { 142 | emit loginFailed(); 143 | qDebug() << "handleLoginFinished error" << reply->errorString(); 144 | } 145 | } 146 | 147 | void NeteaseAPI::handleUserPlaylistFinished() 148 | { 149 | QNetworkReply *reply = qobject_cast(sender()); 150 | 151 | if (!reply->error()) { 152 | QByteArray array = reply->readAll(); 153 | QJsonDocument document = QJsonDocument::fromJson(array); 154 | QJsonObject object = document.object(); 155 | QJsonArray playlist = object["playlist"].toArray(); 156 | 157 | if (!playlist.isEmpty()) { 158 | QJsonDocument document(playlist); 159 | emit userPlaylistGot(QString(document.toJson())); 160 | } else { 161 | qDebug() << "No user playlists found!"; 162 | } 163 | } else { 164 | qWarning() << "handleUserPlaylistFinished" << reply->errorString(); 165 | } 166 | } 167 | 168 | void NeteaseAPI::handleTopPlaylistFinished() 169 | { 170 | QNetworkReply *reply = qobject_cast(sender()); 171 | 172 | if (!reply->error()) { 173 | QByteArray array = reply->readAll(); 174 | // qDebug() << array; 175 | QJsonDocument document = QJsonDocument::fromJson(array); 176 | QJsonObject object = document.object(); 177 | QJsonArray playlists = object["playlists"].toArray(); 178 | 179 | if (!playlists.isEmpty()) { 180 | QJsonDocument document(playlists); 181 | emit topPlaylistGot(QString(document.toJson())); 182 | } else { 183 | qDebug() << "No top playlists found!"; 184 | } 185 | } else { 186 | qWarning() << "handleTopPlaylistFinished" << reply->errorString(); 187 | } 188 | } 189 | 190 | void NeteaseAPI::handlePlaylistDetailFinished() 191 | { 192 | QNetworkReply *reply = qobject_cast(sender()); 193 | 194 | if (!reply->error()) { 195 | QByteArray array = reply->readAll(); 196 | QJsonDocument document = QJsonDocument::fromJson(array); 197 | QJsonObject object = document.object(); 198 | QJsonObject result = object["result"].toObject(); 199 | 200 | if (!result.isEmpty()) { 201 | QJsonDocument document(result); 202 | emit playlistDetailGot(QString(document.toJson())); 203 | } else { 204 | qDebug() << "No detail found!"; 205 | } 206 | } else { 207 | qWarning() << "handlePlaylistDetailFinished" << reply->errorString(); 208 | } 209 | } 210 | 211 | void NeteaseAPI::handleRankingListsFinished() 212 | { 213 | QNetworkReply *reply = qobject_cast(sender()); 214 | 215 | if (!reply->error()) { 216 | QByteArray array = reply->readAll(); 217 | QJsonDocument document = QJsonDocument::fromJson(array); 218 | QJsonObject object = document.object(); 219 | QJsonArray list = object["list"].toArray(); 220 | 221 | if (!list.isEmpty()) { 222 | QJsonDocument document(list); 223 | emit rankingListsGot(QString(document.toJson())); 224 | } else { 225 | qDebug() << "No ranking list found!"; 226 | } 227 | } else { 228 | qWarning() << "handleRankingListsFinished" << reply->errorString(); 229 | } 230 | } 231 | 232 | void NeteaseAPI::handleGetLyricFinished() 233 | { 234 | QNetworkReply *reply = qobject_cast(sender()); 235 | 236 | if (!reply->error()) { 237 | QByteArray array = reply->readAll(); 238 | QJsonDocument document = QJsonDocument::fromJson(array); 239 | QJsonObject object = document.object(); 240 | QJsonObject lrc = object["lrc"].toObject(); 241 | 242 | if (!lrc.isEmpty()) { 243 | emit lyricGot(lrc["lyric"].toString()); 244 | } else { 245 | qDebug() << "No lyric found!"; 246 | } 247 | } else { 248 | qWarning() << "handleGetLyricFinished" << reply->errorString(); 249 | } 250 | } 251 | 252 | void NeteaseAPI::handleHotspotFinished() 253 | { 254 | QNetworkReply *reply = qobject_cast(sender()); 255 | 256 | if (!reply->error()) { 257 | QByteArray array = reply->readAll(); 258 | QJsonDocument document = QJsonDocument::fromJson(array); 259 | QJsonObject object = document.object(); 260 | QJsonArray playlists = object["data"].toArray(); 261 | 262 | if (!playlists.isEmpty()) { 263 | QJsonDocument document(playlists); 264 | emit hotspotGot(QString(document.toJson())); 265 | } else { 266 | qDebug() << "No hotspot found!"; 267 | } 268 | } else { 269 | qWarning() << "handleHotspotFinished" << reply->errorString(); 270 | } 271 | } 272 | 273 | void NeteaseAPI::handleGetBannersFinished() 274 | { 275 | QNetworkReply *reply = qobject_cast(sender()); 276 | 277 | if (!reply->error()) { 278 | QByteArray array = reply->readAll(); 279 | QJsonDocument document = QJsonDocument::fromJson(array); 280 | QJsonObject object = document.object(); 281 | QJsonArray banners = object["banners"].toArray(); 282 | 283 | if (!banners.isEmpty()) { 284 | QJsonDocument document(banners); 285 | emit bannersGot(QString(document.toJson())); 286 | } else { 287 | qDebug() << "No banners found!"; 288 | } 289 | } else { 290 | qWarning() << "handleGetBannersFinished" << reply->errorString(); 291 | } 292 | } 293 | 294 | // private methods 295 | void NeteaseAPI::setHeaderForRequest(QNetworkRequest &request) 296 | { 297 | QMapIterator i(m_headerMap); 298 | while (i.hasNext()) { 299 | i.next(); 300 | request.setRawHeader(i.key().toUtf8(), i.value().toUtf8()); 301 | } 302 | } 303 | 304 | QNetworkReply* NeteaseAPI::get(const QUrl& url) 305 | { 306 | QNetworkRequest request(url); 307 | setHeaderForRequest(request); 308 | return m_networkManager->get(request); 309 | } 310 | 311 | QNetworkReply* NeteaseAPI::post(const QUrl& url, const QByteArray& data) 312 | { 313 | QNetworkRequest request(url); 314 | setHeaderForRequest(request); 315 | return m_networkManager->post(request, data); 316 | } 317 | -------------------------------------------------------------------------------- /src/netease_api/neteaseapi.h: -------------------------------------------------------------------------------- 1 | #ifndef NETEASEAPI_H 2 | #define NETEASEAPI_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class QNetworkAccessManager; 9 | class QNetworkRequest; 10 | class QNetworkReply; 11 | class QUrl; 12 | class NeteaseAPI: public QObject 13 | { 14 | Q_OBJECT 15 | public: 16 | NeteaseAPI(QObject *parent=0); 17 | ~NeteaseAPI(); 18 | 19 | void login(QString, QString); 20 | void userPlaylist(QString); 21 | void topPlaylist(QString category="全部", QString order="hot", quint8 offset=0, quint8 limit=50); 22 | void playlistDetail(QString); 23 | void rankingLists(); 24 | void getLyric(QString); 25 | void hotspot(); 26 | void getBanners(); 27 | 28 | signals: 29 | void loginSucceed(QString info); 30 | void loginFailed(); 31 | void userPlaylistGot(QString playlist); 32 | void topPlaylistGot(QString playlists); 33 | void playlistDetailGot(QString detail); 34 | void rankingListsGot(QString lists); 35 | void lyricGot(QString lyric); 36 | void hotspotGot(QString hotspot); 37 | void bannersGot(QString banners); 38 | 39 | private slots: 40 | void handleLoginFinished(); 41 | void handleUserPlaylistFinished(); 42 | void handleTopPlaylistFinished(); 43 | void handlePlaylistDetailFinished(); 44 | void handleRankingListsFinished(); 45 | void handleGetLyricFinished(); 46 | void handleHotspotFinished(); 47 | void handleGetBannersFinished(); 48 | 49 | private: 50 | QNetworkAccessManager *m_networkManager; 51 | QMap m_headerMap; 52 | 53 | void setHeaderForRequest(QNetworkRequest& request); 54 | 55 | QNetworkReply* get(const QUrl&); 56 | QNetworkReply* post(const QUrl&, const QByteArray& data); 57 | }; 58 | 59 | #endif // NETEASEAPI_H 60 | -------------------------------------------------------------------------------- /src/playlist_model.cpp: -------------------------------------------------------------------------------- 1 | #include "playlist_model.h" 2 | 3 | #include 4 | 5 | PlaylistModel::PlaylistModel(Database* database, QObject *parent) : 6 | QAbstractListModel(parent), 7 | m_database(database) 8 | { 9 | m_songs.append(m_database->getPlaylistItems()); 10 | } 11 | 12 | void PlaylistModel::addSong(QString id, QString name, QString mp3Url, 13 | QString picUrl, QString artist, QString album, int duration) 14 | { 15 | Song *song = new Song(id, name, mp3Url, picUrl, artist, album, duration, this); 16 | 17 | beginInsertRows(QModelIndex(), rowCount(), rowCount()); 18 | m_songs << song; 19 | m_database->addPlaylistItem(song); 20 | endInsertRows(); 21 | } 22 | 23 | Song* PlaylistModel::getSongById(QString id) 24 | { 25 | for (int i = 0; i < m_songs.size(); ++i) { 26 | if (m_songs.at(i)->id() == id) { 27 | return m_songs.at(i); 28 | } 29 | } 30 | return NULL; 31 | } 32 | 33 | Song* PlaylistModel::getNextSong(QString id) 34 | { 35 | int index = -1; 36 | for (int i = 0; i < m_songs.size(); ++i) { 37 | if (m_songs.at(i)->id() == id) { 38 | index = i; 39 | break; 40 | } 41 | } 42 | 43 | if (0 <= index && index <= m_songs.size() - 2) { 44 | return m_songs.at(index + 1); 45 | } else { 46 | return NULL; 47 | } 48 | } 49 | 50 | Song* PlaylistModel::getSongAtIndex(int index) 51 | { 52 | return m_songs.at(index); 53 | } 54 | 55 | 56 | int PlaylistModel::rowCount(const QModelIndex &) const { 57 | return m_songs.count(); 58 | } 59 | 60 | QVariant PlaylistModel::data(const QModelIndex & index, int role) const { 61 | if (index.row() < 0 || index.row() > m_songs.count()) 62 | return QVariant(); 63 | 64 | Song *song = m_songs[index.row()]; 65 | 66 | switch (role) { 67 | case IdRole: 68 | return song->id(); 69 | case NameRole: 70 | return song->name(); 71 | case Mp3UrlRole: 72 | return song->mp3Url(); 73 | case PicUrlRole: 74 | return song->picUrl(); 75 | case ArtistRole: 76 | return song->artist(); 77 | case AlbumRole: 78 | return song->album(); 79 | case DurationRole: 80 | return song->duration(); 81 | } 82 | 83 | return QVariant(); 84 | } 85 | 86 | QHash PlaylistModel::roleNames() const 87 | { 88 | QHash roles; 89 | roles[IdRole] = "id"; 90 | roles[NameRole] = "name"; 91 | roles[Mp3UrlRole] = "mp3Url"; 92 | roles[PicUrlRole] = "picUrl"; 93 | roles[ArtistRole] = "artist"; 94 | roles[AlbumRole] = "album"; 95 | roles[DurationRole] = "duration"; 96 | return roles; 97 | } 98 | -------------------------------------------------------------------------------- /src/playlist_model.h: -------------------------------------------------------------------------------- 1 | #ifndef PLAYLIST_MODEL_H 2 | #define PLAYLIST_MODEL_H 3 | 4 | #include 5 | #include 6 | 7 | #include "song.h" 8 | #include "database.h" 9 | 10 | class PlaylistModel : public QAbstractListModel 11 | { 12 | Q_OBJECT 13 | public: 14 | enum PlaylistRoles { 15 | IdRole = Qt::UserRole + 1, 16 | NameRole, 17 | Mp3UrlRole, 18 | PicUrlRole, 19 | ArtistRole, 20 | AlbumRole, 21 | DurationRole, 22 | }; 23 | 24 | explicit PlaylistModel(Database *database, QObject *parent = 0); 25 | 26 | void addSong(QString id, QString name, QString mp3Url, 27 | QString picUrl, QString artist, QString album, int duration); 28 | Song* getNextSong(QString id); 29 | Song* getSongById(QString id); 30 | 31 | int rowCount(const QModelIndex & parent = QModelIndex()) const; 32 | 33 | QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; 34 | 35 | QHash roleNames() const; 36 | 37 | public slots: 38 | Song* getSongAtIndex(int index); 39 | 40 | private: 41 | QList m_songs; 42 | Database* m_database; 43 | }; 44 | 45 | #endif // PLAYLIST_MODEL_H 46 | -------------------------------------------------------------------------------- /src/qmls/Footer.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import "../qmls/widgets" 4 | 5 | Item { 6 | id: root 7 | width: 100 8 | height: 50 9 | 10 | property alias playing: play_pause_button.playing 11 | property alias progress: progress_bar.progress 12 | property alias timeInfo: time_info.text 13 | property alias volume: volume_button.volume 14 | property alias muted: volume_button.muted 15 | 16 | signal mutedSet(bool muted) 17 | signal play() 18 | signal pause() 19 | signal seek(real progress) 20 | signal volumeSet(real volume) 21 | signal playPrev() 22 | signal playNext() 23 | signal togglePlaylist() 24 | 25 | Rectangle { 26 | anchors.fill: parent 27 | color: "red" 28 | } 29 | 30 | Rectangle { 31 | anchors.fill: parent 32 | color: Qt.rgba(0, 0, 0, 0.1) 33 | } 34 | 35 | Row { 36 | id: button_row 37 | height: parent.height 38 | spacing: 15 39 | anchors.left: parent.left 40 | anchors.leftMargin: 20 41 | 42 | HTImageButton { 43 | normalImage: "qrc:/images/prev_normal.png" 44 | hoverPressedImage: "qrc:/images/prev_hover_pressed.png" 45 | 46 | anchors.verticalCenter: parent.verticalCenter 47 | 48 | onClicked: root.playPrev() 49 | } 50 | 51 | HTImageButton { 52 | id: play_pause_button 53 | normalImage: playing ? "qrc:/images/pause_normal.png" : "qrc:/images/play_normal.png" 54 | hoverPressedImage: playing ? "qrc:/images/pause_hover_pressed.png" : "qrc:/images/play_hover_pressed.png" 55 | 56 | property bool playing: false 57 | 58 | anchors.verticalCenter: parent.verticalCenter 59 | 60 | onClicked: playing ? root.pause() : root.play() 61 | } 62 | 63 | HTImageButton { 64 | normalImage: "qrc:/images/next_normal.png" 65 | hoverPressedImage: "qrc:/images/next_hover_pressed.png" 66 | 67 | anchors.verticalCenter: parent.verticalCenter 68 | 69 | onClicked: root.playNext() 70 | } 71 | } 72 | 73 | HTProgressBar { 74 | id: progress_bar 75 | width: 300 76 | anchors.left: button_row.right 77 | anchors.leftMargin: 30 78 | anchors.verticalCenter: parent.verticalCenter 79 | 80 | onSeek: root.seek(progress) 81 | } 82 | 83 | Text { 84 | id: time_info 85 | color: "white" 86 | anchors.left: progress_bar.right 87 | anchors.leftMargin: 10 88 | anchors.verticalCenter: parent.verticalCenter 89 | } 90 | 91 | HTVolumeButton { 92 | id: volume_button 93 | anchors.left: time_info.right 94 | anchors.leftMargin: 20 95 | anchors.verticalCenter: parent.verticalCenter 96 | 97 | onMutedSet: root.mutedSet(muted) 98 | onVolumeSet: root.volumeSet(volume) 99 | } 100 | 101 | HTImageButton { 102 | normalImage: "qrc:/images/playlist_normal.png" 103 | hoverPressedImage: "qrc:/images/playlist_hover_pressed.png" 104 | anchors.right: parent.right 105 | anchors.rightMargin: 50 106 | anchors.verticalCenter: parent.verticalCenter 107 | 108 | onClicked: root.togglePlaylist() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/qmls/HTImageButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | MouseArea { 4 | width: img.implicitWidth 5 | height: img.implicitHeight 6 | hoverEnabled: true 7 | state: "normal" 8 | 9 | property url normalImage 10 | property url hoverPressedImage 11 | property url inactiveImage 12 | 13 | onEnabledChanged: state = "normal" 14 | 15 | states: [ 16 | State { 17 | name: "normal" 18 | PropertyChanges { 19 | target: img 20 | source: enabled ? normalImage : inactiveImage 21 | } 22 | }, 23 | State { 24 | name: "hover_pressed" 25 | PropertyChanges { 26 | target: img 27 | source: hoverPressedImage 28 | } 29 | } 30 | ] 31 | 32 | Image { id: img; anchors.fill: parent } 33 | 34 | onEntered: state = "hover_pressed" 35 | onExited: state = "normal" 36 | onPressed: state = "hover_pressed" 37 | onReleased: state = (containsMouse ? "hover_pressed" : "normal") 38 | } 39 | -------------------------------------------------------------------------------- /src/qmls/HTTabContent.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.2 3 | 4 | Flickable { 5 | id: root 6 | clip: true 7 | default property alias contents: item.children 8 | 9 | Item { 10 | id: place_holder 11 | width: root.width 12 | height: 30 13 | } 14 | 15 | Item { 16 | id: item 17 | clip: true 18 | width: root.width 19 | height: root.contentHeight - place_holder.height 20 | anchors.top: place_holder.bottom 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/qmls/HTTabView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import QtQuick.Controls 1.2 3 | import QtQuick.Controls.Styles 1.2 4 | 5 | import "../qmls/widgets/" 6 | 7 | TabView { 8 | id: frame 9 | width: 300 10 | height: 300 11 | 12 | style: TabViewStyle { 13 | tabsAlignment: Qt.AlignHCenter 14 | tab: Item { 15 | implicitWidth: Math.max(text.width + 4, 80) 16 | implicitHeight: 40 17 | Text { 18 | id: text 19 | anchors.centerIn: parent 20 | text: styleData.title 21 | color: styleData.selected ? "red" : "black" 22 | } 23 | } 24 | frame: Item { 25 | HSep { width: parent.width } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/qmls/HTVolumeButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import "../qmls/widgets" 4 | 5 | Item { 6 | id: root 7 | width: volume_icon.width + volume_bar.width 8 | height: Math.max(volume_icon.height, volume_bar.height) 9 | 10 | property bool muted: false 11 | property alias volume: volume_bar.progress 12 | 13 | signal mutedSet(bool muted) 14 | signal volumeSet(real volume) 15 | 16 | HTImageButton { 17 | id: volume_icon 18 | normalImage: root.muted ? "qrc:/images/muted_normal.png" : "qrc:/images/volume_normal.png" 19 | hoverPressedImage: root.muted? "qrc:/images/muted_hover_pressed.png" : "qrc:/images/volume_hover_pressed.png" 20 | anchors.verticalCenter: parent.verticalCenter 21 | 22 | onClicked: root.mutedSet(!root.muted) 23 | } 24 | 25 | HTProgressBar { 26 | id: volume_bar 27 | visible: !root.muted 28 | width: 100 29 | anchors.left: volume_icon.right 30 | anchors.leftMargin: 5 31 | anchors.verticalCenter: parent.verticalCenter 32 | 33 | onSeek: root.volumeSet(progress) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/qmls/Header.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import "../qmls/widgets" 4 | 5 | Item { 6 | id: root 7 | width: 100 8 | height: 70 9 | 10 | property alias userNickname: nickname.text 11 | property alias userAvatarUrl: avatar.source 12 | property alias canGoBack: go_back_button.enabled 13 | property alias canGoForward: go_forward_button.enabled 14 | 15 | signal goBack() 16 | signal goForward() 17 | signal login() 18 | 19 | Rectangle { 20 | anchors.fill: parent 21 | color: "red" 22 | } 23 | 24 | Rectangle { 25 | anchors.fill: parent 26 | color: Qt.rgba(0, 0, 0, 0.1) 27 | } 28 | 29 | Text { 30 | id: title_text 31 | color: "white" 32 | text: "网易云音乐" 33 | font.pixelSize: 24 34 | 35 | anchors.left: parent.left 36 | anchors.leftMargin: 20 37 | anchors.verticalCenter: parent.verticalCenter 38 | } 39 | 40 | Row { 41 | anchors.left: title_text.right 42 | anchors.leftMargin: 80 43 | anchors.verticalCenter: parent.verticalCenter 44 | spacing: -1 45 | 46 | HTImageButton { 47 | id: go_back_button 48 | enabled: false 49 | width: 28 50 | height: 28 51 | normalImage: "qrc:/images/left_arrow_normal.png" 52 | hoverPressedImage: "qrc:/images/left_arrow_hover.png" 53 | inactiveImage: "qrc:/images/left_arrow_inactive.png" 54 | 55 | onClicked: root.goBack() 56 | } 57 | HTImageButton { 58 | id: go_forward_button 59 | enabled: false 60 | width: 28 61 | height: 28 62 | normalImage: "qrc:/images/right_arrow_normal.png" 63 | hoverPressedImage: "qrc:/images/right_arrow_hover.png" 64 | inactiveImage: "qrc:/images/right_arrow_inactive.png" 65 | 66 | onClicked: root.goForward() 67 | } 68 | } 69 | 70 | HTTextButton { 71 | text: "登录" 72 | visible: !root.userNickname 73 | anchors.right: parent.right 74 | anchors.rightMargin: 20 75 | anchors.verticalCenter: parent.verticalCenter 76 | 77 | onClicked: root.login() 78 | } 79 | 80 | Row { 81 | visible: root.userNickname 82 | height: avatar.height 83 | spacing: 5 84 | anchors.right: parent.right 85 | anchors.rightMargin: 20 86 | anchors.verticalCenter: parent.verticalCenter 87 | 88 | Image { 89 | id: avatar 90 | width: 32 91 | height: 32 92 | smooth: true 93 | } 94 | 95 | Text { 96 | id: nickname 97 | font.pixelSize: 16 98 | color: "white" 99 | 100 | anchors.verticalCenter: parent.verticalCenter 101 | } 102 | } 103 | } 104 | 105 | -------------------------------------------------------------------------------- /src/qmls/LoginDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Dialogs 1.2 3 | 4 | import "../qmls/widgets" 5 | 6 | Dialog { 7 | id: root 8 | width: 300 9 | height: 200 10 | title: "登录" 11 | standardButtons: StandardButton.Close 12 | 13 | signal login(string account, string password) 14 | 15 | onAccepted: { 16 | if (!(account_input.text && password_input.text)) return 17 | 18 | root.login(account_input.text, password_input.text) 19 | } 20 | 21 | Rectangle { 22 | color: "lightgrey" 23 | anchors.fill: parent 24 | 25 | Column { 26 | id: col 27 | width: childrenRect.width 28 | spacing: -1 29 | anchors.horizontalCenter: parent.horizontalCenter 30 | 31 | HTTextInput { 32 | id: account_input 33 | hint: "账号" 34 | focus: false 35 | 36 | KeyNavigation.tab: password_input 37 | } 38 | 39 | HTTextInput { 40 | id: password_input 41 | hint: "密码" 42 | focus: false 43 | echoMode: TextInput.Password 44 | } 45 | } 46 | 47 | HTTextButton { 48 | width: col.width 49 | text: "登录" 50 | anchors.top: col.bottom 51 | anchors.topMargin: 20 52 | anchors.horizontalCenter: parent.horizontalCenter 53 | 54 | onClicked: { 55 | if (!(account_input.text && password_input.text)) return 56 | 57 | root.login(account_input.text, password_input.text) 58 | } 59 | } 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/qmls/MainController.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtMultimedia 5.0 3 | 4 | import QtQuick.Controls 1.1 5 | 6 | Item { 7 | Action { shortcut: "Space"; onTriggered: togglePlay() } 8 | Action { shortcut: "Ctrl+PgDown"; onTriggered: playNext() } 9 | Action { shortcut: "Ctrl+PgUp"; onTriggered: playPrev() } 10 | Action { shortcut: "Right"; onTriggered: forward() } 11 | Action { shortcut: "Left"; onTriggered: rewind() } 12 | Action { shortcut: "Up"; onTriggered: volumeUp() } 13 | Action { shortcut: "Down"; onTriggered: volumeDown() } 14 | 15 | function playPrev() { 16 | } 17 | 18 | function playNext() { 19 | var song = _controller.getNextPlaylistItem(current_song.id) 20 | playSong(song, false) 21 | } 22 | 23 | function togglePlay() { 24 | if (player.source) { 25 | if (player.playbackState == Audio.PlayingState) { 26 | player.pause() 27 | } else { 28 | player.play() 29 | } 30 | } 31 | } 32 | 33 | function forward() { 34 | if (player.source) { 35 | var position = Math.min(player.position + 3000, player.duration) 36 | player.seek(position) 37 | } 38 | } 39 | 40 | function rewind() { 41 | if (player.source) { 42 | var position = Math.max(player.position - 3000, 0) 43 | player.seek(position) 44 | } 45 | } 46 | 47 | function volumeUp() { 48 | player.volume += 0.1 49 | } 50 | 51 | function volumeDown() { 52 | player.volume -= 0.1 53 | } 54 | 55 | function togglePlaylist() { 56 | playlist_view.visible = !playlist_view.visible 57 | } 58 | 59 | function playSong(song, addToPlaylist) { 60 | win.title = song.name 61 | 62 | current_song.id = song.id 63 | current_song.mp3Url = song.mp3Url 64 | current_song.picUrl = song.picUrl 65 | current_song.artist = song.artist 66 | current_song.name = song.name 67 | current_song.album = song.album 68 | current_song.duration = song.duration 69 | current_song.lyric = "" 70 | 71 | _controller.getLyric(song.id) 72 | 73 | player.play() 74 | _settings.lastSong = song.id 75 | if (addToPlaylist) { 76 | _controller.addPlaylistItem(song.id, song.name, song.mp3Url, song.picUrl, 77 | song.name, song.album, song.duration) 78 | } 79 | } 80 | 81 | function playPlaylist(playlistId) { 82 | _controller.playlistDetailGot.connect(playlistDetailGot) 83 | _controller.playlistDetailGot.disconnect(playlist_detail_view.playlistDetailGot) 84 | _controller.getPlaylistDetail(playlistId) 85 | } 86 | 87 | function playlistDetailGot(detail) { 88 | var result = JSON.parse(detail) 89 | var tracks = result.tracks 90 | 91 | var track = tracks[0] 92 | var song = {} 93 | song.id = track.id 94 | song.name = track.name 95 | song.album = track.album.name 96 | song.artist = track.artists[0].name 97 | song.mp3Url = track.mp3Url 98 | song.picUrl = track.album.picUrl 99 | song.duration = track.duration 100 | playSong(song, false) 101 | 102 | for (var i = 0; i < tracks.length; i ++) { 103 | var track = tracks[i] 104 | _controller.addPlaylistItem(track.id, track.name, track.mp3Url, track.album.picUrl, 105 | track.artists[0].name, track.album.name, track.duration) 106 | } 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /src/qmls/PlayView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtGraphicalEffects 1.0 3 | 4 | import "../qmls/widgets" 5 | 6 | Item { 7 | id: root 8 | width: 100 9 | height: 80 10 | state: "mini" 11 | visible: !!song 12 | 13 | property url picUrl 14 | property string artist 15 | property string album 16 | property string song 17 | property string lyric 18 | property int position 19 | property alias playing: disc.playing 20 | 21 | onLyricChanged: lyric_view.setLyric(lyric) 22 | onPositionChanged: lyric_view.setPosition(position) 23 | 24 | Behavior on width { 25 | SmoothedAnimation { duration: 300 } 26 | } 27 | 28 | Behavior on height { 29 | SmoothedAnimation { duration: 300 } 30 | } 31 | 32 | states: [ 33 | State { 34 | name: "mini" 35 | PropertyChanges { 36 | target: mini_mode 37 | visible: true 38 | } 39 | PropertyChanges { 40 | target: fullscreen_mode 41 | visible: false 42 | } 43 | }, 44 | State { 45 | name: "fullscreen" 46 | PropertyChanges { 47 | target: mini_mode 48 | visible: false 49 | } 50 | PropertyChanges { 51 | target: fullscreen_mode 52 | visible: true 53 | } 54 | } 55 | ] 56 | 57 | HSep { width: parent.width } 58 | 59 | Item { 60 | id: mini_mode 61 | anchors.fill: parent 62 | 63 | Row { 64 | height: parent.height 65 | spacing: 16 66 | 67 | Item { width: 1; height: parent.height } 68 | 69 | Item { 70 | id: mini_cover 71 | width: 60 72 | height: 60 73 | 74 | anchors.verticalCenter: parent.verticalCenter 75 | 76 | Image { 77 | source: root.picUrl 78 | anchors.fill: parent 79 | 80 | Rectangle { 81 | id: image_cover 82 | color: fullscreen_mouse_area.containsMouse ? Qt.rgba(0, 0, 0, 0.3) : "transparent" 83 | border.width: 1 84 | border.color: Qt.rgba(0, 0, 0, 0.3) 85 | anchors.fill: parent 86 | } 87 | 88 | MouseArea { 89 | id: fullscreen_mouse_area 90 | hoverEnabled: true 91 | anchors.fill: parent 92 | 93 | onClicked: root.state = "fullscreen" 94 | } 95 | } 96 | } 97 | 98 | Column { 99 | spacing: 10 100 | width: mini_mode.width - mini_cover.width - parent.spacing * 2 101 | anchors.verticalCenter: parent.verticalCenter 102 | 103 | Text { 104 | width: parent.width 105 | text: root.song 106 | elide: Text.ElideRight 107 | } 108 | Text { 109 | width: parent.width 110 | text: root.artist 111 | elide: Text.ElideRight 112 | } 113 | } 114 | } 115 | } 116 | 117 | Item { 118 | id: fullscreen_mode 119 | anchors.fill: parent 120 | 121 | Image { 122 | id: background_image 123 | opacity: 0.6 124 | source: root.picUrl 125 | anchors.fill: parent 126 | } 127 | 128 | Rectangle { 129 | anchors.fill: background_image 130 | color: "white" 131 | } 132 | 133 | FastBlur { 134 | opacity: 0.5 135 | anchors.fill: background_image 136 | source: background_image 137 | radius: 64 138 | } 139 | 140 | Rectangle { 141 | anchors.fill: background_image 142 | color: Qt.rgba(0.5, 0.5, 0.5, 0.4) 143 | } 144 | 145 | Row { 146 | id: detail_row 147 | width: parent.width 148 | height: parent.height 149 | 150 | RunningDisc { id: disc; width: parent.width / 2; picUrl: root.picUrl } 151 | 152 | Column { 153 | id: detail_column 154 | spacing: 5 155 | width: parent.width / 2 156 | height: parent.height 157 | 158 | Column { 159 | spacing: 5 160 | width: parent.width 161 | height: childrenRect.height 162 | 163 | Item { 164 | width: parent.width 165 | height: 60 166 | 167 | Text { 168 | text: root.song 169 | font.pixelSize: 20 170 | 171 | anchors.verticalCenter: parent.verticalCenter 172 | } 173 | } 174 | 175 | Row { 176 | spacing: 20 177 | 178 | Text { 179 | text: "专辑: " + root.album 180 | } 181 | Text { 182 | text: "歌手: " + root.artist 183 | } 184 | } 185 | } 186 | 187 | HSep { width: detail_column.width * 3 / 4 } 188 | 189 | Item { width: detail_column.width; height: 20 } 190 | 191 | HTLyricView { 192 | id: lyric_view 193 | width: detail_column.width 194 | height: 300 195 | } 196 | } 197 | } 198 | 199 | HTImageButton { 200 | normalImage: "qrc:/images/cancel_fullscreen.png" 201 | hoverPressedImage: "qrc:/images/cancel_fullscreen.png" 202 | onClicked: root.state = "mini" 203 | 204 | anchors.top: parent.top 205 | anchors.right: parent.right 206 | anchors.topMargin: 10 207 | anchors.rightMargin: 10 208 | } 209 | } 210 | } 211 | 212 | -------------------------------------------------------------------------------- /src/qmls/PlaylistDetialView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import "../qmls/utils.js" as Utils 4 | import "../qmls/widgets" 5 | 6 | Item { 7 | id: root 8 | width: 500 9 | height: 500 10 | 11 | property string id 12 | property url coverImgUrl 13 | property string name 14 | property string creator 15 | property string createTime 16 | property string description 17 | 18 | signal songClicked(var song) 19 | signal playAllClicked(string playlistId) 20 | 21 | function setData(data) { songs_list_view.setData(data) } 22 | 23 | Column { 24 | id: col 25 | anchors.fill: parent 26 | anchors.topMargin: 20 27 | anchors.leftMargin: 20 28 | anchors.rightMargin: 20 29 | 30 | Row { 31 | id: info_area 32 | width: col.width 33 | height: 250 34 | 35 | Image { 36 | id: cover_img 37 | width: 180 38 | height: 180 39 | source: root.coverImgUrl 40 | } 41 | 42 | Item { width: 20; height: parent.height } 43 | 44 | Column { 45 | width: info_area.width - 200 46 | 47 | Text { 48 | text: root.name 49 | font.pixelSize: 20 50 | } 51 | 52 | Item { width: parent.width; height: 10 } 53 | 54 | Row { 55 | width: parent.width 56 | spacing: 40 57 | 58 | Text { 59 | text: root.creator 60 | } 61 | 62 | Text { 63 | text: Utils.unixTimeToReadable(root.createTime) 64 | } 65 | } 66 | 67 | Item { width: parent.width; height: 10 } 68 | 69 | HTTextButton { text: "播放全部"; onClicked: root.playAllClicked(root.id) } 70 | 71 | Item { width: parent.width; height: 10 } 72 | 73 | Text { 74 | width: parent.width 75 | text: "简介: " + root.description 76 | wrapMode: Text.Wrap 77 | } 78 | } 79 | } 80 | 81 | HSep { width: col.width } 82 | 83 | 84 | SongsListView { 85 | id: songs_list_view 86 | width: col.width 87 | height: col.height - info_area.height 88 | 89 | onSongClicked: root.songClicked(song) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/qmls/PlaylistIconView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtGraphicalEffects 1.0 3 | 4 | GridView { 5 | id: grid_view 6 | cellWidth: 190 7 | cellHeight: 190 8 | cacheBuffer: 8 9 | clip: true 10 | 11 | model: ListModel {} 12 | delegate: Item { 13 | id: delegate_root 14 | clip: true 15 | width: grid_view.cellWidth 16 | height: grid_view.cellHeight 17 | 18 | Image { 19 | clip: true 20 | asynchronous: true 21 | source: coverImgUrl 22 | fillMode: Image.PreserveAspectFit 23 | anchors.fill: parent 24 | anchors.margins: 20 25 | 26 | LinearGradient { 27 | width: parent.width 28 | height: 20 29 | visible: play_count_text.text 30 | start: Qt.point(0, 0) 31 | end: Qt.point(width, 0) 32 | anchors.top: parent.top 33 | 34 | gradient: Gradient { 35 | GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0) } 36 | GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.5) } 37 | } 38 | 39 | Row { 40 | height: parent.height 41 | spacing: 5 42 | anchors.right: parent.right 43 | anchors.rightMargin: 5 44 | 45 | Image { 46 | source: "qrc:/images/headphone.png" 47 | anchors.verticalCenter: parent.verticalCenter 48 | } 49 | Text { 50 | id: play_count_text 51 | color: "white" 52 | text: playCount || "" 53 | anchors.verticalCenter: parent.verticalCenter 54 | } 55 | } 56 | } 57 | LinearGradient { 58 | width: parent.width 59 | height: 20 60 | visible: creator_text.text 61 | start: Qt.point(0, 0) 62 | end: Qt.point(width, 0) 63 | anchors.bottom: parent.bottom 64 | 65 | gradient: Gradient { 66 | GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.5) } 67 | GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0) } 68 | } 69 | 70 | Row { 71 | height: parent.height 72 | spacing: 5 73 | anchors.left: parent.left 74 | anchors.leftMargin: 5 75 | 76 | Image { 77 | source: "qrc:/images/user_icon.png" 78 | anchors.verticalCenter: parent.verticalCenter 79 | } 80 | Text { 81 | id: creator_text 82 | color: "white" 83 | text: (creator && creator["nickname"]) || "" 84 | anchors.verticalCenter: parent.verticalCenter 85 | } 86 | } 87 | } 88 | 89 | MouseArea { 90 | anchors.fill: parent 91 | onClicked: grid_view.playlistClicked(id) 92 | } 93 | } 94 | } 95 | 96 | signal playlistClicked(string playlistId) 97 | 98 | function setData(data) { 99 | model.clear() 100 | 101 | var playlists = JSON.parse(data) 102 | playlists.forEach(function(playlist){ 103 | if (!playlist["coverImgUrl"]) playlist["coverImgUrl"] = playlist["picUrl"] 104 | if (!playlist["playCount"]) playlist["playCount"] = "" 105 | if (!playlist["creator"]) playlist["creator"] = {} 106 | model.append(playlist) 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/qmls/PlaylistView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtGraphicalEffects 1.0 3 | 4 | import "../qmls/widgets/" 5 | import "../qmls/utils.js" as Utils 6 | 7 | Item { 8 | id: root 9 | width: 560 10 | height: 480 11 | 12 | property alias model: list_view.model 13 | property string currentSong: "" 14 | 15 | signal songClicked(var song) 16 | 17 | Rectangle { 18 | radius: 4 19 | color: Qt.rgba(1, 1, 1, 0.8) 20 | border.width: 1 21 | border.color: Qt.rgba(0, 0, 0, 0.3) 22 | anchors.fill: parent 23 | anchors.leftMargin: 10 24 | anchors.topMargin: 10 25 | 26 | Rectangle { 27 | anchors.fill: list_view 28 | anchors.margins: 1 29 | HSep { width: parent.width } 30 | } 31 | 32 | ListView { 33 | id: list_view 34 | width: parent.width - 2 35 | height: parent.height - 40 36 | clip: true 37 | anchors.bottom: parent.bottom 38 | anchors.horizontalCenter: parent.horizontalCenter 39 | 40 | delegate: Rectangle { 41 | width: list_view.width 42 | height: 36 43 | color: index % 2 ? Qt.rgba(1, 1, 1, 1) : Qt.rgba(0.5, 0.5, 0.5, 0.1) 44 | 45 | Rectangle { 46 | width: 5 47 | height: parent.height - 10 48 | color: "red" 49 | visible: root.currentSong == id 50 | anchors.verticalCenter: parent.verticalCenter 51 | } 52 | 53 | Text { 54 | text: name 55 | anchors.left: parent.left 56 | anchors.leftMargin: 20 57 | anchors.verticalCenter: parent.verticalCenter 58 | } 59 | 60 | Text { 61 | text: artist 62 | anchors.left: parent.left 63 | anchors.leftMargin: 300 64 | anchors.verticalCenter: parent.verticalCenter 65 | } 66 | 67 | Text { 68 | text: Utils.formatTime(duration) 69 | anchors.right: parent.right 70 | anchors.rightMargin: 10 71 | anchors.verticalCenter: parent.verticalCenter 72 | } 73 | 74 | MouseArea { 75 | anchors.fill: parent 76 | onClicked: root.songClicked(list_view.model.getSongAtIndex(index)) 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/qmls/SideBar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Rectangle { 4 | id: root 5 | width: 200 6 | height: 430 7 | color: Qt.rgba(1, 1, 1, 0.3) 8 | clip: true 9 | 10 | signal playlistClicked(string playlistId) 11 | 12 | function setCreatedPlaylists(playlists) { 13 | created_playlists.setData(playlists) 14 | } 15 | 16 | function setMarkedPlaylists(playlists) { 17 | marked_playlists.setData(playlists) 18 | } 19 | 20 | Flickable { 21 | width: user_playlists_column.width 22 | height: user_playlists_column.height 23 | contentWidth: user_playlists_column.childrenRect.width 24 | contentHeight: user_playlists_column.childrenRect.height 25 | clip: true 26 | 27 | Column { 28 | id: user_playlists_column 29 | // clip: true 30 | width: root.width - 20 31 | height: root.height - 100 32 | anchors.top: parent.top 33 | anchors.topMargin: 10 34 | anchors.left: parent.left 35 | anchors.leftMargin: 10 36 | 37 | Text { 38 | width: parent.width 39 | height: implicitHeight 40 | text: "创建的歌单" 41 | color: "grey" 42 | } 43 | 44 | SideBarPlaylists { 45 | id: created_playlists 46 | width: parent.width - 10 47 | anchors.left: parent.left 48 | anchors.leftMargin: 10 49 | 50 | onPlaylistClicked: root.playlistClicked(playlistId) 51 | } 52 | 53 | Text { 54 | width: parent.width 55 | height: implicitHeight 56 | text: "收藏的歌单" 57 | color: "grey" 58 | } 59 | 60 | SideBarPlaylists { 61 | id: marked_playlists 62 | width: parent.width - 10 63 | anchors.left: parent.left 64 | anchors.leftMargin: 10 65 | 66 | onPlaylistClicked: root.playlistClicked(playlistId) 67 | } 68 | } 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/qmls/SideBarPlaylists.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | ListView { 4 | id: list_view 5 | width: 300 6 | height: childrenRect.height 7 | 8 | signal playlistClicked(string playlistId) 9 | 10 | model: ListModel{} 11 | delegate: Item { 12 | width: list_view.width 13 | height: txt.implicitHeight + 10 14 | Text { 15 | id: txt 16 | width: parent.width 17 | text: name 18 | elide: Text.ElideRight 19 | color: Qt.rgba(0, 0, 0, 0.9) 20 | anchors.verticalCenter: parent.verticalCenter 21 | } 22 | 23 | MouseArea { 24 | anchors.fill: parent 25 | onClicked: list_view.playlistClicked(id) 26 | } 27 | } 28 | 29 | function setData(playlists) { 30 | list_view.model.clear() 31 | 32 | for (var i = 0; i < playlists.length; i++) { 33 | list_view.model.append(playlists[i]) 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/qmls/SongsListView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import "../qmls/widgets/" 4 | import "qrc:/src/qmls/utils.js" as Utils 5 | 6 | ListView { 7 | id: list_view 8 | width: 100 9 | height: 100 10 | clip: true 11 | currentIndex: -1 12 | 13 | Component { 14 | id: row_component 15 | 16 | Row { 17 | id: row 18 | property string title 19 | property string artist 20 | property string album 21 | property string duration 22 | 23 | property bool header: false 24 | property color columnNameColor: header ? Qt.rgba(0.5, 0.5, 0.5, 0.7) : "black" 25 | 26 | height: parent.height 27 | spacing: 10 28 | Item { width: 1; height: parent.height } 29 | Text { 30 | width: 280 31 | text: row.title 32 | elide: Text.ElideRight 33 | color: columnNameColor 34 | anchors.verticalCenter: parent.verticalCenter 35 | } 36 | VSep { height: parent.height; visible: header } 37 | Text { 38 | width: 200 39 | text: row.artist 40 | elide: Text.ElideRight 41 | color: columnNameColor 42 | anchors.verticalCenter: parent.verticalCenter 43 | } 44 | VSep { height: parent.height; visible: header } 45 | Text { 46 | width: 150 47 | text: row.album 48 | elide: Text.ElideRight 49 | color: columnNameColor 50 | anchors.verticalCenter: parent.verticalCenter 51 | } 52 | VSep { height: parent.height; visible: header } 53 | Text { 54 | text: row.duration 55 | elide: Text.ElideRight 56 | color: columnNameColor 57 | anchors.verticalCenter: parent.verticalCenter 58 | } 59 | } 60 | } 61 | 62 | model: ListModel {} 63 | delegate: Rectangle { 64 | width: list_view.width 65 | height: 36 66 | color: list_view.currentIndex == index ? Qt.rgba(0, 0, 0, 0.2) : "transparent" 67 | 68 | Loader { 69 | sourceComponent: row_component 70 | onLoaded: { 71 | item.height = parent.height 72 | 73 | item.title = name 74 | item.artist = artists.get(0).name 75 | item.album = album["name"] 76 | item.duration = Utils.formatTime(duration) 77 | } 78 | } 79 | 80 | MouseArea { 81 | anchors.fill: parent 82 | onClicked: list_view.currentIndex = index 83 | onDoubleClicked: { 84 | var song = {} 85 | song.id = id 86 | song.name = name 87 | song.album = album["name"] 88 | song.artist = artists.get(0).name 89 | song.mp3Url = mp3Url 90 | song.picUrl = album.picUrl 91 | song.duration = duration 92 | list_view.songClicked(song) 93 | } 94 | } 95 | } 96 | 97 | header: Item { 98 | width: list_view.width 99 | height: 24 100 | 101 | Loader { 102 | sourceComponent: row_component 103 | onLoaded: { 104 | item.height = parent.height 105 | item.header = true 106 | 107 | item.title = "音乐标题" 108 | item.artist = "歌手" 109 | item.album = "专辑" 110 | item.duration = "时长" 111 | } 112 | } 113 | 114 | HSep { width: parent.width; anchors.bottom: parent.bottom } 115 | } 116 | 117 | signal songClicked(var song) 118 | 119 | function setData(tracks) { 120 | model.clear() 121 | 122 | for (var i = 0; i < tracks.length; i++) { 123 | model.append(tracks[i]) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/qmls/ViewHistoryManager.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | QtObject { 4 | property int _currentIndex: 0 5 | property var _historyList: [] 6 | 7 | property bool canGoBack: _historyList.length != 0 && _currentIndex > 0 8 | property bool canGoForward: _historyList.length != 0 && _currentIndex < _historyList.length - 1 9 | 10 | function append(state) { 11 | _historyList.push(state) 12 | _currentIndex = _historyList.length - 1 13 | } 14 | 15 | function goBack() { 16 | if (_historyList.length != 0 && _currentIndex != 0) { 17 | _currentIndex -= 1 18 | return _historyList[_currentIndex] 19 | } else { 20 | return null 21 | } 22 | } 23 | 24 | function goForward() { 25 | if (_historyList.length != 0 && _currentIndex != _historyList.length - 1) { 26 | _currentIndex += 1 27 | return _historyList[_currentIndex] 28 | } else { 29 | return null 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/qmls/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import QtQuick.Window 2.1 3 | import QtQuick.Controls 1.0 4 | import QtMultimedia 5.0 5 | 6 | import Org.Hualet.Widgets 1.0 7 | 8 | import "../qmls/widgets" 9 | import "qrc:/src/qmls/utils.js" as Utils 10 | 11 | Window { 12 | id: win 13 | visible: true 14 | width: 1000 15 | height: 640 16 | color: "transparent" 17 | title: " " 18 | 19 | Item { 20 | id: window_content 21 | focus: true 22 | anchors.fill: parent 23 | 24 | state: "suggestions" 25 | 26 | property bool stateChangedByUser: true 27 | 28 | states: [ 29 | State { 30 | name: "suggestions" 31 | PropertyChanges { target: main_tab_view; currentIndex: 0 } 32 | PropertyChanges { target: playlist_detail_view; visible: false } 33 | }, 34 | State { 35 | name: "ranklists" 36 | PropertyChanges { target: main_tab_view; currentIndex: 1 } 37 | PropertyChanges { target: playlist_detail_view; visible: false } 38 | }, 39 | State { 40 | name: "playlists" 41 | PropertyChanges { target: main_tab_view; currentIndex: 2 } 42 | PropertyChanges { target: playlist_detail_view; visible: false } 43 | }, 44 | State { 45 | name: "playlist_detail" 46 | PropertyChanges { target: playlist_detail_view; visible: true } 47 | } 48 | ] 49 | 50 | onStateChanged: { 51 | if (stateChangedByUser) { 52 | views_history_manager.append(state) 53 | } else { 54 | stateChangedByUser = true 55 | } 56 | } 57 | 58 | Keys.onLeftPressed: goBack() 59 | Keys.onRightPressed: goForward() 60 | 61 | function goBack() { 62 | var _state = views_history_manager.goBack() 63 | if (_state) { 64 | stateChangedByUser = false 65 | state = _state 66 | } 67 | } 68 | 69 | function goForward() { 70 | var _state = views_history_manager.goForward() 71 | if (_state) { 72 | stateChangedByUser = false 73 | state = _state 74 | } 75 | } 76 | 77 | Audio { 78 | id: player 79 | autoPlay: false 80 | source: current_song.mp3Url 81 | 82 | onVolumeChanged: _settings.volume = volume 83 | 84 | onStatusChanged: { 85 | if (status == Audio.EndOfMedia) { 86 | main_controller.playNext() 87 | } 88 | } 89 | 90 | Component.onCompleted: volume = _settings.volume || 0.5 91 | } 92 | 93 | Song { 94 | id: current_song 95 | 96 | Component.onCompleted: { 97 | var id = _settings.lastSong 98 | if (id) { 99 | var song = _controller.getPlaylistItemById(id) 100 | if (song) { 101 | current_song.id = song.id 102 | current_song.mp3Url = song.mp3Url 103 | current_song.picUrl = song.picUrl 104 | current_song.artist = song.artist 105 | current_song.name = song.name 106 | current_song.album = song.album 107 | current_song.duration = song.duration 108 | current_song.lyric = "" 109 | 110 | _controller.getLyric(song.id) 111 | } 112 | } 113 | } 114 | } 115 | 116 | MainController { id: main_controller } 117 | 118 | ViewHistoryManager { id: views_history_manager } 119 | 120 | Connections { 121 | target: _controller 122 | 123 | onLoginSucceed: { 124 | login_dialog.close() 125 | var loginInfo = JSON.parse(info) 126 | 127 | _settings.userId = loginInfo["profile"]["userId"] 128 | _settings.userNickname = loginInfo["profile"]["nickname"] 129 | _settings.userAvatarUrl = loginInfo["profile"]["avatarUrl"] 130 | _controller.getUserPlaylists(_settings.userId) 131 | } 132 | 133 | onLoginFailed: {} 134 | 135 | onUserPlaylistsGot: { 136 | var playlists = JSON.parse(userPlaylists) 137 | var userCreated = [] 138 | var userMarked = [] 139 | 140 | for (var i = 0; i < playlists.length; i++) { 141 | if (playlists[i].creator.userId == _settings.userId) { 142 | userCreated.push(playlists[i]) 143 | } else { 144 | userMarked.push(playlists[i]) 145 | } 146 | } 147 | 148 | side_bar.setCreatedPlaylists(userCreated) 149 | side_bar.setMarkedPlaylists(userMarked) 150 | } 151 | 152 | onLyricGot: { 153 | current_song.lyric = lyric 154 | } 155 | } 156 | 157 | Rectangle { 158 | id: background 159 | color: Qt.rgba(1, 1, 1, 0.8) 160 | anchors.fill: parent 161 | } 162 | 163 | Column { 164 | width: parent.width 165 | height: parent.height 166 | 167 | Header { 168 | id: header 169 | width: parent.width 170 | canGoBack: views_history_manager.canGoBack 171 | canGoForward: views_history_manager.canGoForward 172 | userNickname: _settings.userNickname 173 | userAvatarUrl: _settings.userAvatarUrl 174 | 175 | onGoBack: window_content.goBack() 176 | onGoForward: window_content.goForward() 177 | onLogin: login_dialog.open() 178 | 179 | Component.onCompleted: print(_settings.userNickname) 180 | } 181 | 182 | Row { 183 | clip: true 184 | width: parent.width 185 | height: parent.height - header.height - footer.height 186 | 187 | SideBar { 188 | id: side_bar 189 | width: 200 190 | height: parent.height 191 | 192 | onPlaylistClicked: playlist_detail_view.setPlaylist(playlistId) 193 | 194 | Component.onCompleted: { 195 | if (_settings.userId) { 196 | _controller.getUserPlaylists(_settings.userId) 197 | } 198 | } 199 | } 200 | 201 | VSep { height: parent.height } 202 | 203 | Rectangle { 204 | width: parent.width - side_bar.width - 1 205 | height: parent.height 206 | 207 | HTTabView { 208 | id: main_tab_view 209 | anchors.fill: parent 210 | visible: !playlist_detail_view.visible 211 | 212 | property var _tabs: ["suggestions", "ranklists", "playlists"] 213 | 214 | onCurrentIndexChanged: window_content.state = _tabs[currentIndex] 215 | 216 | Tab { 217 | title: "推荐" 218 | 219 | HTTabContent { 220 | width: parent.width 221 | height: parent.height 222 | contentWidth: width 223 | contentHeight: banners_view.height + title_hotspot.height + hotspot_icon_view.height 224 | 225 | HTBannersView { 226 | id: banners_view 227 | width: parent.width - 20 228 | anchors.horizontalCenter: parent.horizontalCenter 229 | 230 | Connections { 231 | target: _controller 232 | onBannersGot: banners_view.setData(banners) 233 | } 234 | 235 | Component.onCompleted: _controller.getBanners() 236 | } 237 | 238 | HTSectionTitle { 239 | id: title_hotspot 240 | width: parent.width - 20 241 | title: "热门精选" 242 | 243 | anchors.top: banners_view.bottom 244 | anchors.topMargin: 40 245 | anchors.horizontalCenter: parent.horizontalCenter 246 | } 247 | 248 | PlaylistIconView { 249 | id: hotspot_icon_view 250 | width: cellWidth * 4 251 | height: childrenRect.height 252 | 253 | anchors.top: title_hotspot.bottom 254 | anchors.topMargin: 10 255 | anchors.horizontalCenter: parent.horizontalCenter 256 | 257 | Connections { 258 | target: _controller 259 | onHotspotGot: hotspot_icon_view.setData(hotspot) 260 | } 261 | 262 | Component.onCompleted: _controller.getHotspot() 263 | 264 | onPlaylistClicked: { 265 | playlist_detail_view.setPlaylist(playlistId) 266 | } 267 | } 268 | } 269 | } 270 | Tab { 271 | title: "排行榜" 272 | 273 | HTTabContent { 274 | width: parent.width 275 | height: parent.height 276 | contentWidth: toplist_icon_view.width 277 | contentHeight: toplist_icon_view.height 278 | 279 | PlaylistIconView { 280 | id: toplist_icon_view 281 | width: cellWidth * 4 282 | height: childrenRect.height 283 | 284 | anchors.horizontalCenter: parent.horizontalCenter 285 | 286 | Connections { 287 | target: _controller 288 | onRankingListsGot: toplist_icon_view.setData(lists) 289 | } 290 | 291 | Component.onCompleted: _controller.getRankingLists() 292 | 293 | onPlaylistClicked: { 294 | playlist_detail_view.setPlaylist(playlistId) 295 | } 296 | } 297 | } 298 | } 299 | Tab { 300 | title: "歌单" 301 | 302 | HTTabContent { 303 | width: parent.width 304 | height: parent.height 305 | contentWidth: playlists_icon_view.width 306 | contentHeight: playlists_icon_view.height 307 | 308 | PlaylistIconView { 309 | id: playlists_icon_view 310 | width: cellWidth * 4 311 | height: childrenRect.height 312 | 313 | anchors.horizontalCenter: parent.horizontalCenter 314 | 315 | Connections { 316 | target: _controller 317 | onTopPlaylistsGot: playlists_icon_view.setData(playlists) 318 | } 319 | 320 | Component.onCompleted: _controller.getTopPlaylists() 321 | 322 | onPlaylistClicked: { 323 | playlist_detail_view.setPlaylist(playlistId) 324 | } 325 | } 326 | } 327 | } 328 | // Tab { 329 | // title: "最新音乐" 330 | // } 331 | } 332 | 333 | PlaylistDetialView { 334 | id: playlist_detail_view 335 | visible: false 336 | anchors.fill: parent 337 | 338 | onSongClicked: main_controller.playSong(song, true) 339 | onPlayAllClicked: { 340 | main_controller.playPlaylist(playlistId) 341 | } 342 | 343 | function setPlaylist(playlistId) { 344 | _controller.getPlaylistDetail(playlistId) 345 | 346 | _controller.playlistDetailGot.disconnect(main_controller.playlistDetailGot) 347 | _controller.playlistDetailGot.connect(playlistDetailGot) 348 | } 349 | 350 | function playlistDetailGot(detail) { 351 | print(detail) 352 | var result = JSON.parse(detail) 353 | 354 | playlist_detail_view.id = result.id 355 | playlist_detail_view.name = result.name 356 | playlist_detail_view.coverImgUrl = result.coverImgUrl 357 | playlist_detail_view.creator = result.creator.nickname 358 | playlist_detail_view.createTime = result.createTime 359 | playlist_detail_view.description = result.description || "" 360 | playlist_detail_view.setData(result.tracks) 361 | 362 | playlist_detail_view.visible = true 363 | window_content.state = "playlist_detail" 364 | } 365 | } 366 | } 367 | } 368 | 369 | Footer { 370 | id: footer 371 | width: parent.width 372 | 373 | playing: player.playbackState == Audio.PlayingState 374 | progress: player.position / player.duration 375 | timeInfo: "%1/%2".arg(Utils.formatTime(player.position)).arg(Utils.formatTime(player.duration)) 376 | volume: player.volume 377 | muted: player.muted 378 | 379 | onMutedSet: player.muted = muted 380 | onPlay: player.play() 381 | onPause: player.pause() 382 | onVolumeSet: player.volume = volume 383 | onSeek: player.seek(player.duration * progress) 384 | onPlayPrev: main_controller.playPrev() 385 | onPlayNext: main_controller.playNext() 386 | onTogglePlaylist: main_controller.togglePlaylist() 387 | } 388 | } 389 | 390 | Item { 391 | id: floats 392 | y: header.height 393 | width: parent.width 394 | height: parent.height - header.height - footer.height 395 | 396 | LoginDialog { 397 | id: login_dialog 398 | onLogin: _controller.login(account, password) 399 | } 400 | 401 | PlayView { 402 | width: state == "mini" ? side_bar.width : parent.width 403 | height: state == "mini" ? 80 : parent.height 404 | picUrl: current_song.picUrl 405 | artist: current_song.artist 406 | song: current_song.name 407 | album: current_song.album 408 | lyric: current_song.lyric 409 | position: player.position 410 | playing: player.playbackState == Audio.PlayingState 411 | 412 | anchors.left: parent.left 413 | anchors.bottom: parent.bottom 414 | } 415 | 416 | PlaylistView { 417 | id: playlist_view 418 | visible: false 419 | model: _controller.playlistModel 420 | currentSong: current_song.id 421 | anchors.right: parent.right 422 | anchors.bottom: parent.bottom 423 | 424 | onSongClicked: main_controller.playSong(song, false) 425 | } 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/qmls/utils.js: -------------------------------------------------------------------------------- 1 | function formatTime(millseconds) { 2 | if (millseconds <= 0) return "00:00"; 3 | var secs = Math.ceil(millseconds / 1000) 4 | var hr = Math.floor(secs / 3600); 5 | var min = Math.floor((secs - (hr * 3600))/60); 6 | var sec = secs - (hr * 3600) - (min * 60); 7 | 8 | if (min < 10) {min = "0" + min;} 9 | if (sec < 10) {sec = "0" + sec;} 10 | if (!hr) {hr = "00";} 11 | return min + ':' + sec; 12 | } 13 | 14 | function unixTimeToReadable(time) { 15 | return new Date(parseInt(time)).toLocaleDateString() 16 | } 17 | 18 | function parseLyric(lyric) { 19 | var result = {} 20 | var lines = lyric.split("\n") 21 | var pattern = /^\[(\d*):(\d*).(\d.*)\](.*)/i 22 | for (var i=0; i=0 ) { 24 | var match = lines[i].match(pattern); 25 | // minutes: match[1], seconds: match[2], milliseconds: match[3], lyric: match[4] 26 | var milliseconds = parseInt(match[1]) * 60 * 1000 + parseInt(match[2]) * 1000 + parseInt(match[3]) 27 | result[milliseconds] = match[4] 28 | } 29 | } 30 | 31 | return result 32 | } 33 | -------------------------------------------------------------------------------- /src/qmls/widgets/BackForwardButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Rectangle { 4 | width: 100 5 | height: 62 6 | } 7 | -------------------------------------------------------------------------------- /src/qmls/widgets/HSep.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Rectangle { 4 | width: 100 5 | height: 1 6 | color: Qt.rgba(0, 0, 0, 0.3) 7 | } 8 | -------------------------------------------------------------------------------- /src/qmls/widgets/HTBannersView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | PathView { 4 | id: path_view 5 | width: itemWidth * 2 6 | height: itemHeight 7 | preferredHighlightBegin: 0.5 8 | preferredHighlightEnd: 0.5 9 | pathItemCount: 3 10 | 11 | property int itemWidth: 938 / 2 12 | property int itemHeight: 365 / 2 13 | 14 | delegate: Image { 15 | id: img 16 | z: parent.z + PathView.itemX == 0 ? 10 : 0 17 | width: path_view.itemWidth 18 | height: path_view.itemHeight 19 | scale: PathView.itemScale 20 | source: pic 21 | transform: Rotation { origin.x: rotationOriginX; axis { x: 0; y: 1; z: 0 } angle: rotationAngle } 22 | 23 | property real rotationOriginX: PathView.itemX < 0 ? 0 : path_view.itemWidth 24 | property real rotationAngle: PathView.itemX < 0 ? 10 : PathView.itemX > 0 ? -10 : 0 25 | 26 | MouseArea { 27 | anchors.fill: parent 28 | onClicked: path_view.currentIndex == index ? Qt.openUrlExternally(url) : path_view.currentIndex = index 29 | } 30 | } 31 | model: ListModel {} 32 | path: Path { 33 | startX: 0 34 | startY: path_view.height / 2 35 | PathAttribute { name: "itemScale"; value: 0.8 } 36 | PathAttribute { name: "itemX"; value: -100 } 37 | PathLine { x: path_view.width / 2; y: path_view.height / 2 } 38 | PathAttribute { name: "itemScale"; value: 1 } 39 | PathAttribute { name: "itemX"; value: 0 } 40 | PathLine { x: path_view.width; y: path_view.height / 2 } 41 | PathAttribute { name: "itemScale"; value: 0.8 } 42 | PathAttribute { name: "itemX"; value: 100 } 43 | } 44 | 45 | function setData(data) { 46 | model.clear() 47 | 48 | var banners = JSON.parse(data) 49 | banners.forEach(function(banner){ 50 | model.append(banner) 51 | }) 52 | } 53 | 54 | Timer { 55 | running: true 56 | repeat: true 57 | interval: 2000 58 | onTriggered: { 59 | path_view.currentIndex = (path_view.currentIndex + 1) % 8 60 | } 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/qmls/widgets/HTLyricView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import "../utils.js" as Utils 4 | 5 | Item { 6 | width: 300 7 | height: 300 8 | clip: true 9 | 10 | function setLyric(lyric) { 11 | list_view.model.clear() 12 | 13 | var lyricDict = Utils.parseLyric(lyric) 14 | for (var timeStamp in lyricDict) { 15 | list_view.model.append({ 16 | "lyricTimestamp": timeStamp, 17 | "lyricContent": lyricDict[timeStamp] 18 | }) 19 | } 20 | } 21 | 22 | function setPosition(position) { 23 | var lastMatch = 0 24 | for (var i = 0; i < list_view.count; i++) { 25 | if (list_view.model.get(i).lyricTimestamp <= position) { 26 | lastMatch = i 27 | } else { 28 | break 29 | } 30 | } 31 | list_view.currentIndex = lastMatch 32 | } 33 | 34 | ListView { 35 | id: list_view 36 | highlightRangeMode: ListView.ApplyRange 37 | preferredHighlightBegin: list_view.height / 2 - lineHeight / 2 38 | preferredHighlightEnd: list_view.height / 2 + lineHeight / 2 39 | anchors.fill: parent 40 | 41 | property int lineHeight: 26 42 | 43 | delegate: Item { 44 | width: list_view.width 45 | height: list_view.lineHeight 46 | 47 | Text { 48 | id: txt 49 | text: lyricContent 50 | color: index == list_view.currentIndex ? "white" : "black" 51 | } 52 | } 53 | model: ListModel {} 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/qmls/widgets/HTProgressBar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Item { 4 | id: root 5 | width: 100 6 | height: 62 7 | 8 | property real progress: 0 9 | 10 | signal seek(real progress) 11 | 12 | onProgressChanged: if(!drag_mouse_area.drag.active) handle.x = root.width * progress - handle.width / 2 13 | 14 | Rectangle { 15 | width: parent.width 16 | height: 6 17 | radius: height / 2 18 | color: Qt.rgba(0, 0, 0, 0.3) 19 | anchors.verticalCenter: parent.verticalCenter 20 | 21 | MouseArea { 22 | anchors.fill: parent 23 | onClicked: root.seek(Math.min(Math.max(handle.width / 2, mouse.x), parent.width - handle.width / 2) / parent.width) 24 | } 25 | 26 | Rectangle { 27 | width: parent.width * root.progress 28 | height: parent.height 29 | color: "red" 30 | radius: height / 2 31 | 32 | Rectangle { 33 | anchors.fill: parent 34 | radius: parent.radius 35 | color: Qt.rgba(1, 1, 1, 0.2) 36 | } 37 | } 38 | 39 | Rectangle { 40 | id: handle 41 | width: 12 42 | height: 12 43 | radius: 5 44 | color: "white" 45 | anchors.verticalCenter: parent.verticalCenter 46 | 47 | onXChanged: if(drag_mouse_area.drag.active) root.seek(x / (handle.parent.width - handle.width)) 48 | 49 | MouseArea { 50 | id: drag_mouse_area 51 | anchors.fill: parent 52 | drag.target: parent 53 | drag.axis: Drag.XAxis 54 | drag.minimumX: handle.width / 2 55 | drag.maximumX: handle.parent.width - handle.width / 2 56 | } 57 | 58 | Rectangle { 59 | width: 4 60 | height: 4 61 | radius: 2 62 | color: "red" 63 | anchors.centerIn: parent 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/qmls/widgets/HTSectionTitle.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Column { 4 | width: 100 5 | height: childrenRect.height 6 | 7 | property alias title: txt.text 8 | 9 | Text { 10 | id: txt 11 | color: "black" 12 | font.pixelSize: 18 13 | } 14 | 15 | Item { width: parent.width; height: 5 } 16 | 17 | Rectangle { 18 | width: 100 19 | height: 6 20 | color: sep.color 21 | } 22 | 23 | HSep { id: sep; width: parent.width } 24 | } 25 | -------------------------------------------------------------------------------- /src/qmls/widgets/HTTextButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | MouseArea { 4 | id: root 5 | width: txt.implicitWidth + 14 6 | height: txt.implicitHeight + 8 7 | hoverEnabled: true 8 | 9 | property alias text: txt.text 10 | 11 | Rectangle { 12 | anchors.fill: parent 13 | color: Qt.rgba(1, 1, 1, 0.8) 14 | radius: 6 15 | border.color: Qt.rgba(0, 0, 0, 0.4) 16 | } 17 | 18 | Text { 19 | id: txt 20 | color: root.containsMouse ? Qt.rgba(0.2, 0.2, 0.5, 0.8) : Qt.rgba(0, 0, 0, 0.8) 21 | anchors.centerIn: parent 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/qmls/widgets/HTTextInput.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | FocusScope { 4 | id: root 5 | width: 200 6 | height: 28 7 | 8 | property alias text: text_input.text 9 | property alias hint: hint_text.text 10 | property alias echoMode: text_input.echoMode 11 | 12 | Rectangle { 13 | border.width: 1 14 | border.color: Qt.rgba(0, 0, 0, 0.6) 15 | anchors.fill: parent 16 | 17 | TextInput { 18 | id: text_input 19 | focus: true 20 | font.pixelSize: 16 21 | selectByMouse: true 22 | anchors.fill: parent 23 | anchors.margins: (parent.height - contentHeight) / 2 24 | } 25 | 26 | Text { 27 | id: hint_text 28 | visible: !(root.focus || text_input.text) 29 | color: Qt.lighter(text_input.color) 30 | font.pixelSize: text_input.font.pixelSize 31 | anchors.fill: text_input 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/qmls/widgets/RunningDisc.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtGraphicalEffects 1.0 3 | 4 | Item { 5 | id: root 6 | width: 335 7 | height: 400 8 | clip: true 9 | 10 | property bool playing: false 11 | property alias picUrl: cover_image.source 12 | 13 | Image { 14 | id: disc_image 15 | source: "qrc:/images/disc_disc.png" 16 | anchors.centerIn: image_area 17 | } 18 | 19 | Item { 20 | id: image_area 21 | width: disc_image.implicitWidth 22 | height: disc_image.implicitHeight 23 | visible: false 24 | anchors.bottom: parent.bottom 25 | anchors.horizontalCenter: parent.horizontalCenter 26 | 27 | Image { id: cover_image; width: 200; height: 200; anchors.centerIn: parent } 28 | } 29 | 30 | Item { 31 | id: mask 32 | anchors.fill: image_area 33 | 34 | Rectangle { 35 | width: cover_image.width 36 | height: cover_image.height 37 | radius: 100 38 | anchors.centerIn: parent 39 | } 40 | } 41 | 42 | OpacityMask { 43 | id: opacity_mask 44 | source: image_area 45 | maskSource: mask 46 | anchors.fill: image_area 47 | } 48 | 49 | Image { 50 | source: "qrc:/images/disc_mask.png" 51 | anchors.centerIn: image_area 52 | } 53 | 54 | Image { 55 | x: 250 56 | y: -5 57 | transformOrigin: Item.TopLeft 58 | // rotation: root.playing ? 0 : 30 59 | source: "qrc:/images/disc_needle.png" 60 | } 61 | 62 | PropertyAnimation { 63 | property: "rotation" 64 | target: opacity_mask 65 | from: 0 66 | to: 360 67 | duration: 30 * 1000 68 | running: root.playing 69 | loops: Animation.Infinite 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/qmls/widgets/VSep.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Rectangle { 4 | width: 1 5 | height: 100 6 | color: Qt.rgba(0, 0, 0, 0.3) 7 | } 8 | -------------------------------------------------------------------------------- /src/song.cpp: -------------------------------------------------------------------------------- 1 | #include "song.h" 2 | 3 | Song::Song(QObject *parent) : 4 | QObject(parent) 5 | { 6 | } 7 | 8 | Song::Song(QString& id, QString& name, QString& mp3Url, QString& picUrl, 9 | QString& artist, QString& album, int duration, QObject *parent) : 10 | QObject(parent), 11 | m_id(id), m_name(name), m_mp3Url(mp3Url), 12 | m_picUrl(picUrl), m_artist(artist), m_album(album), 13 | m_duration(duration) 14 | { 15 | 16 | } 17 | 18 | QString Song::mp3Url() const 19 | { 20 | return m_mp3Url; 21 | } 22 | 23 | void Song::setMp3Url(const QString &mp3Url) 24 | { 25 | if (m_mp3Url != mp3Url) { 26 | m_mp3Url = mp3Url; 27 | emit mp3UrlChanged(); 28 | } 29 | } 30 | QString Song::picUrl() const 31 | { 32 | return m_picUrl; 33 | } 34 | 35 | void Song::setPicUrl(const QString &picUrl) 36 | { 37 | if (m_picUrl != picUrl) { 38 | m_picUrl = picUrl; 39 | emit picUrlChanged(); 40 | } 41 | } 42 | QString Song::artist() const 43 | { 44 | return m_artist; 45 | } 46 | 47 | void Song::setArtist(const QString &artist) 48 | { 49 | if (m_artist != artist) { 50 | m_artist = artist; 51 | emit artistChanged(); 52 | } 53 | } 54 | QString Song::name() const 55 | { 56 | return m_name; 57 | } 58 | 59 | void Song::setName(const QString &name) 60 | { 61 | if (m_name != name) { 62 | m_name = name; 63 | emit nameChanged(); 64 | } 65 | } 66 | 67 | QString Song::album() const 68 | { 69 | return m_album; 70 | } 71 | 72 | void Song::setAlbum(const QString &album) 73 | { 74 | if (m_album != album) { 75 | m_album = album; 76 | emit albumChanged(); 77 | } 78 | } 79 | QString Song::id() const 80 | { 81 | return m_id; 82 | } 83 | 84 | void Song::setId(const QString &id) 85 | { 86 | if (m_id != id) { 87 | m_id = id; 88 | emit idChanged(); 89 | } 90 | } 91 | QString Song::lyric() const 92 | { 93 | return m_lyric; 94 | } 95 | 96 | void Song::setLyric(const QString &lyric) 97 | { 98 | if (m_lyric != lyric) { 99 | m_lyric = lyric; 100 | emit lyricChanged(); 101 | } 102 | } 103 | int Song::duration() const 104 | { 105 | return m_duration; 106 | } 107 | 108 | void Song::setDuration(const int duration) 109 | { 110 | if (m_duration != duration) { 111 | m_duration = duration; 112 | emit durationChanged(); 113 | } 114 | } 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/song.h: -------------------------------------------------------------------------------- 1 | #ifndef SONG_H 2 | #define SONG_H 3 | 4 | #include 5 | 6 | class Song : public QObject 7 | { 8 | Q_OBJECT 9 | public: 10 | explicit Song(QObject *parent = 0); 11 | Song(QString& id, QString& name, QString& mp3Url, QString& picUrl, 12 | QString& artist, QString& album, int duration, QObject *parent=0); 13 | 14 | Q_PROPERTY(QString mp3Url READ mp3Url WRITE setMp3Url NOTIFY mp3UrlChanged) 15 | Q_PROPERTY(QString picUrl READ picUrl WRITE setPicUrl NOTIFY picUrlChanged) 16 | Q_PROPERTY(QString artist READ artist WRITE setArtist NOTIFY artistChanged) 17 | Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) 18 | Q_PROPERTY(QString album READ album WRITE setAlbum NOTIFY albumChanged) 19 | Q_PROPERTY(QString id READ id WRITE setId NOTIFY idChanged) 20 | Q_PROPERTY(QString lyric READ lyric WRITE setLyric NOTIFY lyricChanged) 21 | Q_PROPERTY(int duration READ duration WRITE setDuration NOTIFY durationChanged) 22 | 23 | QString mp3Url() const; 24 | void setMp3Url(const QString &mp3Url); 25 | 26 | QString picUrl() const; 27 | void setPicUrl(const QString &picUrl); 28 | 29 | QString artist() const; 30 | void setArtist(const QString &artist); 31 | 32 | QString name() const; 33 | void setName(const QString &name); 34 | 35 | QString album() const; 36 | void setAlbum(const QString &album); 37 | 38 | QString id() const; 39 | void setId(const QString &id); 40 | 41 | QString lyric() const; 42 | void setLyric(const QString &lyric); 43 | 44 | int duration() const; 45 | void setDuration(const int duration); 46 | 47 | signals: 48 | void idChanged(); 49 | void mp3UrlChanged(); 50 | void picUrlChanged(); 51 | void artistChanged(); 52 | void nameChanged(); 53 | void albumChanged(); 54 | void lyricChanged(); 55 | void durationChanged(); 56 | 57 | private: 58 | QString m_id; 59 | QString m_name; 60 | QString m_mp3Url; 61 | QString m_picUrl; 62 | QString m_artist; 63 | QString m_album; 64 | QString m_lyric; 65 | int m_duration; 66 | }; 67 | 68 | #endif // SONG_H 69 | --------------------------------------------------------------------------------