├── icons ├── lastfm.png └── gnome │ ├── 16x16 │ ├── actions │ │ ├── go-up.png │ │ ├── go-down.png │ │ ├── go-home.png │ │ ├── view-restore.png │ │ ├── view-fullscreen.png │ │ ├── application-exit.png │ │ ├── media-playback-start.png │ │ ├── media-playback-stop.png │ │ ├── media-skip-backward.png │ │ └── media-skip-forward.png │ ├── places │ │ └── folder.png │ ├── mimetypes │ │ ├── sound.png │ │ ├── audio-x-generic.png │ │ └── text-x-generic.png │ ├── emblems │ │ └── emblem-web.png │ ├── devices │ │ └── media-cdrom.png │ ├── apps │ │ └── system-file-manager.png │ └── categories │ │ └── preferences-other.png │ ├── 22x22 │ ├── actions │ │ ├── go-up.png │ │ ├── go-down.png │ │ ├── go-home.png │ │ ├── view-restore.png │ │ ├── view-fullscreen.png │ │ ├── application-exit.png │ │ ├── media-playback-start.png │ │ ├── media-playback-stop.png │ │ ├── media-skip-backward.png │ │ └── media-skip-forward.png │ ├── places │ │ └── folder.png │ ├── mimetypes │ │ ├── sound.png │ │ ├── audio-x-generic.png │ │ └── text-x-generic.png │ ├── emblems │ │ └── emblem-web.png │ ├── devices │ │ └── media-cdrom.png │ ├── apps │ │ └── system-file-manager.png │ └── categories │ │ └── preferences-other.png │ ├── 24x24 │ ├── actions │ │ ├── go-up.png │ │ ├── go-down.png │ │ ├── go-home.png │ │ ├── view-restore.png │ │ ├── view-fullscreen.png │ │ ├── application-exit.png │ │ ├── media-playback-start.png │ │ ├── media-playback-stop.png │ │ ├── media-skip-backward.png │ │ └── media-skip-forward.png │ ├── places │ │ └── folder.png │ ├── mimetypes │ │ ├── sound.png │ │ ├── audio-x-generic.png │ │ └── text-x-generic.png │ ├── emblems │ │ └── emblem-web.png │ ├── devices │ │ └── media-cdrom.png │ ├── apps │ │ └── system-file-manager.png │ └── categories │ │ └── preferences-other.png │ ├── 32x32 │ ├── actions │ │ ├── go-up.png │ │ ├── go-down.png │ │ ├── go-home.png │ │ ├── view-restore.png │ │ ├── view-fullscreen.png │ │ ├── application-exit.png │ │ ├── media-playback-start.png │ │ ├── media-playback-stop.png │ │ ├── media-skip-backward.png │ │ └── media-skip-forward.png │ ├── places │ │ └── folder.png │ ├── mimetypes │ │ ├── sound.png │ │ ├── audio-x-generic.png │ │ └── text-x-generic.png │ ├── emblems │ │ └── emblem-web.png │ ├── devices │ │ └── media-cdrom.png │ ├── apps │ │ └── system-file-manager.png │ └── categories │ │ └── preferences-other.png │ ├── 48x48 │ ├── actions │ │ ├── go-up.png │ │ ├── go-down.png │ │ ├── go-home.png │ │ ├── view-restore.png │ │ ├── view-fullscreen.png │ │ ├── application-exit.png │ │ ├── media-playback-start.png │ │ ├── media-playback-stop.png │ │ ├── media-skip-backward.png │ │ └── media-skip-forward.png │ ├── places │ │ └── folder.png │ ├── mimetypes │ │ ├── sound.png │ │ ├── audio-x-generic.png │ │ └── text-x-generic.png │ ├── emblems │ │ └── emblem-web.png │ ├── devices │ │ └── media-cdrom.png │ ├── apps │ │ └── system-file-manager.png │ └── categories │ │ └── preferences-other.png │ ├── 256x256 │ ├── places │ │ └── folder.png │ ├── mimetypes │ │ ├── sound.png │ │ ├── audio-x-generic.png │ │ └── text-x-generic.png │ ├── devices │ │ └── media-cdrom.png │ └── apps │ │ └── system-file-manager.png │ └── index.theme ├── extra_images.qrc ├── .gitignore ├── src ├── iconfactory.h ├── separator.h ├── settings │ ├── contextsettings.h │ ├── lastfmsettings.h │ ├── settingsdialog │ │ ├── widgets │ │ │ ├── contextsettingswidget.h │ │ │ ├── foldersettingswidget.h │ │ │ ├── lastfmsettingswidget.h │ │ │ ├── contextsettingswidget.cpp │ │ │ ├── lastfmsettingswidget.cpp │ │ │ └── foldersettingswidget.cpp │ │ ├── settingsdialog.h │ │ └── settingsdialog.cpp │ ├── applicationsettings.h │ ├── contextsettings.cpp │ ├── lastfmsettings.cpp │ └── applicationsettings.cpp ├── iconfactory.cpp ├── mainwindow │ ├── mainnotebook │ │ ├── collection │ │ │ ├── collectiontreeview.h │ │ │ ├── collectiontreewidgetitem.h │ │ │ ├── collectiontreewidgetitem.cpp │ │ │ ├── collectiontreewidget.h │ │ │ └── collectiontreewidget.cpp │ │ ├── context │ │ │ ├── abstractcontainer.h │ │ │ ├── lyricswidget.h │ │ │ ├── contextwidget.h │ │ │ ├── artistinfowidget.h │ │ │ ├── lyricswidget.cpp │ │ │ ├── abstractcontainer.cpp │ │ │ ├── artistinfowidget.cpp │ │ │ └── contextwidget.cpp │ │ ├── filesystem │ │ │ ├── filesystemwidget.h │ │ │ └── filesystemwidget.cpp │ │ ├── mainnotebook.h │ │ └── mainnotebook.cpp │ ├── playlist │ │ ├── playlistitem.h │ │ ├── playlistitem.cpp │ │ ├── playlistwidget.h │ │ └── playlistwidget.cpp │ ├── mainwindow.h │ ├── playerbar.h │ ├── mainwindow.cpp │ └── playerbar.cpp ├── separator.cpp ├── main.cpp ├── music.h ├── services │ ├── lastfmcontext.h │ ├── lyricsdownloader.h │ ├── lastfmscrobbler.h │ ├── lyricsdownloader.cpp │ ├── lastfmcontext.cpp │ └── lastfmscrobbler.cpp ├── collection │ ├── collectionservice.h │ ├── collectiondatabase.h │ ├── collectionservice.cpp │ └── collectiondatabase.cpp └── music.cpp ├── README ├── Player.pro └── icons.qrc /icons/lastfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/lastfm.png -------------------------------------------------------------------------------- /icons/gnome/16x16/actions/go-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/actions/go-up.png -------------------------------------------------------------------------------- /icons/gnome/16x16/places/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/places/folder.png -------------------------------------------------------------------------------- /icons/gnome/22x22/actions/go-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/actions/go-up.png -------------------------------------------------------------------------------- /icons/gnome/22x22/places/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/places/folder.png -------------------------------------------------------------------------------- /icons/gnome/24x24/actions/go-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/actions/go-up.png -------------------------------------------------------------------------------- /icons/gnome/24x24/places/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/places/folder.png -------------------------------------------------------------------------------- /icons/gnome/32x32/actions/go-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/actions/go-up.png -------------------------------------------------------------------------------- /icons/gnome/32x32/places/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/places/folder.png -------------------------------------------------------------------------------- /icons/gnome/48x48/actions/go-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/actions/go-up.png -------------------------------------------------------------------------------- /icons/gnome/48x48/places/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/places/folder.png -------------------------------------------------------------------------------- /extra_images.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/lastfm.png 4 | 5 | 6 | -------------------------------------------------------------------------------- /icons/gnome/16x16/actions/go-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/actions/go-down.png -------------------------------------------------------------------------------- /icons/gnome/16x16/actions/go-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/actions/go-home.png -------------------------------------------------------------------------------- /icons/gnome/16x16/mimetypes/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/mimetypes/sound.png -------------------------------------------------------------------------------- /icons/gnome/22x22/actions/go-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/actions/go-down.png -------------------------------------------------------------------------------- /icons/gnome/22x22/actions/go-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/actions/go-home.png -------------------------------------------------------------------------------- /icons/gnome/22x22/mimetypes/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/mimetypes/sound.png -------------------------------------------------------------------------------- /icons/gnome/24x24/actions/go-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/actions/go-down.png -------------------------------------------------------------------------------- /icons/gnome/24x24/actions/go-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/actions/go-home.png -------------------------------------------------------------------------------- /icons/gnome/24x24/mimetypes/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/mimetypes/sound.png -------------------------------------------------------------------------------- /icons/gnome/256x256/places/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/256x256/places/folder.png -------------------------------------------------------------------------------- /icons/gnome/32x32/actions/go-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/actions/go-down.png -------------------------------------------------------------------------------- /icons/gnome/32x32/actions/go-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/actions/go-home.png -------------------------------------------------------------------------------- /icons/gnome/32x32/mimetypes/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/mimetypes/sound.png -------------------------------------------------------------------------------- /icons/gnome/48x48/actions/go-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/actions/go-down.png -------------------------------------------------------------------------------- /icons/gnome/48x48/actions/go-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/actions/go-home.png -------------------------------------------------------------------------------- /icons/gnome/48x48/mimetypes/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/mimetypes/sound.png -------------------------------------------------------------------------------- /icons/gnome/16x16/emblems/emblem-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/emblems/emblem-web.png -------------------------------------------------------------------------------- /icons/gnome/22x22/emblems/emblem-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/emblems/emblem-web.png -------------------------------------------------------------------------------- /icons/gnome/24x24/emblems/emblem-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/emblems/emblem-web.png -------------------------------------------------------------------------------- /icons/gnome/256x256/mimetypes/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/256x256/mimetypes/sound.png -------------------------------------------------------------------------------- /icons/gnome/32x32/emblems/emblem-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/emblems/emblem-web.png -------------------------------------------------------------------------------- /icons/gnome/48x48/emblems/emblem-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/emblems/emblem-web.png -------------------------------------------------------------------------------- /icons/gnome/16x16/actions/view-restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/actions/view-restore.png -------------------------------------------------------------------------------- /icons/gnome/16x16/devices/media-cdrom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/devices/media-cdrom.png -------------------------------------------------------------------------------- /icons/gnome/22x22/actions/view-restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/actions/view-restore.png -------------------------------------------------------------------------------- /icons/gnome/22x22/devices/media-cdrom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/devices/media-cdrom.png -------------------------------------------------------------------------------- /icons/gnome/24x24/actions/view-restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/actions/view-restore.png -------------------------------------------------------------------------------- /icons/gnome/24x24/devices/media-cdrom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/devices/media-cdrom.png -------------------------------------------------------------------------------- /icons/gnome/32x32/actions/view-restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/actions/view-restore.png -------------------------------------------------------------------------------- /icons/gnome/32x32/devices/media-cdrom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/devices/media-cdrom.png -------------------------------------------------------------------------------- /icons/gnome/48x48/actions/view-restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/actions/view-restore.png -------------------------------------------------------------------------------- /icons/gnome/48x48/devices/media-cdrom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/devices/media-cdrom.png -------------------------------------------------------------------------------- /icons/gnome/16x16/actions/view-fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/actions/view-fullscreen.png -------------------------------------------------------------------------------- /icons/gnome/22x22/actions/view-fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/actions/view-fullscreen.png -------------------------------------------------------------------------------- /icons/gnome/24x24/actions/view-fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/actions/view-fullscreen.png -------------------------------------------------------------------------------- /icons/gnome/256x256/devices/media-cdrom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/256x256/devices/media-cdrom.png -------------------------------------------------------------------------------- /icons/gnome/32x32/actions/view-fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/actions/view-fullscreen.png -------------------------------------------------------------------------------- /icons/gnome/48x48/actions/view-fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/actions/view-fullscreen.png -------------------------------------------------------------------------------- /icons/gnome/16x16/actions/application-exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/actions/application-exit.png -------------------------------------------------------------------------------- /icons/gnome/16x16/apps/system-file-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/apps/system-file-manager.png -------------------------------------------------------------------------------- /icons/gnome/16x16/mimetypes/audio-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/mimetypes/audio-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/16x16/mimetypes/text-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/mimetypes/text-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/22x22/actions/application-exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/actions/application-exit.png -------------------------------------------------------------------------------- /icons/gnome/22x22/apps/system-file-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/apps/system-file-manager.png -------------------------------------------------------------------------------- /icons/gnome/22x22/mimetypes/audio-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/mimetypes/audio-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/22x22/mimetypes/text-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/mimetypes/text-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/24x24/actions/application-exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/actions/application-exit.png -------------------------------------------------------------------------------- /icons/gnome/24x24/apps/system-file-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/apps/system-file-manager.png -------------------------------------------------------------------------------- /icons/gnome/24x24/mimetypes/audio-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/mimetypes/audio-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/24x24/mimetypes/text-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/mimetypes/text-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/32x32/actions/application-exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/actions/application-exit.png -------------------------------------------------------------------------------- /icons/gnome/32x32/apps/system-file-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/apps/system-file-manager.png -------------------------------------------------------------------------------- /icons/gnome/32x32/mimetypes/audio-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/mimetypes/audio-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/32x32/mimetypes/text-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/mimetypes/text-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/48x48/actions/application-exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/actions/application-exit.png -------------------------------------------------------------------------------- /icons/gnome/48x48/apps/system-file-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/apps/system-file-manager.png -------------------------------------------------------------------------------- /icons/gnome/48x48/mimetypes/audio-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/mimetypes/audio-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/48x48/mimetypes/text-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/mimetypes/text-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/16x16/actions/media-playback-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/actions/media-playback-start.png -------------------------------------------------------------------------------- /icons/gnome/16x16/actions/media-playback-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/actions/media-playback-stop.png -------------------------------------------------------------------------------- /icons/gnome/16x16/actions/media-skip-backward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/actions/media-skip-backward.png -------------------------------------------------------------------------------- /icons/gnome/16x16/actions/media-skip-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/actions/media-skip-forward.png -------------------------------------------------------------------------------- /icons/gnome/16x16/categories/preferences-other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/16x16/categories/preferences-other.png -------------------------------------------------------------------------------- /icons/gnome/22x22/actions/media-playback-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/actions/media-playback-start.png -------------------------------------------------------------------------------- /icons/gnome/22x22/actions/media-playback-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/actions/media-playback-stop.png -------------------------------------------------------------------------------- /icons/gnome/22x22/actions/media-skip-backward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/actions/media-skip-backward.png -------------------------------------------------------------------------------- /icons/gnome/22x22/actions/media-skip-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/actions/media-skip-forward.png -------------------------------------------------------------------------------- /icons/gnome/22x22/categories/preferences-other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/22x22/categories/preferences-other.png -------------------------------------------------------------------------------- /icons/gnome/24x24/actions/media-playback-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/actions/media-playback-start.png -------------------------------------------------------------------------------- /icons/gnome/24x24/actions/media-playback-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/actions/media-playback-stop.png -------------------------------------------------------------------------------- /icons/gnome/24x24/actions/media-skip-backward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/actions/media-skip-backward.png -------------------------------------------------------------------------------- /icons/gnome/24x24/actions/media-skip-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/actions/media-skip-forward.png -------------------------------------------------------------------------------- /icons/gnome/24x24/categories/preferences-other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/24x24/categories/preferences-other.png -------------------------------------------------------------------------------- /icons/gnome/256x256/apps/system-file-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/256x256/apps/system-file-manager.png -------------------------------------------------------------------------------- /icons/gnome/256x256/mimetypes/audio-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/256x256/mimetypes/audio-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/256x256/mimetypes/text-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/256x256/mimetypes/text-x-generic.png -------------------------------------------------------------------------------- /icons/gnome/32x32/actions/media-playback-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/actions/media-playback-start.png -------------------------------------------------------------------------------- /icons/gnome/32x32/actions/media-playback-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/actions/media-playback-stop.png -------------------------------------------------------------------------------- /icons/gnome/32x32/actions/media-skip-backward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/actions/media-skip-backward.png -------------------------------------------------------------------------------- /icons/gnome/32x32/actions/media-skip-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/actions/media-skip-forward.png -------------------------------------------------------------------------------- /icons/gnome/32x32/categories/preferences-other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/32x32/categories/preferences-other.png -------------------------------------------------------------------------------- /icons/gnome/48x48/actions/media-playback-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/actions/media-playback-start.png -------------------------------------------------------------------------------- /icons/gnome/48x48/actions/media-playback-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/actions/media-playback-stop.png -------------------------------------------------------------------------------- /icons/gnome/48x48/actions/media-skip-backward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/actions/media-skip-backward.png -------------------------------------------------------------------------------- /icons/gnome/48x48/actions/media-skip-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/actions/media-skip-forward.png -------------------------------------------------------------------------------- /icons/gnome/48x48/categories/preferences-other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esdrasbeleza/audactile/HEAD/icons/gnome/48x48/categories/preferences-other.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore .o files 2 | *.o 3 | # Ignore executable file 4 | Player 5 | 6 | # Igore IDE user files 7 | Player.pro.user 8 | 9 | # Ignore moc_* and qrc_* files generated by IDE, Qt, whatever 10 | moc_* 11 | qrc_* 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/iconfactory.h: -------------------------------------------------------------------------------- 1 | #ifndef ICONFACTORY_H 2 | #define ICONFACTORY_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class IconFactory : public QObject 9 | { 10 | 11 | public: 12 | static QIcon fromTheme(QString iconName); 13 | 14 | 15 | }; 16 | 17 | #endif // ICONFACTORY_H 18 | -------------------------------------------------------------------------------- /src/separator.h: -------------------------------------------------------------------------------- 1 | #ifndef SEPARATOR_H 2 | #define SEPARATOR_H 3 | 4 | #include 5 | 6 | class Separator : public QFrame 7 | { 8 | public: 9 | Separator(QWidget *parent = NULL); 10 | static Separator *horizontalSeparator(QWidget *parent = NULL); 11 | static Separator *verticalSeparator(QWidget *parent = NULL); 12 | }; 13 | 14 | #endif // SEPARATOR_H 15 | -------------------------------------------------------------------------------- /src/settings/contextsettings.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTEXTSETTINGS_H 2 | #define CONTEXTSETTINGS_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class ContextSettings 9 | { 10 | public: 11 | static void setFetchArtistInfoActive(bool active); 12 | static void setFetchLyricsActive(bool active); 13 | static bool isFetchArtistInfoActive(); 14 | static bool isFetchLyricsActive(); 15 | 16 | }; 17 | 18 | #endif // CONTEXTSETTINGS_H 19 | -------------------------------------------------------------------------------- /src/iconfactory.cpp: -------------------------------------------------------------------------------- 1 | #include "iconfactory.h" 2 | 3 | #ifdef Q_WS_X11 4 | QIcon IconFactory::fromTheme(QString iconName) { 5 | return QIcon::fromTheme(iconName); 6 | } 7 | 8 | #else 9 | /* 10 | * TODO: icons that will be used in Mac OS and Windows 11 | */ 12 | QIcon IconFactory::fromTheme(QString iconName) { 13 | QIcon::setThemeSearchPaths(QStringList() << QLatin1String(":/icons")); 14 | QIcon::setThemeName("gnome"); 15 | return QIcon::fromTheme(iconName); 16 | } 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/collection/collectiontreeview.h: -------------------------------------------------------------------------------- 1 | #ifndef COLLECTIONTREEVIEW_H 2 | #define COLLECTIONTREEVIEW_H 3 | 4 | #include 5 | #include 6 | #include "../../../collection/collectionservice.h" 7 | 8 | class CollectionTreeView : public QTreeView 9 | { 10 | Q_OBJECT 11 | public: 12 | explicit CollectionTreeView(QWidget *parent = 0); 13 | 14 | signals: 15 | 16 | public slots: 17 | 18 | private: 19 | CollectionService *service; 20 | 21 | }; 22 | 23 | #endif // COLLECTIONTREEVIEW_H 24 | -------------------------------------------------------------------------------- /src/settings/lastfmsettings.h: -------------------------------------------------------------------------------- 1 | #ifndef LASTFMSETTINGS_H 2 | #define LASTFMSETTINGS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class LastFmSettings 10 | { 11 | public: 12 | static QString username(); 13 | static QString password(); 14 | static bool isActive(); 15 | static void setUsername(QString username); 16 | static void setPassword(QString password); 17 | static void setActive(bool active); 18 | 19 | 20 | 21 | }; 22 | 23 | #endif // LASTFMSETTINGS_H 24 | -------------------------------------------------------------------------------- /src/settings/settingsdialog/widgets/contextsettingswidget.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTEXTSETTINGSWIDGET_H 2 | #define CONTEXTSETTINGSWIDGET_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "../../contextsettings.h" 8 | 9 | class ContextSettingsWidget : public QWidget 10 | { 11 | public: 12 | explicit ContextSettingsWidget(QWidget *parent = 0); 13 | void applySettings(); 14 | 15 | private: 16 | QCheckBox *fetchSummary; 17 | QCheckBox *fetchLyrics; 18 | 19 | }; 20 | 21 | #endif // CONTEXTSETTINGSWIDGET_H 22 | -------------------------------------------------------------------------------- /src/separator.cpp: -------------------------------------------------------------------------------- 1 | #include "separator.h" 2 | 3 | Separator::Separator(QWidget *parent) : QFrame(parent) 4 | { 5 | } 6 | 7 | 8 | Separator *Separator::horizontalSeparator(QWidget *parent) { 9 | Separator *separator = new Separator(parent); 10 | separator->setFrameShape(QFrame::HLine); 11 | separator->setFrameShadow(QFrame::Sunken); 12 | return separator; 13 | } 14 | 15 | Separator *Separator::verticalSeparator(QWidget *parent) { 16 | Separator *separator = new Separator(parent); 17 | separator->setFrameShape(QFrame::VLine); 18 | separator->setFrameShadow(QFrame::Sunken); 19 | return separator; 20 | } 21 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "mainwindow/mainwindow.h" 4 | #include "settings/applicationsettings.h" 5 | #include "collection/collectionservice.h" 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | QApplication a(argc, argv); 10 | a.setOrganizationName("Audactile"); 11 | a.setApplicationName("Audactile"); 12 | a.setApplicationVersion("0.1"); 13 | 14 | // Read and/or set application settings 15 | ApplicationSettings::createAppDirIfNeeded(); 16 | ApplicationSettings::initialisation(); 17 | 18 | MainWindow w; 19 | w.showMaximized(); 20 | return a.exec(); 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/mainwindow/playlist/playlistitem.h: -------------------------------------------------------------------------------- 1 | #ifndef PLAYLISTITEM_H 2 | #define PLAYLISTITEM_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "../../music.h" 9 | 10 | 11 | class PlaylistItem : public QTreeWidgetItem 12 | { 13 | 14 | public: 15 | PlaylistItem(QString); 16 | PlaylistItem(QUrl url); 17 | Music * getMusic(); 18 | bool isValid(); 19 | void setBold(); 20 | void removeBold(); 21 | int index; // Index where item must be inserted at PlaylistWidget 22 | 23 | 24 | private: 25 | QUrl fileUrl; 26 | Music *music; 27 | void loadFile(); 28 | }; 29 | 30 | #endif // PLAYLISTITEM_H 31 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/context/abstractcontainer.h: -------------------------------------------------------------------------------- 1 | #ifndef ABSTRACTCONTAINER_H 2 | #define ABSTRACTCONTAINER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class AbstractContainer : public QFrame 12 | { 13 | Q_OBJECT 14 | public: 15 | AbstractContainer(QWidget *parent = 0); 16 | void setHtml(QString html); 17 | QWebView *contentView; 18 | 19 | private: 20 | QString header; 21 | QString footer; 22 | 23 | private slots: 24 | void openLinksInExternalWindow(QUrl url); 25 | 26 | signals: 27 | 28 | public slots: 29 | 30 | }; 31 | 32 | #endif // ABSTRACTCONTAINER_H 33 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/context/lyricswidget.h: -------------------------------------------------------------------------------- 1 | #ifndef LYRICSWIDGET_H 2 | #define LYRICSWIDGET_H 3 | 4 | #include 5 | #include 6 | #include "abstractcontainer.h" 7 | #include "../../../services/lyricsdownloader.h" 8 | 9 | class LyricsWidget : public AbstractContainer 10 | { 11 | Q_OBJECT 12 | 13 | public: 14 | LyricsWidget(QWidget *parent); 15 | void songInformationUpdated(QMap newContextInformation); 16 | void resetLabels(); 17 | 18 | private: 19 | LyricsDownloader *lyricsProvider; 20 | QMap lastRequest; 21 | 22 | private slots: 23 | void showLyrics(QWebElement webElement); 24 | 25 | }; 26 | 27 | #endif // LYRICSWIDGET_H 28 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Audactile 2 | --------- 3 | 4 | Audactile is my current personal project of create a media player 5 | with a small, but good set of features. I just want to merge the 6 | best features I've found in another media players and cut the crap 7 | off. 8 | 9 | Some cool guidelines: 10 | 11 | - Simple is good. 12 | - Features are good, since they keep Audactile simple. 13 | - There are too much iTunes clones. 14 | - Amarok 1.x, I miss you. 15 | - I don't know if I need menubars. Old Winamp/XMMS, I miss you too. 16 | - Stability is good. 17 | 18 | 19 | Extra information 20 | ----------------- 21 | 22 | The Last.fm icon used in settings was borrowed from 23 | http://www.gorvan.com/design/icons/last.fm . Its license 24 | is BY-NC-SA. 25 | 26 | -------------------------------------------------------------------------------- /src/settings/settingsdialog/widgets/foldersettingswidget.h: -------------------------------------------------------------------------------- 1 | #ifndef FOLDERSETTINGSWIDGET_H 2 | #define FOLDERSETTINGSWIDGET_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "../../applicationsettings.h" 11 | 12 | class FolderSettingsWidget : public QWidget 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | FolderSettingsWidget(QWidget *parent); 18 | void applySettings(); 19 | 20 | private: 21 | QListWidget *folderList; 22 | QStringList originalFoldersList; 23 | 24 | private slots: 25 | void addFolder(); 26 | void removeFolder(); 27 | 28 | 29 | 30 | 31 | }; 32 | 33 | #endif // FOLDERSETTINGSWIDGET_H 34 | -------------------------------------------------------------------------------- /src/settings/settingsdialog/widgets/lastfmsettingswidget.h: -------------------------------------------------------------------------------- 1 | #ifndef LASTFMSETTINGSWIDGET_H 2 | #define LASTFMSETTINGSWIDGET_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "../../lastfmsettings.h" 10 | 11 | class LastFmSettingsWidget : public QWidget 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | LastFmSettingsWidget(QWidget *parent); 17 | void applySettings(); 18 | 19 | private: 20 | QLabel *usernameLabel; 21 | QLabel *passwordLabel; 22 | QLineEdit *usernameTextBox; 23 | QLineEdit *passwordTextBox; 24 | QCheckBox *active; 25 | 26 | private slots: 27 | void handleLastFmState(bool enabled); 28 | 29 | }; 30 | 31 | #endif // LASTFMSETTINGSWIDGET_H 32 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/collection/collectiontreewidgetitem.h: -------------------------------------------------------------------------------- 1 | #ifndef COLLECTIONTREEWIDGETITEM_H 2 | #define COLLECTIONTREEWIDGETITEM_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class CollectionTreeWidgetItem : public QTreeWidgetItem 9 | { 10 | public: 11 | // TODO: use TreeLevel instead of unsigned int 12 | explicit CollectionTreeWidgetItem(unsigned int level, unsigned int id, QTreeWidget *parent = 0); 13 | unsigned int getNodeLevel(); 14 | unsigned int getId(); 15 | QList getUrlList(QSqlTableModel *collectionModel); 16 | 17 | private: 18 | unsigned int level; 19 | unsigned int id; 20 | 21 | signals: 22 | 23 | public slots: 24 | 25 | }; 26 | 27 | #endif // COLLECTIONTREEWIDGETITEM_H 28 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/context/contextwidget.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTEXTWIDGET_H 2 | #define CONTEXTWIDGET_H 3 | 4 | #include 5 | #include 6 | #include "artistinfowidget.h" 7 | #include "lyricswidget.h" 8 | 9 | class ContextWidget : public QWidget 10 | { 11 | Q_OBJECT 12 | 13 | public: 14 | ContextWidget(QWidget *parent); 15 | 16 | private: 17 | QButtonGroup *buttonGroup; 18 | ArtistInfoWidget *artistInfoWidget; 19 | LyricsWidget *lyricsWidget; 20 | QMap currentContext; 21 | 22 | QPushButton *artistButton; 23 | QPushButton *lyricsButton; 24 | 25 | 26 | public slots: 27 | void songInformationUpdated(QMap newContextInformation); 28 | void fetchButtonPressed(); 29 | }; 30 | 31 | #endif // CONTEXTWIDGET_H 32 | -------------------------------------------------------------------------------- /src/music.h: -------------------------------------------------------------------------------- 1 | #ifndef MUSIC_H 2 | #define MUSIC_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class Music 9 | { 10 | public: 11 | Music(); 12 | Music(QString artist, QString album, QString title, QString path, unsigned int trackNumber); 13 | Music(QUrl fileUrl); 14 | QUrl getFileUrl(); 15 | QString getArtist(); 16 | QString getTitle(); 17 | QString getAlbum(); 18 | unsigned int getTrackNumber(); 19 | QString getSongInfo(); 20 | unsigned int getDuration(); 21 | bool isValid(); 22 | 23 | private: 24 | void readMetaData(); 25 | bool valid; 26 | QUrl fileUrl; 27 | unsigned int trackNumber; 28 | QString artist; 29 | QString title; 30 | QString album; 31 | unsigned int duration; 32 | 33 | }; 34 | 35 | #endif // MUSIC_H 36 | -------------------------------------------------------------------------------- /src/services/lastfmcontext.h: -------------------------------------------------------------------------------- 1 | #ifndef LASTFMCONTEXT_H 2 | #define LASTFMCONTEXT_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | class LastFmContext : public QObject 16 | { 17 | Q_OBJECT 18 | public: 19 | explicit LastFmContext(QObject *parent = 0); 20 | void getInfo(QString artist); 21 | 22 | private: 23 | QNetworkAccessManager *netManager; 24 | QNetworkReply *contextReply; 25 | QMap contextData; 26 | 27 | signals: 28 | void contextUpdated(QMap contextData); 29 | void contextError(); 30 | 31 | private slots: 32 | void readContextReply(); 33 | 34 | }; 35 | 36 | #endif // LASTFMCONTEXT_H 37 | -------------------------------------------------------------------------------- /src/services/lyricsdownloader.h: -------------------------------------------------------------------------------- 1 | #ifndef LYRICSDOWNLOADER_H 2 | #define LYRICSDOWNLOADER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | class LyricsDownloader : public QObject 15 | { 16 | Q_OBJECT 17 | public: 18 | explicit LyricsDownloader(QObject *parent = 0); 19 | void getLyrics(QString artist, QString song); 20 | 21 | private: 22 | QWebPage *page; 23 | QNetworkAccessManager *netManager; 24 | QNetworkReply *contextReply; 25 | 26 | signals: 27 | void lyricsReady(QWebElement); 28 | void lyricsError(); 29 | 30 | public slots: 31 | void fetchLyrics(QUrl); 32 | void render(); 33 | void readContextReply(); 34 | 35 | }; 36 | 37 | #endif // LYRICSDOWNLOADER_H 38 | -------------------------------------------------------------------------------- /src/settings/settingsdialog/settingsdialog.h: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGSDIALOG_H 2 | #define SETTINGSDIALOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "../../separator.h" 10 | #include "../../iconfactory.h" 11 | #include "widgets/foldersettingswidget.h" 12 | #include "widgets/lastfmsettingswidget.h" 13 | #include "widgets/contextsettingswidget.h" 14 | 15 | class SettingsDialog : public QWidget 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | SettingsDialog(QWidget *parent); 21 | 22 | private: 23 | void updateMediaFolders(); 24 | QDialogButtonBox *buttonBox; 25 | FolderSettingsWidget *folderSettingsWidget; 26 | LastFmSettingsWidget *lastFmSettingsWidget; 27 | ContextSettingsWidget *contextSettingsWidget; 28 | 29 | private slots: 30 | void applySettings(); 31 | void handleAbstractButton(QAbstractButton*); 32 | 33 | 34 | }; 35 | 36 | #endif // SETTINGSDIALOG_H 37 | -------------------------------------------------------------------------------- /src/settings/applicationsettings.h: -------------------------------------------------------------------------------- 1 | #ifndef APPLICATIONSETTINGS_H 2 | #define APPLICATIONSETTINGS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "../collection/collectiondatabase.h" 11 | 12 | class ApplicationSettings 13 | { 14 | public: 15 | static void initialisation(); 16 | static QStringList collectionFolderList(); 17 | static void setTabOrder(QString tab, int value); 18 | static int getTabOrder(QString tab); 19 | static void setSplitterSize(float value); 20 | static float getSplitterSize(); 21 | static void addCollectionFolder(QString location); 22 | static void removeColletionFolder(QString location); 23 | static void setCollectionFolders(QStringList folders); 24 | static void createAppDirIfNeeded(); 25 | 26 | private: 27 | QSettings qsettings; 28 | 29 | }; 30 | 31 | #endif // APPLICATIONSETTINGS_H 32 | -------------------------------------------------------------------------------- /src/settings/settingsdialog/widgets/contextsettingswidget.cpp: -------------------------------------------------------------------------------- 1 | #include "contextsettingswidget.h" 2 | 3 | ContextSettingsWidget::ContextSettingsWidget(QWidget *parent) : 4 | QWidget(parent) 5 | { 6 | // Create checkboxes 7 | fetchSummary = new QCheckBox("Fetch artist summary from Last.fm automatically"); 8 | fetchLyrics = new QCheckBox("Fetch lyrics from LyricWiki automatically"); 9 | 10 | // Set their default value 11 | fetchSummary->setChecked(ContextSettings::isFetchArtistInfoActive()); 12 | fetchLyrics->setChecked(ContextSettings::isFetchLyricsActive()); 13 | 14 | // Add them into a layout 15 | QVBoxLayout *layout = new QVBoxLayout(this); 16 | layout->addWidget(fetchSummary); 17 | layout->addWidget(fetchLyrics); 18 | layout->addStretch(0); 19 | 20 | setLayout(layout); 21 | } 22 | 23 | void ContextSettingsWidget::applySettings() { 24 | ContextSettings::setFetchArtistInfoActive(fetchSummary->isChecked()); 25 | ContextSettings::setFetchLyricsActive(fetchLyrics->isChecked()); 26 | } 27 | -------------------------------------------------------------------------------- /src/mainwindow/mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include "mainnotebook/mainnotebook.h" 7 | #include 8 | #include 9 | #include "playlist/playlistwidget.h" 10 | #include "playerbar.h" 11 | #include "../services/lastfmscrobbler.h" 12 | 13 | 14 | 15 | class MainWindow : public QMainWindow 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | MainWindow(QWidget *parent = 0); 21 | PlaylistWidget *playlistWidget; 22 | Phonon::MediaObject *mediaObject; 23 | Phonon::AudioOutput *audioOutput; 24 | 25 | private: 26 | Qt::WindowStates originalWindowState; 27 | qreal outputVolume; 28 | MainNotebook *mainNotebook; 29 | QSplitter *middleSplitter; 30 | void keyPressEvent(QKeyEvent *event); 31 | 32 | private slots: 33 | void handleMute(bool mute); 34 | void handleVolume(qreal volume); 35 | void saveSplitterSize(int pos, int index); 36 | void toggleFullscreen(); 37 | 38 | signals: 39 | void windowStateChanged(Qt::WindowStates); 40 | 41 | }; 42 | 43 | #endif // MAINWINDOW_H 44 | -------------------------------------------------------------------------------- /src/collection/collectionservice.h: -------------------------------------------------------------------------------- 1 | #ifndef COLLECTIONSERVICE_H 2 | #define COLLECTIONSERVICE_H 3 | 4 | #include 5 | #include 6 | #include "../settings/applicationsettings.h" 7 | #include "../collection/collectiondatabase.h" 8 | #include "../music.h" 9 | 10 | class CollectionService : public QThread 11 | { 12 | Q_OBJECT 13 | public: 14 | explicit CollectionService(QObject *parent = 0); 15 | void refresh(); 16 | void run(); 17 | QSqlTableModel *collectionModel(); 18 | QSqlTableModel *artistModel(); 19 | QSqlTableModel *albumModel(); 20 | QSqlTableModel *musicModel(); 21 | 22 | signals: 23 | void scanning(); 24 | void listUpdated(); 25 | void songAdded(Music *music); 26 | void songRemoved(unsigned int id); 27 | 28 | private: 29 | QFileSystemWatcher *watcher; 30 | CollectionDatabase *collectionDb; 31 | void scanRecursive(QString path); 32 | 33 | public slots: 34 | void verifyFiles(); 35 | void scan(); 36 | 37 | private slots: 38 | void fileChanged(QString path); 39 | void dirChanged(QString path); 40 | 41 | 42 | }; 43 | 44 | #endif // COLLECTIONSERVICE_H 45 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/context/artistinfowidget.h: -------------------------------------------------------------------------------- 1 | #ifndef ARTISTINFOWIDGET_H 2 | #define ARTISTINFOWIDGET_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include "abstractcontainer.h" 18 | #include "../../../services/lastfmcontext.h" 19 | 20 | class ArtistInfoWidget : public AbstractContainer 21 | { 22 | Q_OBJECT 23 | 24 | public: 25 | explicit ArtistInfoWidget(QWidget *parent = 0); 26 | void songInformationUpdated(QMap newContextInformation); 27 | void resetLabels(); 28 | 29 | private: 30 | LastFmContext *context; 31 | QMap lastRequest; 32 | QImage pictureData; 33 | QNetworkReply *contextReply; 34 | 35 | 36 | signals: 37 | 38 | public slots: 39 | void updateContextInformation(QMap newContextInformation); 40 | void showContextError(); 41 | }; 42 | 43 | 44 | #endif // ARTISTINFOWIDGET_H 45 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/filesystem/filesystemwidget.h: -------------------------------------------------------------------------------- 1 | #ifndef FILESYSTEMWIDGET_H 2 | #define FILESYSTEMWIDGET_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "../../../iconfactory.h" 14 | #include "../../playlist/playlistitem.h" 15 | #include "../../../iconfactory.h" 16 | 17 | 18 | class FilesystemWidget : public QWidget 19 | { 20 | Q_OBJECT 21 | 22 | public: 23 | FilesystemWidget(QWidget *parent); 24 | 25 | private slots: 26 | void updateWidget(); 27 | void goUp(); 28 | void goHome(); 29 | void pathChanged(); 30 | void doubleClickAt(QModelIndex modelIndex); 31 | void setNameFilter(QString filter); 32 | 33 | signals: 34 | void askToAddItemToPlaylist(PlaylistItem*); 35 | 36 | private: 37 | QDir dir; 38 | QString startLocation; 39 | QFileSystemModel *fsWidgetModel; 40 | QListView *fsListView; 41 | QAction *goUpAction; 42 | QAction *goHomeAction; 43 | QLineEdit* filterEdit; 44 | void setNameFilter(); 45 | }; 46 | 47 | #endif // FILESYSTEMWIDGET_H 48 | -------------------------------------------------------------------------------- /src/collection/collectiondatabase.h: -------------------------------------------------------------------------------- 1 | #ifndef COLLECTIONDATABASE_H 2 | #define COLLECTIONDATABASE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include "../music.h" 16 | 17 | class CollectionDatabase : public QObject 18 | { 19 | Q_OBJECT 20 | public: 21 | explicit CollectionDatabase(QObject *parent = 0); 22 | void addArtist(QString artistName); 23 | void addAlbum(QString artistName, QString albumName); 24 | bool addOrUpdateMusic(Music *music); 25 | void removeArtist(QString artistName); 26 | void removeAlbum(QString artistName, QString albumName); 27 | void removeMusic(Music *music); 28 | void removeMusic(QString path); 29 | QSqlTableModel *collectionModel(); 30 | QSqlTableModel *artistModel(); 31 | QSqlTableModel *albumModel(); 32 | QSqlTableModel *musicModel(); 33 | 34 | signals: 35 | 36 | public slots: 37 | 38 | private: 39 | QSqlDatabase db; 40 | void createDatabase(); 41 | 42 | }; 43 | 44 | #endif // COLLECTIONDATABASE_H 45 | -------------------------------------------------------------------------------- /src/settings/contextsettings.cpp: -------------------------------------------------------------------------------- 1 | #include "contextsettings.h" 2 | 3 | void ContextSettings::setFetchArtistInfoActive(bool active) { 4 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 5 | settings.beginGroup("context"); 6 | settings.setValue("fetch_artist_info", active); 7 | settings.endGroup(); 8 | } 9 | 10 | bool ContextSettings::isFetchArtistInfoActive() { 11 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 12 | settings.beginGroup("context"); 13 | bool ret = settings.value("fetch_artist_info", true).toBool(); 14 | settings.endGroup(); 15 | return ret; 16 | } 17 | 18 | void ContextSettings::setFetchLyricsActive(bool active) { 19 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 20 | settings.beginGroup("context"); 21 | settings.setValue("fetch_lyrics", active); 22 | settings.endGroup(); 23 | } 24 | 25 | bool ContextSettings::isFetchLyricsActive() { 26 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 27 | settings.beginGroup("context"); 28 | bool ret = settings.value("fetch_lyrics", false).toBool(); 29 | settings.endGroup(); 30 | return ret; 31 | } 32 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/mainnotebook.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINNOTEBOOK_H 2 | #define MAINNOTEBOOK_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "QProgressBar" 10 | #include "../../settings/applicationsettings.h" 11 | #include "../playlist/playlistwidget.h" 12 | #include "filesystem/filesystemwidget.h" 13 | #include "collection/collectiontreewidget.h" 14 | #include "context/contextwidget.h" 15 | 16 | class MainNotebook : public QTabWidget 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | MainNotebook(QWidget *parent, PlaylistWidget *playlistWidget); 22 | 23 | private slots: 24 | void saveTabOrder(); 25 | void showCollectionProgress(); // TODO: put this in a better place 26 | void hideCollectionProgress(); // TODO: put this in a better place 27 | 28 | private: 29 | int collectionPosition; 30 | int contextPosition; 31 | int filesystemPosition; 32 | int playlistsPosition; 33 | 34 | // Collection progressbar -- TODO: put this in a better place 35 | QWidget *progressContainer; 36 | QProgressBar *scanProgress; 37 | QLabel *scanLabel; 38 | 39 | // Widgets 40 | CollectionTreeWidget *collectionWidget; 41 | QWidget *collectionContainer; 42 | QWidget *contextContainer; 43 | QWidget *filesystemContainer; 44 | QLabel *testLabel3; 45 | 46 | }; 47 | 48 | #endif // MAINNOTEBOOK_H 49 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/collection/collectiontreewidgetitem.cpp: -------------------------------------------------------------------------------- 1 | #include "collectiontreewidgetitem.h" 2 | #include "collectiontreewidget.h" 3 | 4 | CollectionTreeWidgetItem::CollectionTreeWidgetItem(unsigned int level, unsigned int id, QTreeWidget *parent) : 5 | QTreeWidgetItem(parent, 0) 6 | { 7 | this->level = level; 8 | this->id = id; 9 | } 10 | 11 | unsigned int CollectionTreeWidgetItem::getNodeLevel() { 12 | return level; 13 | } 14 | 15 | unsigned int CollectionTreeWidgetItem::getId() { 16 | return id; 17 | } 18 | 19 | QList CollectionTreeWidgetItem::getUrlList(QSqlTableModel *collectionModel) { 20 | QList urlList; 21 | 22 | QString idType; 23 | if (level == CollectionTreeWidget::LevelArtist) { 24 | idType = "id_artist"; 25 | } 26 | else if (level == CollectionTreeWidget::LevelAlbum) { 27 | idType = "id_album"; 28 | } 29 | else if (level == CollectionTreeWidget::LevelMusic) { 30 | idType = "id_music"; 31 | } 32 | collectionModel->setFilter(idType + " = " + QString::number(id)); 33 | collectionModel->select(); 34 | 35 | while (collectionModel->canFetchMore()) collectionModel->fetchMore(); 36 | int total = collectionModel->rowCount(); 37 | 38 | for (int i = 0; i < total; i++) { 39 | QString path = collectionModel->record(i).value(collectionModel->fieldIndex("path")).toString(); 40 | QUrl url(path); 41 | urlList.append(url); 42 | } 43 | 44 | return urlList; 45 | } 46 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/context/lyricswidget.cpp: -------------------------------------------------------------------------------- 1 | #include "lyricswidget.h" 2 | 3 | LyricsWidget::LyricsWidget(QWidget *parent) : AbstractContainer(parent) 4 | { 5 | resetLabels(); 6 | 7 | // Try to fetch lyrics 8 | lyricsProvider = new LyricsDownloader(); 9 | connect(lyricsProvider, SIGNAL(lyricsReady(QWebElement)), this, SLOT(showLyrics(QWebElement))); 10 | } 11 | 12 | void LyricsWidget::songInformationUpdated(QMap newContextInformation) { 13 | lastRequest = newContextInformation; 14 | QString html("
" + newContextInformation.value("artist") + " - " + newContextInformation.value("title") + "
"); 15 | setHtml(html); 16 | lyricsProvider->getLyrics(newContextInformation.value("artist"), newContextInformation.value("title")); 17 | } 18 | 19 | void LyricsWidget::showLyrics(QWebElement webElement) { 20 | // FIXME: if you change song before the lyrics are loaded, the lyrics shown are for the previous song 21 | QString html("
" + lastRequest.value("artist") + " - " + lastRequest.value("title") + "
"); 22 | setHtml(html + "

" + webElement.toOuterXml() + "

"); 23 | 24 | // Code to try to remove some tags 25 | QString code = "$('object').remove()"; 26 | contentView->page()->mainFrame()->evaluateJavaScript(code); 27 | } 28 | 29 | void LyricsWidget::resetLabels() { 30 | setHtml("
Listen a song and push the Fetch button above to get the song lyrics!
"); 31 | } 32 | -------------------------------------------------------------------------------- /src/mainwindow/playlist/playlistitem.cpp: -------------------------------------------------------------------------------- 1 | #include "playlistitem.h" 2 | 3 | // TODO: if path is invalid, show error 4 | 5 | 6 | PlaylistItem::PlaylistItem(QString itemFilePath) 7 | { 8 | fileUrl = QUrl().fromLocalFile(itemFilePath); 9 | loadFile(); 10 | } 11 | 12 | PlaylistItem::PlaylistItem(QUrl url) { 13 | fileUrl = url; 14 | loadFile(); 15 | } 16 | 17 | void PlaylistItem::loadFile() { 18 | music = new Music(fileUrl); 19 | 20 | if (music->getTrackNumber() > 0) { setData(0, Qt::DisplayRole, music->getTrackNumber()); } 21 | else { setData(0, Qt::DisplayRole, QString("")); } 22 | 23 | QString qStr; 24 | int duration = music->getDuration(); 25 | int secs = duration % 60; 26 | int mins = duration / 60; 27 | 28 | setText(1, music->getTitle()); 29 | setText(2, music->getAlbum()); 30 | setText(3, music->getArtist()); 31 | setText(4, QString::number(mins) + ":" + qStr.sprintf("%02d", secs)); 32 | } 33 | 34 | 35 | Music * PlaylistItem::getMusic() { 36 | return music; 37 | } 38 | 39 | bool PlaylistItem::isValid() { 40 | if (music != NULL) { 41 | return music->isValid(); 42 | } 43 | else { 44 | return false; 45 | } 46 | } 47 | 48 | void PlaylistItem::setBold() { 49 | for (int i = 0; i < columnCount(); i++) { 50 | QFont itemFont = font(i); 51 | itemFont.setBold(true); 52 | setFont(i, itemFont); 53 | } 54 | } 55 | 56 | void PlaylistItem::removeBold() { 57 | for (int i = 0; i < columnCount(); i++) { 58 | QFont itemFont = font(i); 59 | itemFont.setBold(false); 60 | setFont(i, itemFont); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/collection/collectiontreewidget.h: -------------------------------------------------------------------------------- 1 | #ifndef COLLECTIONTREEWIDGET_H 2 | #define COLLECTIONTREEWIDGET_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "../../../music.h" 10 | #include "../../../iconfactory.h" 11 | #include "../../../collection/collectionservice.h" 12 | #include "collectiontreewidgetitem.h" 13 | 14 | class CollectionTreeWidget : public QTreeWidget 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | CollectionTreeWidget(); 20 | QTreeWidgetItem *addArtist(QString artist, unsigned int id = 0); 21 | QTreeWidgetItem *addAlbum(QString artist, QString album, unsigned int albumId = 0); 22 | QList musicList; 23 | bool removeArtist(QString artist); 24 | bool removeAlbum(QString artist, QString album); 25 | enum TreeLevel { LevelNone = 0, LevelArtist = 1, LevelAlbum = 2, LevelMusic = 3 }; 26 | 27 | private: 28 | QStringList toColumns(QString string); 29 | void cleanUp(QTreeWidgetItem *parent, int level); 30 | CollectionService *service; 31 | 32 | // Override drag and drop methods 33 | void mouseMoveEvent(QMouseEvent *event); 34 | 35 | 36 | private slots: 37 | CollectionTreeWidgetItem *addMusic(Music *music, unsigned int id = 0); 38 | bool removeMusic(unsigned int id); 39 | void doubleClickAt(QModelIndex); 40 | void showChildrenOf(QModelIndex index); 41 | 42 | signals: 43 | void askToAddItemToPlaylist(QList); 44 | void scanning(); 45 | void listUpdated(); 46 | 47 | 48 | }; 49 | 50 | #endif // COLLECTIONTREEWIDGET_H 51 | -------------------------------------------------------------------------------- /src/mainwindow/playerbar.h: -------------------------------------------------------------------------------- 1 | #ifndef PLAYERBAR_H 2 | #define PLAYERBAR_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "../separator.h" 15 | #include "../iconfactory.h" 16 | #include "../settings/settingsdialog/settingsdialog.h" 17 | 18 | 19 | using namespace Phonon; 20 | 21 | class PlayerBar : public QWidget 22 | { 23 | Q_OBJECT 24 | 25 | public: 26 | PlayerBar(QWidget *parent, Phonon::MediaObject *mediaObject, Phonon::AudioOutput *audioOutput); 27 | 28 | public slots: 29 | void updateSongPosition(); 30 | void updateSongInformation(QMap newSongInformation); 31 | void handleState(Phonon::State newState); 32 | void handlePlayButton(); 33 | void handleWindowStateChange(Qt::WindowStates windowState); 34 | void handleStopButton(); 35 | void exitApplication(); 36 | void openSettings(); 37 | void finish(); 38 | 39 | signals: 40 | void nextButtonClicked(); 41 | void previousButtonClicked(); 42 | void toggleFullScreen(); 43 | 44 | private: 45 | Phonon::MediaObject *mainMediaObject; 46 | Phonon::AudioOutput *audioOutput; 47 | QLabel *currentSongPosition; 48 | QLabel *remainingSongPosition; 49 | QLabel *currentSongInfo; 50 | SeekSlider *songPositionSlider; 51 | 52 | QToolButton *playButton; 53 | QToolButton *stopButton; 54 | QToolButton *exitButton; 55 | QToolButton *nextButton; 56 | QToolButton *previousButton; 57 | QToolButton *prefButton; 58 | QToolButton *fullScreenButton; 59 | 60 | void resetDisplay(); 61 | 62 | }; 63 | 64 | #endif // PLAYERBAR_H 65 | -------------------------------------------------------------------------------- /src/services/lastfmscrobbler.h: -------------------------------------------------------------------------------- 1 | #ifndef LASTFMSCROBBLER_H 2 | #define LASTFMSCROBBLER_H 3 | 4 | #include 5 | #include // To generate the token. 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "../settings/lastfmsettings.h" 15 | 16 | class LastFmScrobbler : QObject 17 | { 18 | Q_OBJECT 19 | 20 | public: 21 | LastFmScrobbler(Phonon::MediaObject *mediaObject); 22 | 23 | signals: 24 | 25 | public slots: 26 | void onTick(qint64 time); 27 | void handleStateChange(Phonon::State, Phonon::State); 28 | void resetSongStatus(); 29 | 30 | // Network related stuff 31 | void readAuthenticationReply(); 32 | void readNowPlayingReply(); 33 | void readSubmissionReply(); 34 | 35 | private: 36 | void handshake(); 37 | void tryToScrobble(); 38 | QString generateToken(QString input, QString timestamp); 39 | Phonon::MediaObject *mediaObject; 40 | int ellapsedTime; 41 | bool canScrobble; 42 | struct SongInfo { 43 | QString artist; 44 | QString album; 45 | QString title; 46 | QString startTimeStamp; 47 | QString duration; 48 | }; 49 | SongInfo currentSong; 50 | QList *songsToScrobble; 51 | 52 | // Network related stuff 53 | enum LastFmState { 54 | LastFmStateNone = 0, 55 | LastFmStateWaitingToken = 1, 56 | LastFmGotToken = 2 57 | }; 58 | LastFmState state; 59 | int timeToScrobble; 60 | qint64 lastTickTime; 61 | QString sessionId; 62 | QString nowPlayingUrl; 63 | QString submissionUrl; 64 | QNetworkAccessManager *netManager; 65 | QNetworkReply *authReply; 66 | QNetworkReply *nowPlayingReply; 67 | QNetworkReply *submissionReply; 68 | 69 | 70 | 71 | }; 72 | 73 | #endif // LASTFMSCROBBLER_H 74 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/context/abstractcontainer.cpp: -------------------------------------------------------------------------------- 1 | #include "abstractcontainer.h" 2 | 3 | AbstractContainer::AbstractContainer(QWidget *parent) : 4 | QFrame(parent) 5 | { 6 | setFrameShape(QFrame::Box); 7 | setFrameShadow(QFrame::Sunken); 8 | 9 | contentView = new QWebView(this); 10 | connect(contentView, SIGNAL(linkClicked(QUrl)), this, SLOT(openLinksInExternalWindow(QUrl))); 11 | contentView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); 12 | contentView->setContextMenuPolicy(Qt::NoContextMenu); 13 | 14 | // Stole font from a QLabel 15 | QLabel *label = new QLabel("I will die soon!"); 16 | QFont font = label->font(); 17 | delete label; 18 | 19 | // TODO: set margins, color, etc. 20 | QStringList headerList; 21 | headerList += ""; 34 | 35 | header = headerList.join(""); 36 | 37 | footer = ""; // no footer yet. 38 | 39 | 40 | // TODO: open links in another window 41 | QVBoxLayout *layout = new QVBoxLayout(this); 42 | layout->addWidget(contentView); 43 | layout->setContentsMargins(0, 0, 0, 0); 44 | 45 | setLayout(layout); 46 | } 47 | 48 | void AbstractContainer::setHtml(QString html) { 49 | html = header + html + footer; 50 | contentView->setHtml(html); 51 | } 52 | 53 | void AbstractContainer::openLinksInExternalWindow(QUrl url) { 54 | QDesktopServices::openUrl(url); 55 | } 56 | -------------------------------------------------------------------------------- /src/settings/settingsdialog/widgets/lastfmsettingswidget.cpp: -------------------------------------------------------------------------------- 1 | #include "lastfmsettingswidget.h" 2 | 3 | LastFmSettingsWidget::LastFmSettingsWidget(QWidget *parent) : QWidget(parent) 4 | { 5 | usernameLabel = new QLabel("Username:", this); 6 | passwordLabel = new QLabel("Password:", this); 7 | usernameTextBox = new QLineEdit(this); 8 | passwordTextBox = new QLineEdit(this); 9 | passwordTextBox->setEchoMode(QLineEdit::Password); 10 | active = new QCheckBox("Active", this); 11 | 12 | QGridLayout *layout = new QGridLayout(this); 13 | layout->addWidget(active, 0, 0, 1, 2); 14 | layout->addWidget(usernameLabel, 1, 0, Qt::AlignRight); 15 | layout->addWidget(usernameTextBox, 1, 1); 16 | layout->addWidget(passwordLabel, 2, 0, Qt::AlignRight); 17 | layout->addWidget(passwordTextBox, 2, 1); 18 | layout->setSizeConstraint(QLayout::SetFixedSize); 19 | 20 | // TODO: make this fill better the window width. 21 | 22 | usernameTextBox->setText(LastFmSettings::username()); 23 | passwordTextBox->setText(LastFmSettings::password()); 24 | active->setChecked(LastFmSettings::isActive()); 25 | 26 | // If Last.fm is not active, disable the textboxes 27 | if (!active->isChecked()) { 28 | usernameTextBox->setDisabled(true); 29 | passwordTextBox->setDisabled(true); 30 | usernameLabel->setDisabled(true); 31 | passwordLabel->setDisabled(true); 32 | } 33 | connect(active, SIGNAL(toggled(bool)), this, SLOT(handleLastFmState(bool))); 34 | 35 | 36 | } 37 | 38 | void LastFmSettingsWidget::applySettings() { 39 | LastFmSettings::setPassword(passwordTextBox->text()); 40 | LastFmSettings::setUsername(usernameTextBox->text()); 41 | LastFmSettings::setActive(active->isChecked()); 42 | } 43 | 44 | void LastFmSettingsWidget::handleLastFmState(bool enabled) { 45 | usernameLabel->setEnabled(enabled); 46 | usernameTextBox->setEnabled(enabled); 47 | passwordLabel->setEnabled(enabled); 48 | passwordTextBox->setEnabled(enabled); 49 | } 50 | -------------------------------------------------------------------------------- /src/music.cpp: -------------------------------------------------------------------------------- 1 | #include "music.h" 2 | #include 3 | 4 | /* 5 | * TODO: 6 | * - Watch file for changes. If it's changed, update playlist. 7 | */ 8 | Music::Music() { } 9 | 10 | Music::Music(QString artist, QString album, QString title, QString path, unsigned int trackNumber) { 11 | this->artist = artist; 12 | this->album = album; 13 | this->title = title; 14 | this->fileUrl = QUrl(path); 15 | this->trackNumber = trackNumber; 16 | } 17 | 18 | Music::Music(QUrl fileUrl) 19 | { 20 | this->fileUrl = fileUrl; 21 | readMetaData(); 22 | } 23 | 24 | void Music::readMetaData() { 25 | TagLib::FileRef taglibFileRef = TagLib::FileRef(fileUrl.toLocalFile().toUtf8()); 26 | 27 | // Verify if some file is valid 28 | if (!taglibFileRef.isNull()) { 29 | trackNumber = taglibFileRef.tag()->track(); 30 | 31 | // Read metadata 32 | artist = QString(taglibFileRef.tag()->artist().toCString()).toAscii().trimmed(); 33 | album = QString(taglibFileRef.tag()->album().toCString()).toAscii().trimmed(); 34 | title = QString(taglibFileRef.tag()->title().toCString()).toAscii().trimmed(); 35 | duration = taglibFileRef.audioProperties()->length(); 36 | 37 | if (artist.isEmpty()) artist = "Undefined"; 38 | if (album.isEmpty()) album = "Undefined"; 39 | if (title.isEmpty()) title = QFileInfo(fileUrl.toLocalFile()).fileName(); 40 | 41 | valid = true; 42 | } 43 | else { 44 | valid = false; 45 | // TODO: throw some exception 46 | } 47 | } 48 | 49 | bool Music::isValid() { 50 | return valid; 51 | } 52 | 53 | QString Music::getAlbum() { 54 | return album; 55 | } 56 | 57 | QString Music::getArtist() { 58 | return artist; 59 | } 60 | 61 | unsigned int Music::getDuration() { 62 | return duration; 63 | } 64 | 65 | QString Music::getTitle() { 66 | return title; 67 | } 68 | 69 | unsigned int Music::getTrackNumber() { 70 | return trackNumber; 71 | } 72 | 73 | QUrl Music::getFileUrl() { 74 | return fileUrl; 75 | } 76 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/context/artistinfowidget.cpp: -------------------------------------------------------------------------------- 1 | #include "artistinfowidget.h" 2 | 3 | // TODO: put a "fetch info" and "reload" buttons. 4 | // TODO: show a message when no information is available. 5 | ArtistInfoWidget::ArtistInfoWidget(QWidget *parent) : 6 | AbstractContainer(parent) 7 | { 8 | context = new LastFmContext(this); 9 | connect(context, SIGNAL(contextUpdated(QMap)), this, SLOT(updateContextInformation(QMap))); 10 | connect(context, SIGNAL(contextError()), this, SLOT(showContextError())); 11 | 12 | resetLabels(); 13 | } 14 | 15 | void ArtistInfoWidget::songInformationUpdated(QMap newContextInformation) { 16 | lastRequest = newContextInformation; 17 | QString html("
" + newContextInformation.value("artist") + "
"); 18 | setHtml(html); 19 | context->getInfo(newContextInformation.value("artist")); 20 | } 21 | 22 | void ArtistInfoWidget::updateContextInformation(QMapnewContextInformation) { 23 | QString artist = newContextInformation.value("artist"); 24 | 25 | if (artist.toLower().trimmed() != Qt::escape(lastRequest.value("artist").toLower().trimmed())) return; 26 | 27 | QString html = "
" + artist + "
"; 28 | if (!newContextInformation.value("picture").isEmpty()) { 29 | html += "

"; 30 | } 31 | html += "

" + newContextInformation.value("summary") + "

"; 32 | html += ""; 33 | setHtml(html); 34 | } 35 | 36 | void ArtistInfoWidget::showContextError() { 37 | setHtml("
Error while trying to download information for " + lastRequest.value("artist") + "!
"); 38 | } 39 | 40 | void ArtistInfoWidget::resetLabels() { 41 | setHtml("
Listen a song and push the Fetch button above to get informations about artists!
"); 42 | } 43 | -------------------------------------------------------------------------------- /src/settings/lastfmsettings.cpp: -------------------------------------------------------------------------------- 1 | #include "lastfmsettings.h" 2 | 3 | void LastFmSettings::setUsername(QString username) { 4 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 5 | settings.beginGroup("lastfm"); 6 | settings.setValue("username", username); 7 | settings.endGroup(); 8 | } 9 | 10 | 11 | void LastFmSettings::setPassword(QString password) { 12 | QByteArray text = password.toUtf8(); 13 | password = QString(text.toBase64()); 14 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 15 | settings.beginGroup("lastfm"); 16 | settings.setValue("password", password); 17 | settings.endGroup(); 18 | } 19 | 20 | void LastFmSettings::setActive(bool active) { 21 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 22 | settings.beginGroup("lastfm"); 23 | settings.setValue("active", active); 24 | settings.endGroup(); 25 | } 26 | 27 | QString LastFmSettings::username() { 28 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 29 | settings.beginGroup("lastfm"); 30 | QString ret; 31 | if (settings.contains("username")) { 32 | ret = settings.value("username").toString(); 33 | } 34 | settings.endGroup(); 35 | return ret; 36 | } 37 | 38 | 39 | QString LastFmSettings::password() { 40 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 41 | settings.beginGroup("lastfm"); 42 | QString ret; 43 | if (settings.contains("password")) { 44 | QByteArray text = settings.value("password").toByteArray(); 45 | text = QByteArray::fromBase64(text); 46 | ret = QString(text); 47 | } 48 | settings.endGroup(); 49 | return ret; 50 | } 51 | 52 | 53 | bool LastFmSettings::isActive() { 54 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 55 | settings.beginGroup("lastfm"); 56 | bool ret = settings.value("active", false).toBool(); 57 | settings.endGroup(); 58 | return ret; 59 | } 60 | -------------------------------------------------------------------------------- /src/mainwindow/playlist/playlistwidget.h: -------------------------------------------------------------------------------- 1 | #ifndef PLAYLISTWIDGET_H 2 | #define PLAYLISTWIDGET_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "playlistitem.h" 13 | #include "../../music.h" 14 | 15 | /* 16 | * TODO: make a PlaylistEngine, which will make the work of handle the current songs. 17 | * PlaylistWidget will only make the work of handle the visualization. 18 | * 19 | */ 20 | class PlaylistWidget : public QTreeWidget 21 | { 22 | Q_OBJECT 23 | 24 | private slots: 25 | void playSong(QTreeWidgetItem *doubleClickedItem); 26 | void handleStateChange(Phonon::State newState); 27 | void enqueueNextSong(); 28 | void removeBold(); 29 | void fileChanged(); 30 | void playNextSong(); 31 | void playPreviousSong(); 32 | void dndActionChanged(Qt::DropAction newAction); 33 | void insertValidItem(PlaylistItem *newItem); 34 | void deleteInvalidItem(PlaylistItem *invalidItem); 35 | void addSong(PlaylistItem *newItem, int index = -1); 36 | void addSong(QUrl url, int index = -1); 37 | void addSong(QList urlList); 38 | 39 | // Keyboard / menu events 40 | void selectAll(); 41 | void removeSelectedItems(); 42 | 43 | signals: 44 | void songInformationUpdated(QMap newSongInformation); 45 | 46 | 47 | public: 48 | PlaylistWidget(QWidget *parent, Phonon::MediaObject *mediaObject); 49 | 50 | 51 | private: 52 | PlaylistItem *currentSong; 53 | PlaylistItem *nextSong; 54 | Phonon::MediaObject *mainMediaObject; 55 | QDrag *drag; 56 | Qt::DropAction dndAction; 57 | void emitSongInformationUpdated(); 58 | void addFolder(QUrl url, int &index); 59 | 60 | // Menu events 61 | void contextMenuEvent(QContextMenuEvent *event); 62 | 63 | // Drag and drop events 64 | void dragEnterEvent(QDragEnterEvent *event); 65 | void dropEvent(QDropEvent *event); 66 | Qt::DropActions supportedDropActions() const; 67 | void dragMoveEvent(QDragMoveEvent *event); 68 | void mouseMoveEvent(QMouseEvent *event); 69 | 70 | // Keyboard events 71 | void keyPressEvent(QKeyEvent *event); 72 | 73 | 74 | }; 75 | 76 | #endif // PLAYLISTWIDGET_H 77 | -------------------------------------------------------------------------------- /src/settings/settingsdialog/widgets/foldersettingswidget.cpp: -------------------------------------------------------------------------------- 1 | #include "foldersettingswidget.h" 2 | 3 | FolderSettingsWidget::FolderSettingsWidget(QWidget *parent) : QWidget(parent) 4 | { 5 | // Create buttons and a widget to enclose them 6 | QVBoxLayout *vbox = new QVBoxLayout(); 7 | QWidget *buttonsWidget = new QWidget(); 8 | QPushButton *addButton = new QPushButton("Add folder"); 9 | QPushButton *removeButton = new QPushButton("Remove folder"); 10 | vbox->addWidget(addButton); 11 | vbox->addWidget(removeButton); 12 | buttonsWidget->setLayout(vbox); 13 | 14 | // Horizontal box to put the folders list and buttons 15 | QHBoxLayout *hbox = new QHBoxLayout(); 16 | folderList = new QListWidget(); 17 | folderList->setSelectionMode(QListWidget::ExtendedSelection); 18 | hbox->addWidget(folderList); 19 | hbox->addWidget(buttonsWidget); 20 | setLayout(hbox); 21 | 22 | // Update data 23 | originalFoldersList = ApplicationSettings::collectionFolderList(); 24 | foreach (QString folder, originalFoldersList) { 25 | folderList->addItem(folder); 26 | } 27 | 28 | connect(addButton, SIGNAL(clicked()), this, SLOT(addFolder())); 29 | connect(removeButton, SIGNAL(clicked()), this, SLOT(removeFolder())); 30 | } 31 | 32 | void FolderSettingsWidget::addFolder() { 33 | QString dir = QFileDialog::getExistingDirectory(this, tr("Add folder"), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); 34 | QDir qdir; 35 | if (!dir.isEmpty() && qdir.exists(dir)) { 36 | folderList->addItem(dir); 37 | folderList->sortItems(); 38 | } 39 | } 40 | 41 | void FolderSettingsWidget::removeFolder() { 42 | if (folderList->selectedItems().count() == 0) return; 43 | foreach(QListWidgetItem *item, folderList->selectedItems()) { 44 | delete item; 45 | } 46 | } 47 | 48 | void FolderSettingsWidget::applySettings() { 49 | /* 50 | * 51 | * Settings for folders 52 | * 53 | */ 54 | QStringList folders; 55 | for(int i = 0; i < folderList->count(); i++) { 56 | QListWidgetItem *item = folderList->item(i); 57 | folders.append(item->text()); 58 | } 59 | ApplicationSettings::setCollectionFolders(folders); 60 | 61 | // If folders have changed, rebuild the collection database 62 | if (folders != originalFoldersList) { 63 | // TODO: rebuild collection 64 | originalFoldersList = folders; 65 | } 66 | 67 | 68 | /* 69 | * 70 | * Settings for folders - end 71 | * 72 | */ 73 | } 74 | -------------------------------------------------------------------------------- /src/services/lyricsdownloader.cpp: -------------------------------------------------------------------------------- 1 | #include "lyricsdownloader.h" 2 | 3 | LyricsDownloader::LyricsDownloader(QObject *parent) : 4 | QObject(parent) 5 | { 6 | netManager = new QNetworkAccessManager(this); 7 | contextReply = NULL; 8 | } 9 | 10 | void LyricsDownloader::getLyrics(QString artist, QString song) { 11 | QUrl url("http://lyrics.wikia.com/api.php"); 12 | url.addQueryItem("func", "getSong"); 13 | url.addQueryItem("artist", artist.toUtf8()); 14 | url.addQueryItem("song", song.toUtf8()); 15 | url.addQueryItem("fmt", "xml"); 16 | 17 | QNetworkRequest netRequest; 18 | netRequest.setUrl(url); 19 | 20 | qDebug("Requesting lyrics..."); 21 | contextReply = netManager->get(netRequest); 22 | connect(contextReply, SIGNAL(finished()), this, SLOT(readContextReply())); 23 | connect(contextReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SIGNAL(lyricsError())); // TODO: specify which error was given 24 | } 25 | 26 | void LyricsDownloader::readContextReply() { 27 | QString replyString = QString::fromUtf8(contextReply->readAll()); 28 | qDebug("Answer received: " + replyString.toUtf8()); 29 | 30 | if (replyString.trimmed().isEmpty()) { 31 | emit lyricsError(); 32 | qDebug("Empty answer!"); 33 | return; // Avoid empty parsing of XML 34 | } 35 | 36 | QString status; 37 | QXmlQuery query; 38 | query.setFocus(replyString); 39 | query.setQuery("LyricsResult/lyrics/text()"); 40 | query.evaluateTo(&status); 41 | status = status.trimmed(); 42 | 43 | /* 44 | * If we got the data successfully, let's read it. 45 | * I hate XML. 46 | */ 47 | if (status.toLower() != "not found") { 48 | QString url; 49 | query.setQuery("LyricsResult/url/text()"); 50 | query.evaluateTo(&url); 51 | url = url.trimmed(); 52 | 53 | // Try to fetch lyrics 54 | fetchLyrics(url); 55 | } 56 | else { 57 | emit lyricsError(); 58 | } 59 | } 60 | 61 | void LyricsDownloader::fetchLyrics(QUrl url) { 62 | qDebug("Asking to fetch lyrics"); 63 | page = new QWebPage(this); 64 | page->mainFrame()->load(url); 65 | connect(page, SIGNAL(loadFinished(bool)), this, SLOT(render())); 66 | } 67 | 68 | 69 | // FIXME: why is this emitting the signal twice? 70 | void LyricsDownloader::render() { 71 | QWebElement document = page->mainFrame()->documentElement(); 72 | // TODO: remove mobile ringtone ad 73 | QWebElement lyricsDiv = document.findFirst("div.lyricbox"); 74 | delete page; 75 | emit lyricsReady(lyricsDiv); 76 | } 77 | -------------------------------------------------------------------------------- /src/services/lastfmcontext.cpp: -------------------------------------------------------------------------------- 1 | #include "lastfmcontext.h" 2 | 3 | LastFmContext::LastFmContext(QObject *parent) : 4 | QObject(parent) 5 | { 6 | netManager = new QNetworkAccessManager(this); 7 | } 8 | 9 | void LastFmContext::getInfo(QString artist) { 10 | QUrl url("http://ws.audioscrobbler.com/2.0/"); 11 | url.addQueryItem("method", "artist.getinfo"); 12 | url.addQueryItem("artist", artist.toUtf8()); 13 | url.addQueryItem("api_key", "ee988217b1695e36e1447dc0de443ac3"); 14 | 15 | QNetworkRequest netRequest; 16 | netRequest.setUrl(url); 17 | contextReply = netManager->get(netRequest); 18 | connect(contextReply, SIGNAL(finished()), this, SLOT(readContextReply())); 19 | connect(contextReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SIGNAL(contextError())); // TODO: specify which error was given 20 | } 21 | 22 | void LastFmContext::readContextReply() { 23 | QString replyString = QString::fromUtf8(contextReply->readAll()); 24 | 25 | if (replyString.isEmpty()) { 26 | return; // Avoid empty parsing of XML 27 | } 28 | 29 | // Parametres 30 | QString status; 31 | QString artistName; 32 | QString artistPicture; 33 | QString artistSummary; 34 | QString artistProfile; 35 | 36 | QXmlQuery query; 37 | query.setFocus(replyString); 38 | query.setQuery("lfm[@status = 'ok']/count(artist)"); 39 | query.evaluateTo(&status); 40 | status = status.trimmed(); 41 | 42 | /* 43 | * If we got the data successfully, let's read it. 44 | * I hate XML. 45 | */ 46 | if (status == "1") { 47 | query.setQuery("lfm/artist/name/text()"); 48 | query.evaluateTo(&artistName); 49 | artistName = artistName.trimmed(); 50 | 51 | query.setQuery("lfm/artist/url/text()"); 52 | query.evaluateTo(&artistProfile); 53 | artistProfile = artistProfile.trimmed(); 54 | 55 | query.setQuery("lfm/artist/image[@size=\"extralarge\"]/text()"); 56 | query.evaluateTo(&artistPicture); 57 | artistPicture = artistPicture.trimmed(); 58 | 59 | // Summary has HTML entities that must be un-replaced. 60 | query.setQuery("lfm/artist/bio/summary/text()"); 61 | query.evaluateTo(&artistSummary); 62 | artistSummary = artistSummary.trimmed().replace("<","<").replace(">",">"); 63 | 64 | // Store the context data into... contextData. Nice! 65 | contextData.clear(); 66 | contextData.insert("artist", artistName); 67 | contextData.insert("picture", artistPicture); 68 | contextData.insert("summary", artistSummary); 69 | contextData.insert("profile", artistProfile); 70 | 71 | // Emit the signal 72 | emit contextUpdated(contextData); 73 | } 74 | else { 75 | qDebug("LastFmContext: FAIL!"); 76 | emit contextError(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Player.pro: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------- 2 | # Project created by QtCreator 2010-05-03T16:01:44 3 | # ------------------------------------------------- 4 | QT += sql \ 5 | phonon \ 6 | xml \ 7 | xmlpatterns \ 8 | network \ 9 | webkit 10 | TARGET = Player 11 | TEMPLATE = app 12 | SOURCES += src/mainwindow/mainnotebook/collection/collectiontreewidget.cpp \ 13 | src/mainwindow/mainnotebook/filesystem/filesystemwidget.cpp \ 14 | src/mainwindow/mainnotebook/mainnotebook.cpp \ 15 | src/mainwindow/playlist/playlistwidget.cpp \ 16 | src/mainwindow/playlist/playlistitem.cpp \ 17 | src/mainwindow/playerbar.cpp \ 18 | src/mainwindow/mainwindow.cpp \ 19 | src/main.cpp \ 20 | src/settings/applicationsettings.cpp \ 21 | src/settings/settingsdialog/widgets/foldersettingswidget.cpp \ 22 | src/settings/settingsdialog/settingsdialog.cpp \ 23 | src/settings/settingsdialog/widgets/lastfmsettingswidget.cpp \ 24 | src/settings/lastfmsettings.cpp \ 25 | src/services/lastfmscrobbler.cpp \ 26 | src/services/lastfmcontext.cpp \ 27 | src/iconfactory.cpp \ 28 | src/separator.cpp \ 29 | src/mainwindow/mainnotebook/context/lyricswidget.cpp \ 30 | src/mainwindow/mainnotebook/context/artistinfowidget.cpp \ 31 | src/mainwindow/mainnotebook/context/contextwidget.cpp \ 32 | src/services/lyricsdownloader.cpp \ 33 | src/mainwindow/mainnotebook/context/abstractcontainer.cpp \ 34 | src/settings/contextsettings.cpp \ 35 | src/settings/settingsdialog/widgets/contextsettingswidget.cpp \ 36 | src/music.cpp \ 37 | src/collection/collectiondatabase.cpp \ 38 | src/collection/collectionservice.cpp \ 39 | src/mainwindow/mainnotebook/collection/collectiontreewidgetitem.cpp 40 | HEADERS += src/mainwindow/mainnotebook/collection/collectiontreewidget.h \ 41 | src/mainwindow/mainnotebook/filesystem/filesystemwidget.h \ 42 | src/mainwindow/mainnotebook/mainnotebook.h \ 43 | src/mainwindow/playlist/playlistwidget.h \ 44 | src/mainwindow/playlist/playlistitem.h \ 45 | src/mainwindow/playerbar.h \ 46 | src/mainwindow/mainwindow.h \ 47 | src/settings/applicationsettings.h \ 48 | src/settings/settingsdialog/widgets/foldersettingswidget.h \ 49 | src/settings/settingsdialog/settingsdialog.h \ 50 | src/settings/settingsdialog/widgets/lastfmsettingswidget.h \ 51 | src/settings/lastfmsettings.h \ 52 | src/services/lastfmscrobbler.h \ 53 | src/services/lastfmcontext.h \ 54 | src/iconfactory.h \ 55 | src/separator.h \ 56 | src/mainwindow/mainnotebook/context/lyricswidget.h \ 57 | src/mainwindow/mainnotebook/context/artistinfowidget.h \ 58 | src/mainwindow/mainnotebook/context/contextwidget.h \ 59 | src/services/lyricsdownloader.h \ 60 | src/mainwindow/mainnotebook/context/abstractcontainer.h \ 61 | src/services/collectionservice.h \ 62 | src/settings/contextsettings.h \ 63 | src/settings/settingsdialog/widgets/contextsettingswidget.h \ 64 | src/music.h \ 65 | src/collection/collectiondatabase.h \ 66 | src/collection/collectionservice.h \ 67 | src/mainwindow/mainnotebook/collection/collectiontreewidgetitem.h 68 | RESOURCES = extra_images.qrc 69 | win32|macx:RESOURCES += icons.qrc 70 | CONFIG += link_pkgconfig 71 | PKGCONFIG += taglib 72 | OTHER_FILES += README 73 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/context/contextwidget.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "contextwidget.h" 8 | #include "../../../settings/contextsettings.h" 9 | #include "../../../separator.h" 10 | #include "../../../iconfactory.h" 11 | 12 | ContextWidget::ContextWidget(QWidget *parent) : QWidget(parent) 13 | { 14 | // Create widgets 15 | artistInfoWidget = new ArtistInfoWidget(this); 16 | lyricsWidget = new LyricsWidget(this); 17 | 18 | // Create a QButtonGroup where user will select artist, lyrics, etc. 19 | buttonGroup = new QButtonGroup(this); 20 | buttonGroup->setExclusive(true); 21 | 22 | // The amazing buttons 23 | artistButton = new QPushButton("Last.fm summary", this); 24 | lyricsButton = new QPushButton("Lyrics", this); 25 | 26 | artistButton->setIconSize(QSize(24, 24)); 27 | artistButton->setCheckable(true); 28 | artistButton->setChecked(true); 29 | artistButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); 30 | 31 | lyricsButton->setCheckable(true); 32 | lyricsButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); 33 | 34 | buttonGroup->addButton(artistButton, 0); 35 | buttonGroup->addButton(lyricsButton, 1); 36 | 37 | // Stacked widget where we'll put artist info, lyrics, etc. 38 | QStackedWidget *contextContainer = new QStackedWidget(this); 39 | contextContainer->addWidget(artistInfoWidget); 40 | contextContainer->addWidget(lyricsWidget); 41 | 42 | // Connect buttons to container 43 | connect(buttonGroup, SIGNAL(buttonClicked(int)), contextContainer, SLOT(setCurrentIndex(int))); 44 | 45 | // Fetch button 46 | QToolButton *fetchButton = new QToolButton(this); 47 | fetchButton->setIcon(IconFactory::fromTheme("go-down")); 48 | fetchButton->setIconSize(QSize(24, 24)); 49 | connect(fetchButton, SIGNAL(clicked()), this, SLOT(fetchButtonPressed())); 50 | 51 | // The button group needs a layout 52 | QHBoxLayout *buttonsLayout = new QHBoxLayout(); 53 | buttonsLayout->addWidget(artistButton); 54 | buttonsLayout->addWidget(lyricsButton); 55 | buttonsLayout->addWidget(Separator::verticalSeparator()); 56 | buttonsLayout->addWidget(fetchButton); 57 | buttonsLayout->setSpacing(5); 58 | buttonsLayout->setMargin(0); 59 | QWidget *buttonsWidget = new QWidget(this); 60 | buttonsWidget->setLayout(buttonsLayout); 61 | 62 | // Create layout 63 | QVBoxLayout *vlayout = new QVBoxLayout(); 64 | vlayout->addWidget(buttonsWidget); 65 | vlayout->addWidget(contextContainer); 66 | this->setLayout(vlayout); 67 | 68 | } 69 | 70 | void ContextWidget::songInformationUpdated(QMap newContextInformation) { 71 | currentContext = newContextInformation; 72 | 73 | artistInfoWidget->resetLabels(); 74 | lyricsWidget->resetLabels(); 75 | 76 | if (ContextSettings::isFetchArtistInfoActive()) artistInfoWidget->songInformationUpdated(currentContext); 77 | if (ContextSettings::isFetchLyricsActive()) lyricsWidget->songInformationUpdated(currentContext); 78 | } 79 | 80 | void ContextWidget::fetchButtonPressed() { 81 | if (artistButton->isChecked()) artistInfoWidget->songInformationUpdated(currentContext); 82 | else if (lyricsButton->isChecked()) lyricsWidget->songInformationUpdated(currentContext); 83 | } 84 | -------------------------------------------------------------------------------- /src/collection/collectionservice.cpp: -------------------------------------------------------------------------------- 1 | #include "collectionservice.h" 2 | 3 | CollectionService::CollectionService(QObject *parent) : 4 | QThread(parent) 5 | { 6 | watcher = new QFileSystemWatcher(this); 7 | collectionDb = new CollectionDatabase(this); 8 | 9 | // Set watcher paths to paths in settings file 10 | refresh(); 11 | 12 | connect(watcher, SIGNAL(fileChanged(QString)), this, SLOT(fileChanged(QString))); 13 | connect(watcher, SIGNAL(directoryChanged(QString)), this, SLOT(dirChanged(QString))); 14 | 15 | } 16 | 17 | void CollectionService::run() { 18 | verifyFiles(); 19 | scan(); 20 | } 21 | 22 | QSqlTableModel *CollectionService::collectionModel() { 23 | return collectionDb->collectionModel(); 24 | } 25 | 26 | QSqlTableModel *CollectionService::artistModel() { 27 | return collectionDb->artistModel(); 28 | } 29 | 30 | QSqlTableModel *CollectionService::albumModel() { 31 | return collectionDb->albumModel(); 32 | } 33 | 34 | QSqlTableModel *CollectionService::musicModel() { 35 | return collectionDb->musicModel(); 36 | } 37 | 38 | void CollectionService::fileChanged(QString path) { 39 | qDebug("FILE CHANGED " + path.toUtf8()); 40 | } 41 | 42 | void CollectionService::dirChanged(QString path) { 43 | qDebug("DIR CHANGED " + path.toUtf8()); 44 | } 45 | 46 | void CollectionService::refresh() { 47 | if (!watcher->directories().isEmpty()) { 48 | watcher->removePaths(watcher->directories()); 49 | } 50 | 51 | // TODO: verify if every path is valid 52 | QStringList directories = ApplicationSettings::collectionFolderList(); 53 | if (!directories.isEmpty()) { 54 | watcher->addPaths(ApplicationSettings::collectionFolderList()); 55 | } 56 | } 57 | 58 | /* 59 | * Verify if all files in database exist. 60 | */ 61 | void CollectionService::verifyFiles() { 62 | QSqlTableModel *model = musicModel(); 63 | model->select(); 64 | while (model->canFetchMore()) model->fetchMore(); 65 | int total = model->rowCount(); 66 | 67 | for (int i = 0; i < total; i++) { 68 | QString path = model->record(i).value(model->fieldIndex("path")).toString(); 69 | if (!QFileInfo(path).exists()) { 70 | collectionDb->removeMusic(path); 71 | emit songRemoved(model->record(i).value(model->fieldIndex("id")).toUInt()); 72 | } 73 | } 74 | 75 | delete model; 76 | } 77 | 78 | /* 79 | * Scan collection folders in order to assert that 80 | * all files in folders are in collection. 81 | */ 82 | void CollectionService::scan() { 83 | emit scanning(); 84 | qDebug("Beginning scan"); 85 | QStringList directories = ApplicationSettings::collectionFolderList(); 86 | foreach (QString path, directories) { 87 | scanRecursive(path); 88 | } 89 | emit listUpdated(); 90 | qDebug("Scan ended"); 91 | } 92 | 93 | void CollectionService::scanRecursive(QString path) { 94 | // You shouldn't call this to add files 95 | if (QFileInfo(path).isFile()) return; 96 | 97 | QDir directory(path); 98 | foreach (QString fileEntry, directory.entryList(directory.AllEntries | directory.NoDotAndDotDot, directory.DirsFirst | directory.Name)) { 99 | // Change file entry to a full path 100 | fileEntry = directory.absolutePath() + directory.separator() + fileEntry; 101 | 102 | if (QFileInfo(fileEntry).isDir()) { 103 | scanRecursive(fileEntry); 104 | } 105 | else if (QFileInfo(fileEntry).isFile()) { 106 | Music *music = new Music(QUrl(fileEntry)); 107 | if (collectionDb->addOrUpdateMusic(music)) { 108 | emit songAdded(music); 109 | } 110 | } 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /src/settings/settingsdialog/settingsdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "settingsdialog.h" 2 | 3 | SettingsDialog::SettingsDialog(QWidget *parent) : QWidget(parent) 4 | { 5 | setWindowTitle("Settings"); 6 | setWindowFlags(Qt::Dialog); 7 | setWindowModality(Qt::ApplicationModal); 8 | 9 | // TODO: add icons. Icons are friends! 10 | 11 | // List to show the selected configuration 12 | QListWidget *listWidget = new QListWidget(this); 13 | listWidget->setViewMode(QListView::IconMode); 14 | listWidget->setIconSize(QSize(48, 48)); 15 | QLabel *label = new QLabel(this); 16 | listWidget->setMaximumHeight(96 + 2*label->minimumHeight()); 17 | listWidget->setSpacing(12); 18 | listWidget->setMovement(QListView::Static); 19 | 20 | 21 | // Create tab bar 22 | QStackedWidget *settingsStack = new QStackedWidget(this); 23 | 24 | // Settings widgets 25 | QListWidgetItem *folderSettingsListItem = new QListWidgetItem(IconFactory::fromTheme("system-file-manager"), "Collection folders"); 26 | folderSettingsWidget = new FolderSettingsWidget(this); 27 | QListWidgetItem *lastFmSettingsListItem = new QListWidgetItem(QIcon(":/icons/lastfm.png"), "Last.fm"); 28 | lastFmSettingsWidget = new LastFmSettingsWidget(this); 29 | QListWidgetItem *contextSettingsListItem = new QListWidgetItem(IconFactory::fromTheme("emblem-web"), "Context"); 30 | contextSettingsWidget = new ContextSettingsWidget(this); 31 | 32 | /* 33 | * Add the widgets above to stacked widget and 34 | * make a reference for them in the list 35 | */ 36 | listWidget->addItem(folderSettingsListItem); 37 | settingsStack->addWidget(folderSettingsWidget); 38 | listWidget->addItem(lastFmSettingsListItem); 39 | settingsStack->addWidget(lastFmSettingsWidget); 40 | listWidget->addItem(contextSettingsListItem); 41 | settingsStack->addWidget(contextSettingsWidget); 42 | 43 | // Link listwidget and settings stack 44 | connect(listWidget, SIGNAL(currentRowChanged(int)), settingsStack, SLOT(setCurrentIndex(int))); 45 | 46 | // Create button box 47 | buttonBox = new QDialogButtonBox(Qt::Horizontal, this); 48 | buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply); 49 | 50 | // Other vertical box to put the widgets created before. 51 | QVBoxLayout *mainVBox = new QVBoxLayout(); 52 | mainVBox->addWidget(listWidget, 0); 53 | mainVBox->addWidget(Separator::horizontalSeparator(this)); 54 | mainVBox->addWidget(settingsStack); 55 | mainVBox->addWidget(Separator::horizontalSeparator(this)); 56 | mainVBox->addWidget(buttonBox); 57 | setLayout(mainVBox); 58 | 59 | // Connect buttons to their slots 60 | connect(buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(handleAbstractButton(QAbstractButton*))); 61 | 62 | // Show window! 63 | show(); 64 | 65 | // Center window 66 | int x = (parent->width() - width())/2; 67 | int y = (parent->height() - height())/2; 68 | move(x, y); 69 | 70 | // Set this to remove maximize buttons! 71 | setFixedSize(width(), height()); 72 | } 73 | 74 | 75 | 76 | void SettingsDialog::applySettings() { 77 | folderSettingsWidget->applySettings(); 78 | lastFmSettingsWidget->applySettings(); 79 | contextSettingsWidget->applySettings(); 80 | } 81 | 82 | void SettingsDialog::handleAbstractButton(QAbstractButton *button) { 83 | qDebug("handleAbstractButton()"); 84 | switch (buttonBox->buttonRole(button)) { 85 | case QDialogButtonBox::ApplyRole: 86 | applySettings(); 87 | break; 88 | case QDialogButtonBox::AcceptRole: 89 | applySettings(); 90 | close(); 91 | break; 92 | case QDialogButtonBox::RejectRole: 93 | close(); 94 | break; 95 | default: 96 | // Does nothing 97 | break; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/filesystem/filesystemwidget.cpp: -------------------------------------------------------------------------------- 1 | #include "filesystemwidget.h" 2 | 3 | FilesystemWidget::FilesystemWidget(QWidget *parent) 4 | { 5 | setParent(parent); 6 | 7 | // Create the toolbar 8 | QToolBar *fsToolbar = new QToolBar(); 9 | fsToolbar->setMovable(false); 10 | 11 | goUpAction = new QAction(IconFactory::fromTheme("go-up"), tr("Go up"), this); 12 | connect(goUpAction, SIGNAL(triggered()), this, SLOT(goUp())); 13 | fsToolbar->addAction(goUpAction); 14 | 15 | goHomeAction = new QAction(IconFactory::fromTheme("go-home"), tr("Go to the home folder"), this); 16 | connect(goHomeAction, SIGNAL(triggered()), this, SLOT(goHome())); 17 | fsToolbar->addAction(goHomeAction); 18 | 19 | // TODO: use placeholderText in Qt 4.7. 20 | filterEdit = new QLineEdit(); 21 | QLabel* filterLabel = new QLabel(tr("Filter:")); 22 | filterLabel->setContentsMargins(5, 0, 5, 0); 23 | fsToolbar->addSeparator(); 24 | fsToolbar->addWidget(filterLabel); 25 | fsToolbar->addWidget(filterEdit); 26 | connect(filterEdit, SIGNAL(textChanged(QString)), this, SLOT(setNameFilter(QString))); 27 | 28 | // Create the filesystem view 29 | fsWidgetModel = new QFileSystemModel(); 30 | fsWidgetModel->setNameFilterDisables(false); 31 | fsWidgetModel->setFilter(QDir::AllDirs|QDir::Files|QDir::NoDotAndDotDot); 32 | fsListView = new QListView(); 33 | fsListView->setSelectionMode(QAbstractItemView::ExtendedSelection); 34 | fsListView->setDragEnabled(true); 35 | fsListView->setModel(fsWidgetModel); 36 | 37 | // We shall use this to filter available file extensions from Phonon 38 | //fsWidgetModel->setFilter(getPhononExtensions()); 39 | 40 | connect(fsWidgetModel, SIGNAL(rootPathChanged(QString)), this, SLOT(pathChanged())); 41 | connect(fsListView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(doubleClickAt(QModelIndex))); 42 | 43 | // Create a new horizontal box 44 | QVBoxLayout *vlayout = new QVBoxLayout(); 45 | vlayout->addWidget(fsToolbar); 46 | vlayout->addWidget(fsListView); 47 | goHome(); 48 | 49 | this->setLayout(vlayout); 50 | } 51 | 52 | 53 | void FilesystemWidget::goUp() { 54 | dir.cdUp(); 55 | updateWidget(); 56 | } 57 | 58 | 59 | void FilesystemWidget::setNameFilter() { 60 | setNameFilter(filterEdit->text()); 61 | } 62 | 63 | void FilesystemWidget::setNameFilter(QString filter) { 64 | QStringList filterList; 65 | if (!filter.isEmpty()) { 66 | filter = "*" + filter + "*"; 67 | filterList = QStringList(filter); 68 | } 69 | else { 70 | filterList = QStringList(); 71 | } 72 | fsWidgetModel->setNameFilters(filterList); 73 | } 74 | 75 | 76 | void FilesystemWidget::goHome() { 77 | dir = QDir::homePath(); 78 | updateWidget(); 79 | } 80 | 81 | void FilesystemWidget::pathChanged() { 82 | // Enable/disable go up button 83 | if (dir.absolutePath() == QDir::rootPath()) { 84 | goUpAction->setDisabled(true); 85 | // If we are on a Mac, we should allow hidden files - GH-40 86 | // fsWidgetModel->setFilter(QDir::AllDirs|QDir::Files|QDir::NoDotAndDotDot|QDir::Hidden); 87 | } 88 | else { goUpAction->setEnabled(true); } 89 | updateWidget(); 90 | } 91 | 92 | void FilesystemWidget::doubleClickAt(QModelIndex modelIndex) { 93 | // It it's a dir, we must change path 94 | if (fsWidgetModel->isDir(modelIndex)) { 95 | dir = QDir(fsWidgetModel->filePath(modelIndex)); 96 | updateWidget(); 97 | } 98 | 99 | // If it's a file, we must add it to playlist 100 | else { 101 | /* 102 | * TODO: emit signal using URL, not path. 103 | * Let PlaylistWidget handle this. 104 | */ 105 | PlaylistItem *newItem = new PlaylistItem(fsWidgetModel->filePath(modelIndex)); 106 | emit askToAddItemToPlaylist(newItem); 107 | } 108 | } 109 | 110 | 111 | void FilesystemWidget::updateWidget() { 112 | fsWidgetModel->setRootPath(dir.absolutePath()); 113 | fsListView->setRootIndex(fsWidgetModel->index(dir.absolutePath())); 114 | setNameFilter(); 115 | } 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/mainnotebook.cpp: -------------------------------------------------------------------------------- 1 | #include "mainnotebook.h" 2 | 3 | /// @brief Construtor 4 | /// 5 | /// @param parent Parent widget 6 | MainNotebook::MainNotebook(QWidget *parent, PlaylistWidget *playlistWidget) 7 | { 8 | setParent(parent); 9 | setTabPosition(QTabWidget::West); 10 | 11 | // Set tabs movable 12 | setMovable(true); 13 | QTabBar *tabbar = tabBar(); 14 | connect(tabbar, SIGNAL(tabMoved(int,int)), this, SLOT(saveTabOrder())); 15 | 16 | // Widget of the Collection tab 17 | collectionWidget = new CollectionTreeWidget(); 18 | connect(collectionWidget, SIGNAL(askToAddItemToPlaylist(QList)), playlistWidget, SLOT(addSong(QList))); 19 | connect(collectionWidget, SIGNAL(scanning()), this, SLOT(showCollectionProgress())); 20 | connect(collectionWidget, SIGNAL(listUpdated()), this, SLOT(hideCollectionProgress())); 21 | collectionContainer = new QWidget(this); 22 | QVBoxLayout *collectionLayout = new QVBoxLayout(collectionContainer); 23 | 24 | progressContainer = new QWidget(collectionContainer); 25 | scanProgress = new QProgressBar(collectionContainer); 26 | scanLabel = new QLabel("Scanning collection...", collectionContainer); 27 | QHBoxLayout *progressHLayout = new QHBoxLayout(progressContainer); 28 | progressHLayout->addWidget(scanLabel); 29 | progressHLayout->addWidget(scanProgress); 30 | progressContainer->setLayout(progressHLayout); 31 | progressContainer->hide(); 32 | 33 | collectionLayout->addWidget(collectionWidget); 34 | collectionLayout->addWidget(progressContainer); 35 | collectionContainer->setLayout(collectionLayout); 36 | 37 | // Widget of the Files tab 38 | FilesystemWidget *filesystemWidget = new FilesystemWidget(this); 39 | connect(filesystemWidget, SIGNAL(askToAddItemToPlaylist(PlaylistItem*)), playlistWidget, SLOT(addSong(PlaylistItem*))); 40 | filesystemContainer = new QWidget(this); 41 | QVBoxLayout *filesystemLayout = new QVBoxLayout(filesystemContainer); 42 | filesystemLayout->setContentsMargins(5, 5, 5, 5); 43 | filesystemLayout->addWidget(filesystemWidget); 44 | 45 | // Widget of the Context tab 46 | ContextWidget *contextWidget = new ContextWidget(this); 47 | connect(playlistWidget, SIGNAL(songInformationUpdated(QMap)), contextWidget, SLOT(songInformationUpdated(QMap))); 48 | contextContainer = new QWidget(this); 49 | QVBoxLayout *contextLayout = new QVBoxLayout(contextContainer); 50 | contextLayout->setContentsMargins(5, 5, 5, 5); 51 | contextLayout->addWidget(contextWidget); 52 | 53 | // Playlists widget 54 | testLabel3 = new QLabel(tr("Test label 3")); 55 | 56 | // Set positions 57 | collectionPosition = (ApplicationSettings::getTabOrder("collection") > -1) ? ApplicationSettings::getTabOrder("collection") : 0; 58 | filesystemPosition = (ApplicationSettings::getTabOrder("filesystem") > -1) ? ApplicationSettings::getTabOrder("filesystem") : 1; 59 | contextPosition = (ApplicationSettings::getTabOrder("context") > -1) ? ApplicationSettings::getTabOrder("context") : 2; 60 | playlistsPosition = (ApplicationSettings::getTabOrder("playlists") > -1) ? ApplicationSettings::getTabOrder("playlists") : 3; 61 | 62 | 63 | // Insert tabs 64 | for (int i = 0; i < 4; i++) { 65 | if (i == collectionPosition) { addTab(collectionContainer, IconFactory::fromTheme("audio-x-generic"), tr("Collection")); } 66 | else if (i == filesystemPosition) { addTab(filesystemContainer, IconFactory::fromTheme("system-file-manager"), tr("Files")); } 67 | else if (i == contextPosition) { addTab(contextContainer, IconFactory::fromTheme("emblem-web"), tr("Context")); } 68 | else if (i == playlistsPosition) { addTab(testLabel3, IconFactory::fromTheme("text-x-generic"), tr("Playlists")); } 69 | } 70 | 71 | show(); 72 | } 73 | 74 | void MainNotebook::saveTabOrder() { 75 | 76 | for (int i = 0; i < count(); i++) { 77 | if (widget(i) == collectionContainer) collectionPosition = i; 78 | else if (widget(i) == filesystemContainer) filesystemPosition = i; 79 | else if (widget(i) == contextContainer) contextPosition = i; 80 | else if (widget(i) == testLabel3) playlistsPosition = i; 81 | } 82 | 83 | ApplicationSettings::setTabOrder("collection", collectionPosition); 84 | ApplicationSettings::setTabOrder("filesystem", filesystemPosition); 85 | ApplicationSettings::setTabOrder("context", contextPosition); 86 | ApplicationSettings::setTabOrder("playlists", playlistsPosition); 87 | 88 | } 89 | 90 | void MainNotebook::showCollectionProgress() { 91 | // Set minimum and maximum to 0 makes the progress bar go left and right forever 92 | scanProgress->setMinimum(0); 93 | scanProgress->setMaximum(0); 94 | progressContainer->show(); 95 | } 96 | 97 | void MainNotebook::hideCollectionProgress() { 98 | scanProgress->setMinimum(0); 99 | scanProgress->setMaximum(100); 100 | progressContainer->hide(); 101 | } 102 | -------------------------------------------------------------------------------- /src/settings/applicationsettings.cpp: -------------------------------------------------------------------------------- 1 | #include "applicationsettings.h" 2 | 3 | void ApplicationSettings::initialisation() { 4 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 5 | settings.beginGroup("General"); 6 | settings.setValue("version", QApplication::applicationVersion()); 7 | settings.endGroup(); 8 | } 9 | 10 | void ApplicationSettings::setTabOrder(QString tab, int value) { 11 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 12 | settings.beginGroup("TabOrder"); 13 | settings.setValue(tab, value); 14 | settings.endGroup(); 15 | } 16 | 17 | int ApplicationSettings::getTabOrder(QString tab) { 18 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 19 | 20 | settings.beginGroup("TabOrder"); 21 | int position = settings.value(tab, -1).toInt(); 22 | settings.endGroup(); 23 | 24 | return position; 25 | } 26 | 27 | void ApplicationSettings::setSplitterSize(float value) { 28 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 29 | 30 | settings.beginGroup("Widgets"); 31 | settings.setValue("SplitterRelativeSize", QString::number(value)); // Save as string only to make it readable 32 | settings.endGroup(); 33 | } 34 | 35 | float ApplicationSettings::getSplitterSize() { 36 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 37 | 38 | settings.beginGroup("Widgets"); 39 | float size = settings.value("SplitterRelativeSize", 0.25).toFloat(); 40 | settings.endGroup(); 41 | 42 | return size; 43 | } 44 | 45 | void ApplicationSettings::createAppDirIfNeeded() { 46 | QString storageLocation = QDesktopServices::storageLocation(QDesktopServices::DataLocation); 47 | QDir dir(storageLocation); 48 | if (!dir.exists()) { 49 | if (!dir.mkpath(storageLocation)) { 50 | qWarning("Could not create data directory.\n\nExiting..."); 51 | QApplication::exit(1); 52 | } 53 | } 54 | else if (!dir.isReadable()) { 55 | qWarning("Could not read data directory.\n\nExiting..."); 56 | QApplication::exit(1); 57 | } 58 | 59 | } 60 | 61 | QStringList ApplicationSettings::collectionFolderList() { 62 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 63 | 64 | QStringList folders; 65 | int size = settings.beginReadArray("folders"); 66 | for (int i = 0; i < size; ++i) { 67 | settings.setArrayIndex(i); 68 | folders.append(settings.value("path").toString()); 69 | } 70 | folders.removeDuplicates(); 71 | 72 | settings.endArray(); 73 | // TODO: remove paths that do not exist. 74 | 75 | return folders; 76 | } 77 | 78 | void ApplicationSettings::addCollectionFolder(QString location) { 79 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 80 | 81 | QStringList folders = collectionFolderList(); 82 | folders.append(location); 83 | folders.removeDuplicates(); 84 | settings.beginWriteArray("folders"); 85 | for (int i = 0; i < folders.size(); ++i) { 86 | settings.setArrayIndex(i); 87 | settings.setValue("path", folders.at(i)); 88 | } 89 | settings.endArray(); 90 | } 91 | 92 | void ApplicationSettings::setCollectionFolders(QStringList folders) { 93 | 94 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 95 | 96 | // Get list of old values 97 | QStringList oldFolders = collectionFolderList(); 98 | 99 | // Clear old values from settings file 100 | settings.remove("folders"); 101 | 102 | settings.beginWriteArray("folders"); 103 | for (int i = 0; i < folders.size(); ++i) { 104 | settings.setArrayIndex(i); 105 | settings.setValue("path", folders.at(i)); 106 | 107 | /* 108 | * If we're adding a folder that does not exist, call service to scan it! 109 | */ 110 | if (!oldFolders.contains(folders.at(i), Qt::CaseSensitive)) { 111 | qDebug("Parsing directory..."); 112 | // TODO: scan directory - #1 113 | } 114 | } 115 | settings.endArray(); 116 | } 117 | 118 | 119 | void ApplicationSettings::removeColletionFolder(QString location) { 120 | QSettings settings(QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); 121 | QStringList folders = collectionFolderList(); 122 | folders.append(location); 123 | folders.removeAll(location); 124 | settings.beginWriteArray("folders"); 125 | for (int i = 0; i < folders.size(); ++i) { 126 | settings.setArrayIndex(i); 127 | qDebug(folders.at(i).toUtf8()); 128 | settings.setValue("path", folders.at(i)); 129 | } 130 | settings.endArray(); 131 | } 132 | -------------------------------------------------------------------------------- /src/mainwindow/mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | 3 | /// @brief Construtor. 4 | /// 5 | /// @param parent Parent widget 6 | MainWindow::MainWindow(QWidget *parent) 7 | : QMainWindow(parent) 8 | { 9 | setObjectName("MainWindow"); 10 | setWindowTitle("Audactile"); 11 | setMinimumWidth(700); 12 | setMinimumHeight(500); 13 | 14 | // Set strong focus, to capture all keyboard events 15 | setFocusPolicy(Qt::StrongFocus); 16 | 17 | // Our Phonon MediaObject 18 | mediaObject = new Phonon::MediaObject(this); 19 | mediaObject->setTickInterval(1000); 20 | audioOutput = new Phonon::AudioOutput(Phonon::MusicCategory, this); 21 | Phonon::createPath(mediaObject, audioOutput); 22 | connect(audioOutput, SIGNAL(mutedChanged(bool)), this, SLOT(handleMute(bool))); 23 | connect(audioOutput, SIGNAL(volumeChanged(qreal)), this, SLOT(handleVolume(qreal))); 24 | 25 | // Connect the MediaObject to our Last.fm scrobbler 26 | LastFmScrobbler *scrobbler = new LastFmScrobbler(mediaObject); 27 | 28 | // Creates the horizontal layout where we'll put our notebook 29 | middleSplitter = new QSplitter(); 30 | connect(middleSplitter, SIGNAL(splitterMoved(int,int)), this, SLOT(saveSplitterSize(int, int))); 31 | playlistWidget = new PlaylistWidget(this, mediaObject); 32 | mainNotebook = new MainNotebook(this, playlistWidget); // Notebook needs to connect context to playlistwidget! 33 | // TODO: playlistWidget must be a singleton. 34 | mainNotebook->setMinimumWidth(200); 35 | 36 | // Add widgets 37 | middleSplitter->addWidget(mainNotebook); 38 | middleSplitter->addWidget(playlistWidget); 39 | middleSplitter->setStretchFactor(0, 1); 40 | middleSplitter->setStretchFactor(1, 3); 41 | 42 | // Set splitter sizes to saved sizes, if existent 43 | int notebookSize = ceil(width() * ApplicationSettings::getSplitterSize()); 44 | QList sizes; 45 | sizes.append(notebookSize); 46 | sizes.append(width() - notebookSize); 47 | middleSplitter->setSizes(sizes); 48 | middleSplitter->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::MinimumExpanding); 49 | 50 | // Includes our toolbar with more widgets, friendly called PlayerBar 51 | PlayerBar *playerbar = new PlayerBar(this, mediaObject, audioOutput); 52 | connect(playlistWidget, SIGNAL(songInformationUpdated(QMap)), playerbar, SLOT(updateSongInformation(QMap))); 53 | connect(playerbar, SIGNAL(nextButtonClicked()), playlistWidget, SLOT(playNextSong())); 54 | connect(playerbar, SIGNAL(previousButtonClicked()), playlistWidget, SLOT(playPreviousSong())); 55 | connect(playerbar, SIGNAL(toggleFullScreen()), this, SLOT(toggleFullscreen())); 56 | 57 | // Makes playerbar change the window state button 58 | connect(this, SIGNAL(windowStateChanged(Qt::WindowStates)), playerbar, SLOT(handleWindowStateChange(Qt::WindowStates))); 59 | 60 | // Create a vertical layout 61 | QWidget *mainVerticalWidget = new QWidget(); 62 | QVBoxLayout *verticalLayout = new QVBoxLayout(); 63 | verticalLayout->addWidget(playerbar); 64 | verticalLayout->addWidget(middleSplitter); 65 | mainVerticalWidget->setLayout(verticalLayout); 66 | setCentralWidget(mainVerticalWidget); 67 | } 68 | 69 | 70 | // Handle splitter movement 71 | void MainWindow::saveSplitterSize(int pos, int index) { 72 | if (middleSplitter->widget(index) == playlistWidget) { 73 | ApplicationSettings::setSplitterSize((float)pos / width()); 74 | } 75 | } 76 | 77 | /* 78 | * 79 | * The functions handleMute and handleVolume were created to avoid 80 | * the problems with Phonon::VolumeSlider, that was showing wrong 81 | * volume = 0% when volume was unmuted. 82 | * 83 | */ 84 | 85 | 86 | void MainWindow::handleMute(bool mute) { 87 | if (!mute) { 88 | audioOutput->setVolume(outputVolume); 89 | } 90 | } 91 | void MainWindow::handleVolume(qreal volume) { 92 | outputVolume = volume; 93 | } 94 | 95 | 96 | // Handle keyboard events 97 | void MainWindow::keyPressEvent(QKeyEvent *event) { 98 | switch (event->key()) { 99 | case Qt::Key_Space: 100 | case Qt::Key_Enter: 101 | case Qt::Key_MediaPlay: // Will not work in Linux, verify Mac and Windows 102 | case Qt::Key_Play: // Will not work in Linux, verify Mac and Windows 103 | case Qt::Key_Return: 104 | event->accept(); 105 | break; 106 | 107 | // Full screen support! 108 | case Qt::Key_F11: 109 | toggleFullscreen(); 110 | event->accept(); 111 | break; 112 | default: 113 | event->ignore(); 114 | break; 115 | } 116 | } 117 | 118 | // TODO: add button to toggle fullscreen mode. 119 | void MainWindow::toggleFullscreen() { 120 | if (windowState() != Qt::WindowFullScreen) { 121 | originalWindowState = windowState(); 122 | showFullScreen(); 123 | } 124 | else { 125 | switch (originalWindowState) { 126 | case Qt::WindowNoState: 127 | showNormal(); 128 | break; 129 | case Qt::WindowMinimized: 130 | showMinimized(); 131 | break; 132 | default: 133 | showMaximized(); 134 | break; 135 | } 136 | } 137 | emit windowStateChanged(windowState()); 138 | } 139 | -------------------------------------------------------------------------------- /icons.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/gnome/16x16/actions/application-exit.png 4 | icons/gnome/16x16/actions/go-home.png 5 | icons/gnome/16x16/actions/go-up.png 6 | icons/gnome/16x16/actions/media-playback-start.png 7 | icons/gnome/16x16/actions/media-playback-stop.png 8 | icons/gnome/16x16/actions/media-skip-backward.png 9 | icons/gnome/16x16/actions/media-skip-forward.png 10 | icons/gnome/16x16/apps/system-file-manager.png 11 | icons/gnome/16x16/categories/preferences-other.png 12 | icons/gnome/16x16/devices/media-cdrom.png 13 | icons/gnome/16x16/emblems/emblem-web.png 14 | icons/gnome/16x16/mimetypes/audio-x-generic.png 15 | icons/gnome/16x16/mimetypes/sound.png 16 | icons/gnome/16x16/mimetypes/text-x-generic.png 17 | icons/gnome/16x16/places/folder.png 18 | icons/gnome/22x22/actions/application-exit.png 19 | icons/gnome/22x22/actions/go-home.png 20 | icons/gnome/22x22/actions/go-up.png 21 | icons/gnome/22x22/actions/media-playback-start.png 22 | icons/gnome/22x22/actions/media-playback-stop.png 23 | icons/gnome/22x22/actions/media-skip-backward.png 24 | icons/gnome/22x22/actions/media-skip-forward.png 25 | icons/gnome/22x22/apps/system-file-manager.png 26 | icons/gnome/22x22/categories/preferences-other.png 27 | icons/gnome/22x22/devices/media-cdrom.png 28 | icons/gnome/22x22/emblems/emblem-web.png 29 | icons/gnome/22x22/mimetypes/audio-x-generic.png 30 | icons/gnome/22x22/mimetypes/sound.png 31 | icons/gnome/22x22/mimetypes/text-x-generic.png 32 | icons/gnome/22x22/places/folder.png 33 | icons/gnome/24x24/actions/application-exit.png 34 | icons/gnome/24x24/actions/go-home.png 35 | icons/gnome/24x24/actions/go-up.png 36 | icons/gnome/24x24/actions/media-playback-start.png 37 | icons/gnome/24x24/actions/media-playback-stop.png 38 | icons/gnome/24x24/actions/media-skip-backward.png 39 | icons/gnome/24x24/actions/media-skip-forward.png 40 | icons/gnome/24x24/apps/system-file-manager.png 41 | icons/gnome/24x24/categories/preferences-other.png 42 | icons/gnome/24x24/devices/media-cdrom.png 43 | icons/gnome/24x24/emblems/emblem-web.png 44 | icons/gnome/24x24/mimetypes/audio-x-generic.png 45 | icons/gnome/24x24/mimetypes/sound.png 46 | icons/gnome/24x24/mimetypes/text-x-generic.png 47 | icons/gnome/24x24/places/folder.png 48 | icons/gnome/256x256/apps/system-file-manager.png 49 | icons/gnome/256x256/devices/media-cdrom.png 50 | icons/gnome/256x256/mimetypes/audio-x-generic.png 51 | icons/gnome/256x256/mimetypes/sound.png 52 | icons/gnome/256x256/mimetypes/text-x-generic.png 53 | icons/gnome/256x256/places/folder.png 54 | icons/gnome/32x32/actions/application-exit.png 55 | icons/gnome/32x32/actions/go-home.png 56 | icons/gnome/32x32/actions/go-up.png 57 | icons/gnome/32x32/actions/media-playback-start.png 58 | icons/gnome/32x32/actions/media-playback-stop.png 59 | icons/gnome/32x32/actions/media-skip-backward.png 60 | icons/gnome/32x32/actions/media-skip-forward.png 61 | icons/gnome/32x32/apps/system-file-manager.png 62 | icons/gnome/32x32/categories/preferences-other.png 63 | icons/gnome/32x32/devices/media-cdrom.png 64 | icons/gnome/32x32/emblems/emblem-web.png 65 | icons/gnome/32x32/mimetypes/audio-x-generic.png 66 | icons/gnome/32x32/mimetypes/sound.png 67 | icons/gnome/32x32/mimetypes/text-x-generic.png 68 | icons/gnome/32x32/places/folder.png 69 | icons/gnome/48x48/actions/application-exit.png 70 | icons/gnome/48x48/actions/go-home.png 71 | icons/gnome/48x48/actions/go-up.png 72 | icons/gnome/48x48/actions/media-playback-start.png 73 | icons/gnome/48x48/actions/media-playback-stop.png 74 | icons/gnome/48x48/actions/media-skip-backward.png 75 | icons/gnome/48x48/actions/media-skip-forward.png 76 | icons/gnome/48x48/apps/system-file-manager.png 77 | icons/gnome/48x48/categories/preferences-other.png 78 | icons/gnome/48x48/devices/media-cdrom.png 79 | icons/gnome/48x48/emblems/emblem-web.png 80 | icons/gnome/48x48/mimetypes/audio-x-generic.png 81 | icons/gnome/48x48/mimetypes/sound.png 82 | icons/gnome/48x48/mimetypes/text-x-generic.png 83 | icons/gnome/48x48/places/folder.png 84 | icons/gnome/index.theme 85 | icons/gnome/16x16/actions/view-fullscreen.png 86 | icons/gnome/16x16/actions/view-restore.png 87 | icons/gnome/22x22/actions/view-fullscreen.png 88 | icons/gnome/22x22/actions/view-restore.png 89 | icons/gnome/24x24/actions/view-fullscreen.png 90 | icons/gnome/24x24/actions/view-restore.png 91 | icons/gnome/32x32/actions/view-fullscreen.png 92 | icons/gnome/32x32/actions/view-restore.png 93 | icons/gnome/48x48/actions/view-fullscreen.png 94 | icons/gnome/48x48/actions/view-restore.png 95 | icons/gnome/16x16/actions/go-down.png 96 | icons/gnome/22x22/actions/go-down.png 97 | icons/gnome/24x24/actions/go-down.png 98 | icons/gnome/32x32/actions/go-down.png 99 | icons/gnome/48x48/actions/go-down.png 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/services/lastfmscrobbler.cpp: -------------------------------------------------------------------------------- 1 | #include "lastfmscrobbler.h" 2 | 3 | LastFmScrobbler::LastFmScrobbler(Phonon::MediaObject *mediaObject) 4 | { 5 | resetSongStatus(); 6 | this->mediaObject = mediaObject; 7 | songsToScrobble = new QList(); 8 | connect(mediaObject, SIGNAL(stateChanged(Phonon::State,Phonon::State)), this, SLOT(handleStateChange(Phonon::State,Phonon::State))); 9 | 10 | // TODO: call this only if Last.fm is enabled. 11 | // Maybe do a function to call this, calling only 12 | // on applicaton start or when Last.fm is enabled. 13 | state = this->LastFmStateNone; 14 | netManager = new QNetworkAccessManager(this); 15 | handshake(); 16 | } 17 | 18 | 19 | void LastFmScrobbler::handshake() { 20 | QDateTime time = QDateTime::currentDateTime(); 21 | QString timeStamp = QString::number(time.toTime_t()); 22 | 23 | QUrl url("http://post.audioscrobbler.com/"); 24 | url.addQueryItem("hs", "true"); 25 | url.addQueryItem("p", "1.2.1"); 26 | url.addQueryItem("c", "adl"); 27 | url.addQueryItem("v", "0.1"); 28 | url.addQueryItem("u", LastFmSettings::username()); 29 | url.addQueryItem("t", timeStamp); 30 | url.addQueryItem("a", generateToken(LastFmSettings::password(), timeStamp)); 31 | 32 | state = LastFmStateWaitingToken; 33 | QNetworkRequest netRequest; 34 | netRequest.setUrl(url); 35 | qDebug("LastFmScrobbler: Asking to login to Last.fm..."); 36 | authReply = netManager->get(netRequest); 37 | connect(authReply, SIGNAL(finished()), this, SLOT(readAuthenticationReply())); // TODO: handle error() signal 38 | 39 | // If we already have songs in the queue, scrobble them! 40 | if (songsToScrobble->count() > 0) { 41 | tryToScrobble(); 42 | } 43 | 44 | } 45 | 46 | void LastFmScrobbler::readAuthenticationReply() { 47 | qDebug("LastFmScrobbler: Got reply!"); 48 | QString replyString = authReply->readAll(); 49 | if (state == LastFmStateWaitingToken) { 50 | QStringList lines = replyString.split('\n'); 51 | qDebug("LastFmScrobbler: Last.fm answer: " + QString(lines.at(0)).toUtf8()); 52 | if (lines.at(0) == "OK") { 53 | state = LastFmGotToken; 54 | sessionId = lines.at(1); 55 | nowPlayingUrl = lines.at(2); 56 | submissionUrl = lines.at(3); 57 | qDebug("LastFmScrobbler: Last.fm token: " + sessionId.toUtf8()); 58 | } 59 | // TODO: better feedback for the user of what's wrong. 60 | // BANNED / BADAUTH / BADTIME / FAILED 61 | else { 62 | qDebug("LastFmScrobbler: Authentication problem! Disabling Last.fm"); 63 | LastFmSettings::setActive(false); 64 | state = LastFmStateNone; 65 | } 66 | } 67 | } 68 | 69 | QString LastFmScrobbler::generateToken(QString input, QString timestamp) { 70 | /* 71 | * As said in http://www.last.fm/api/submissions#1.2 , 72 | * we must create a token in the format 73 | * token = md5(md5(password) + timestamp) 74 | * to use in scrobble. 75 | */ 76 | QByteArray encryptedPassword = QCryptographicHash::hash(input.toUtf8(), QCryptographicHash::Md5).toHex(); 77 | QByteArray token = QCryptographicHash::hash(encryptedPassword + timestamp.toUtf8(), QCryptographicHash::Md5).toHex(); 78 | return QString(token); 79 | } 80 | 81 | void LastFmScrobbler::onTick(qint64 time) { 82 | /* 83 | * If Last.fm was disabled, the signal will be disconnected only 84 | * when the Phonon state changes. This test avoid undesired 85 | * scrobbling. 86 | */ 87 | if (!LastFmSettings::isActive()) return; 88 | 89 | /* 90 | * Since tickinterval = 1000 isn't asserting that onTick is called 91 | * every 1 second, we'll use lastTickTime to assert that we'll only 92 | * sum 1 to ellapsedTime when 1 second has passed. 93 | */ 94 | if (time == lastTickTime) return; 95 | 96 | ellapsedTime++; 97 | lastTickTime = time; 98 | /* 99 | * We only can scrobble songs that were played for more than 100 | * 240s or half of its length. 101 | */ 102 | if (ellapsedTime >= timeToScrobble || ellapsedTime >= 240) { 103 | songsToScrobble->append(currentSong); 104 | tryToScrobble(); 105 | 106 | // Disconnect this slot if the song was already queued. 107 | disconnect(mediaObject, SIGNAL(tick(qint64)), this, SLOT(onTick(qint64))); 108 | } 109 | } 110 | 111 | void LastFmScrobbler::resetSongStatus() { 112 | ellapsedTime = 0; 113 | lastTickTime = 0; 114 | canScrobble = false; 115 | 116 | // Reset currentSong 117 | SongInfo resetSong; 118 | currentSong = resetSong; 119 | } 120 | 121 | 122 | void LastFmScrobbler::tryToScrobble() { 123 | qDebug("LastFmScrobbler: Trying to scrobble queued songs"); 124 | // If we got no token or queue, nothing done. 125 | if (state != LastFmGotToken || songsToScrobble->count() == 0) return; 126 | 127 | // Try to scrobble queue 128 | QUrl url(submissionUrl); 129 | QString dataToPost = "s=" + sessionId; 130 | 131 | for (int i = 0; i < songsToScrobble->count(); i++) { 132 | SongInfo song = songsToScrobble->at(i); 133 | dataToPost += "&a[" + QString::number(i) + "]=" + song.artist + "&" + 134 | "t[" + QString::number(i) + "]=" + song.title + "&" + 135 | "b[" + QString::number(i) + "]=" + song.album + "&" + 136 | "i[" + QString::number(i) + "]=" + song.startTimeStamp + "&" + 137 | "l[" + QString::number(i) + "]=" + song.duration + "&" + 138 | "n[" + QString::number(i) + "]=" + "&" + // TODO: track number 139 | "m[" + QString::number(i) + "]=" + "&" + // TODO: MusicBrainz id 140 | "r[" + QString::number(i) + "]=" + "&" + // TODO: rating 141 | "o[" + QString::number(i) + "]=P"; 142 | } 143 | //dataToPost = dataToPost.replace(' ',"%20"); 144 | qDebug("LastFmScrobbler: Data to post to " + submissionUrl.toUtf8() + ": " + dataToPost.toUtf8()); 145 | 146 | QNetworkRequest netRequest; 147 | netRequest.setUrl(url); 148 | qDebug("LastFmScrobbler: Scrobbling..."); 149 | 150 | submissionReply = netManager->post(netRequest,dataToPost.toUtf8()); 151 | connect(submissionReply, SIGNAL(readyRead()), this, SLOT(readSubmissionReply())); 152 | } 153 | 154 | 155 | void LastFmScrobbler::readSubmissionReply() { 156 | qDebug("LastFmScrobbler: Got submission reply!"); 157 | QString replyString = QString::fromUtf8(submissionReply->readAll().replace('\n', "")); 158 | qDebug("LastFmScrobbler: Reply: " + QString(replyString).toAscii()); 159 | 160 | // If we get an OK, clear our list of songs to scrobble. 161 | if (replyString == "OK") { 162 | songsToScrobble->clear(); 163 | } 164 | // BADSESSION? Handshake again. And try to scrobble the songs after. 165 | else if (replyString == "BADSESSION") { 166 | handshake(); 167 | } 168 | 169 | 170 | } 171 | 172 | void LastFmScrobbler::handleStateChange(Phonon::State newState, Phonon::State oldState) { 173 | // Disconnect slots if Last.fm is disabled. 174 | if (!LastFmSettings::isActive()) { 175 | qDebug("Last.fm is disabled"); 176 | disconnect(mediaObject, SIGNAL(tick(qint64)), this, SLOT(onTick(qint64))); 177 | disconnect(mediaObject, SIGNAL(finished()), this, SLOT(resetSongStatus())); 178 | return; 179 | } 180 | 181 | 182 | // Save information to scrobble! 183 | if (newState == Phonon::PlayingState && (oldState == Phonon::StoppedState || oldState == Phonon::LoadingState)) { 184 | QMap metaData = mediaObject->metaData(); 185 | QString artist = metaData.value("ARTIST"); 186 | QString album = metaData.value("ALBUM"); 187 | QString title = metaData.value("TITLE"); 188 | 189 | // We only can scrobble titles that have 190 | // artist and title defined. 191 | if (!artist.isEmpty() && !title.isEmpty() && mediaObject->totalTime() >= 30000) { 192 | timeToScrobble = qRound(mediaObject->totalTime() / 2000); 193 | connect(mediaObject, SIGNAL(tick(qint64)), this, SLOT(onTick(qint64))); 194 | connect(mediaObject, SIGNAL(finished()), this, SLOT(resetSongStatus())); 195 | 196 | canScrobble = true; 197 | 198 | // Reset currentSong 199 | SongInfo resetSong; 200 | currentSong = resetSong; 201 | currentSong.artist = artist; 202 | currentSong.title = title; 203 | currentSong.duration = QString::number(qRound(mediaObject->totalTime() / 1000)); 204 | if (!album.isEmpty()) currentSong.album = album; 205 | 206 | QDateTime time = QDateTime::currentDateTime(); 207 | currentSong.startTimeStamp = QString::number(time.toTime_t()).toUtf8(); 208 | 209 | // Send a "Now Playing" notification to Last.fm 210 | QUrl url(nowPlayingUrl); 211 | QString dataToPost = "s=" + sessionId + "&" + 212 | "a=" + currentSong.artist + "&" + 213 | "t=" + currentSong.title + "&" + 214 | "b=" + currentSong.album + "&" + 215 | "l=" + currentSong.duration + "&" + 216 | "n="; 217 | 218 | QNetworkRequest netRequest; 219 | netRequest.setUrl(url); 220 | qDebug("Sending Now Playing..."); 221 | nowPlayingReply = netManager->post(netRequest,dataToPost.toUtf8()); 222 | connect(nowPlayingReply, SIGNAL(readyRead()), this, SLOT(readNowPlayingReply())); 223 | 224 | } 225 | } 226 | else if (newState == Phonon::StoppedState) { 227 | resetSongStatus(); 228 | } 229 | 230 | } 231 | 232 | void LastFmScrobbler::readNowPlayingReply() { 233 | qDebug("Got Now Playing reply!"); 234 | QString replyString = QString::fromUtf8(nowPlayingReply->readAll()); 235 | qDebug("Reply: " + replyString.toUtf8()); 236 | } 237 | -------------------------------------------------------------------------------- /src/mainwindow/playerbar.cpp: -------------------------------------------------------------------------------- 1 | #include "playerbar.h" 2 | 3 | using namespace Phonon; 4 | 5 | /// @brief Audactile's main toolbar class. 6 | /// 7 | /// @param parent Parent widget 8 | /// @param mediaObject MediaObject that will handle the files 9 | /// @param audioOutput AudioOutput that will be linked to MediaObject 10 | PlayerBar::PlayerBar(QWidget *parent, Phonon::MediaObject *mediaObject, Phonon::AudioOutput *audioOutput) 11 | { 12 | mainMediaObject = mediaObject; 13 | setParent(parent); 14 | setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum); 15 | 16 | // Buttons 17 | playButton = new QToolButton(this); 18 | stopButton = new QToolButton(this); 19 | nextButton = new QToolButton(this); 20 | previousButton = new QToolButton(this); 21 | //fullScreenButton = new QToolButton(this); 22 | prefButton = new QToolButton(this); 23 | exitButton = new QToolButton(this); 24 | 25 | playButton->setIconSize(QSize(48, 48)); 26 | stopButton->setIconSize(QSize(48, 48)); 27 | nextButton->setIconSize(QSize(48, 48)); 28 | previousButton->setIconSize(QSize(48, 48)); 29 | //fullScreenButton->setIconSize(QSize(48, 48)); 30 | prefButton->setIconSize(QSize(48, 48)); 31 | exitButton->setIconSize(QSize(48, 48)); 32 | 33 | playButton->setAutoRaise(true); 34 | stopButton->setAutoRaise(true); 35 | nextButton->setAutoRaise(true); 36 | previousButton->setAutoRaise(true); 37 | prefButton->setAutoRaise(true); 38 | //fullScreenButton->setAutoRaise(true); 39 | exitButton->setAutoRaise(true); 40 | 41 | playButton->setIcon(IconFactory::fromTheme("media-playback-start")); 42 | stopButton->setIcon(IconFactory::fromTheme("media-playback-stop")); 43 | nextButton->setIcon(IconFactory::fromTheme("media-skip-forward")); 44 | previousButton->setIcon(IconFactory::fromTheme("media-skip-backward")); 45 | prefButton->setIcon(IconFactory::fromTheme("preferences-other")); 46 | //fullScreenButton->setIcon(IconFactory::fromTheme("view-fullscreen")); 47 | exitButton->setIcon(IconFactory::fromTheme("application-exit")); 48 | 49 | stopButton->setDisabled(true); 50 | 51 | // Signals from buttons 52 | connect(playButton, SIGNAL(clicked()), this, SLOT(handlePlayButton())); 53 | connect(stopButton, SIGNAL(clicked()), this, SLOT(handleStopButton())); 54 | connect(nextButton, SIGNAL(clicked()), this, SIGNAL(nextButtonClicked())); 55 | connect(previousButton, SIGNAL(clicked()), this, SIGNAL(previousButtonClicked())); 56 | connect(prefButton, SIGNAL(clicked()), this, SLOT(openSettings())); 57 | connect(exitButton, SIGNAL(clicked()), this, SLOT(exitApplication())); 58 | //connect(fullScreenButton, SIGNAL(clicked()), this, SIGNAL(toggleFullScreen())); 59 | 60 | // Vertical box with slider and current song time labels 61 | QFrame *songPositionWidget = new QFrame(this); 62 | songPositionWidget->setFrameShadow(QFrame::Sunken); 63 | songPositionWidget->setFrameShape(QFrame::StyledPanel); 64 | songPositionWidget->setMidLineWidth(4); 65 | 66 | 67 | // Horizontal box with current song position labels 68 | QWidget *songPositionLabelsWidget = new QWidget(this); 69 | QHBoxLayout *songPositionLabelsHBox = new QHBoxLayout(songPositionLabelsWidget); 70 | currentSongPosition = new QLabel(tr("--:--")); 71 | remainingSongPosition = new QLabel(tr("--:--")); 72 | currentSongInfo = new QLabel(tr("")); 73 | 74 | // Define song info to be bold 75 | QFont songInfoFont = currentSongInfo->font(); 76 | songInfoFont.setBold(true); 77 | songInfoFont.setPointSize(songInfoFont.pointSize() + 2); 78 | currentSongInfo->setFont(songInfoFont); 79 | 80 | // Add labels 81 | songPositionLabelsHBox->addWidget(currentSongPosition, 1, Qt::AlignLeft); 82 | songPositionLabelsHBox->addWidget(currentSongInfo, 1, Qt::AlignCenter); 83 | songPositionLabelsHBox->addWidget(remainingSongPosition, 1, Qt::AlignRight); 84 | songPositionLabelsWidget->setLayout(songPositionLabelsHBox); 85 | 86 | // Set background for song position widget 87 | QPalette palette = songPositionWidget->palette(); 88 | QLinearGradient songPositionBgGradient(1.0, 1.0, 1.0, 40.0); 89 | songPositionBgGradient.setColorAt(0.0, QColor(250,250,230)); 90 | songPositionBgGradient.setColorAt(1.0, QColor(220,220,200)); 91 | palette.setBrush(songPositionLabelsWidget->backgroundRole(), QBrush(songPositionBgGradient)); 92 | palette.setColor(songPositionLabelsWidget->foregroundRole(), QColor(100,100,100)); 93 | songPositionWidget->setPalette(palette); 94 | songPositionWidget->setAutoFillBackground(true); 95 | 96 | QVBoxLayout *songPositionVBox = new QVBoxLayout(songPositionWidget); 97 | songPositionSlider = new SeekSlider(mainMediaObject, this); 98 | songPositionSlider->setOrientation(Qt::Horizontal); 99 | songPositionVBox->addWidget(songPositionLabelsWidget); 100 | songPositionVBox->addWidget(songPositionSlider); 101 | songPositionWidget->setLayout(songPositionVBox); 102 | 103 | // Volume button 104 | Phonon::VolumeSlider *volumeSlider = new Phonon::VolumeSlider(audioOutput, this); 105 | volumeSlider->setMaximumWidth((int)floor(0.2*(window()->width()))); 106 | 107 | QHBoxLayout *playerBarLayout = new QHBoxLayout(this); 108 | playerBarLayout->addWidget(playButton); 109 | playerBarLayout->addWidget(stopButton); 110 | playerBarLayout->addWidget(Separator::verticalSeparator(this)); 111 | playerBarLayout->addWidget(previousButton); 112 | playerBarLayout->addWidget(nextButton); 113 | playerBarLayout->addWidget(Separator::verticalSeparator(this)); 114 | playerBarLayout->addWidget(songPositionWidget); 115 | playerBarLayout->addWidget(Separator::verticalSeparator(this)); 116 | playerBarLayout->addWidget(volumeSlider); 117 | playerBarLayout->addWidget(Separator::verticalSeparator(this)); 118 | //playerBarLayout->addWidget(fullScreenButton); 119 | playerBarLayout->addWidget(prefButton); 120 | playerBarLayout->addWidget(exitButton); 121 | 122 | setLayout(playerBarLayout); 123 | 124 | // Signals from media source 125 | connect(mainMediaObject, SIGNAL(tick(qint64)), this, SLOT(updateSongPosition())); 126 | connect(mainMediaObject, SIGNAL(stateChanged(Phonon::State,Phonon::State)), this, SLOT(handleState(Phonon::State))); 127 | connect(mainMediaObject, SIGNAL(finished()), this, SLOT(finish())); 128 | 129 | show(); 130 | } 131 | 132 | 133 | 134 | /// @brief Update current song position and remaining song time on playerbar. 135 | void PlayerBar::updateSongPosition() { 136 | int secs, mins; 137 | QString qStr; 138 | 139 | /* 140 | * Get the remaining time. If we have more than 1000 ms, we 141 | * can update the time. The last second must not update the 142 | * playerbar, since after that the timer will become "--:--". 143 | */ 144 | qint64 rem = mainMediaObject->remainingTime(); 145 | if (rem > 1000) { 146 | qint64 cur = mainMediaObject->currentTime(); 147 | 148 | songPositionSlider->setEnabled(true); 149 | 150 | secs = (rem / 1000) % 60; 151 | mins = (rem / 1000) / 60; 152 | remainingSongPosition->setText("-" + QString::number(mins) + ":" + qStr.sprintf("%02d", secs)); 153 | 154 | secs = (cur / 1000) % 60; 155 | mins = (cur / 1000) / 60; 156 | currentSongPosition->setText(QString::number(mins) + ":" + qStr.sprintf("%02d", secs)); 157 | } 158 | 159 | } 160 | 161 | 162 | /// @brief Handle the state change in the MediaObject. 163 | /// 164 | /// @param newState New state 165 | /// @param oldState Old state 166 | void PlayerBar::handleState(Phonon::State newState) { 167 | // Handle play button 168 | if (newState == Phonon::PlayingState) { 169 | playButton->setIcon(IconFactory::fromTheme("media-playback-pause")); 170 | } 171 | else if (newState == Phonon::StoppedState) { 172 | playButton->setIcon(IconFactory::fromTheme("media-playback-start")); 173 | } 174 | else if (newState == Phonon::PausedState) { 175 | playButton->setIcon(IconFactory::fromTheme("media-playback-start")); 176 | } 177 | 178 | // Handle stop button 179 | if (newState <= Phonon::StoppedState) { 180 | stopButton->setDisabled(true); 181 | } 182 | else { 183 | stopButton->setEnabled(true); 184 | } 185 | } 186 | 187 | // TODO: deliver the play/stop buttons callback to PlaylistWidget using signals. 188 | 189 | /// @brief Callback for play button 190 | void PlayerBar::handlePlayButton() { 191 | if (mainMediaObject->state() == Phonon::PlayingState) { 192 | mainMediaObject->pause(); 193 | } 194 | else if (mainMediaObject->currentSource().type() != MediaSource::Empty) { 195 | mainMediaObject->play(); 196 | } 197 | updateSongPosition(); 198 | } 199 | 200 | /// @brief Callback for stop button 201 | void PlayerBar::handleStopButton() { 202 | if (mainMediaObject->state() > Phonon::StoppedState) { 203 | resetDisplay(); 204 | mainMediaObject->stop(); 205 | } 206 | } 207 | 208 | /// @brief Update the song information in the bar 209 | /// 210 | /// @param newSongInformation 211 | void PlayerBar::updateSongInformation(QMapnewSongInformation) { 212 | currentSongInfo->setText(newSongInformation.value("artist") + " - " + newSongInformation.value("title")); 213 | } 214 | 215 | /// @brief Reset information to its initial state 216 | void PlayerBar::resetDisplay() { 217 | qDebug("resetDisplay"); 218 | currentSongPosition->setText(tr("--:--")); 219 | remainingSongPosition->setText(tr("--:--")); 220 | currentSongInfo->setText(tr("")); 221 | } 222 | 223 | /// @brief Commands to execute when the music ends 224 | void PlayerBar::finish() { 225 | resetDisplay(); 226 | mainMediaObject->stop(); 227 | } 228 | 229 | 230 | /// @brief Quit application 231 | void PlayerBar::exitApplication() { 232 | exit(0); 233 | } 234 | 235 | /// @brief Open settings 236 | void PlayerBar::openSettings() { 237 | SettingsDialog *dialog = new SettingsDialog(parentWidget()); 238 | } 239 | 240 | /// @brief Set the correct button to screen button 241 | void PlayerBar::handleWindowStateChange(Qt::WindowStates windowState) { 242 | if (windowState == Qt::WindowFullScreen) { 243 | fullScreenButton->setIcon(IconFactory::fromTheme("view-restore")); 244 | } 245 | else { 246 | fullScreenButton->setIcon(IconFactory::fromTheme("view-fullscreen")); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/collection/collectiondatabase.cpp: -------------------------------------------------------------------------------- 1 | #include "collectiondatabase.h" 2 | 3 | 4 | /* 5 | * TODO: 6 | * - When some music is removed, verify if the album must exist 7 | * - When some album is removed, verify if the artist must exist 8 | * - Handle errors 9 | * - Improve indexes 10 | * - Use some mechanism to speed up the process to add multiple songs of the same artist 11 | * 12 | */ 13 | 14 | CollectionDatabase::CollectionDatabase(QObject *parent) : 15 | QObject(parent) 16 | { 17 | db = QSqlDatabase::addDatabase("QSQLITE"); 18 | QString database = QDesktopServices::storageLocation(QDesktopServices::DataLocation) + QDir::separator() + "collection.db"; 19 | db.setDatabaseName(database); 20 | 21 | createDatabase(); 22 | } 23 | 24 | QSqlTableModel *CollectionDatabase::collectionModel() { 25 | QSqlTableModel *model = new QSqlTableModel(); 26 | model->setTable("collection"); 27 | model->setSort(model->fieldIndex("track_number"), Qt::AscendingOrder); 28 | model->setSort(model->fieldIndex("album"), Qt::AscendingOrder); // TODO: sort by year 29 | model->setSort(model->fieldIndex("artist"), Qt::AscendingOrder); 30 | return model; 31 | } 32 | 33 | QSqlTableModel *CollectionDatabase::artistModel() { 34 | QSqlTableModel *model = new QSqlTableModel(); 35 | model->setTable("artist"); 36 | model->setSort(model->fieldIndex("name"), Qt::AscendingOrder); 37 | return model; 38 | } 39 | 40 | QSqlTableModel *CollectionDatabase::albumModel() { 41 | QSqlTableModel *model = new QSqlTableModel(); 42 | model->setTable("album"); 43 | model->setSort(model->fieldIndex("title"), Qt::AscendingOrder); 44 | return model; 45 | } 46 | 47 | QSqlTableModel *CollectionDatabase::musicModel() { 48 | QSqlTableModel *model = new QSqlTableModel(); 49 | model->setTable("music"); 50 | return model; 51 | } 52 | 53 | void CollectionDatabase::createDatabase() { 54 | if (!db.open()) { 55 | qDebug("ERROR! " + db.lastError().text().toUtf8()); 56 | return; 57 | } 58 | 59 | QStringList tables = db.tables(QSql::Tables); 60 | QStringList views = db.tables(QSql::Views); 61 | 62 | // TODO: use titles as primary key? 63 | 64 | if (!tables.contains("artist")) { 65 | db.exec("CREATE TABLE artist (" 66 | "id integer primary key AUTOINCREMENT, " 67 | "name varchar(120) UNIQUE NOT NULL" 68 | ");"); 69 | } 70 | 71 | if (!tables.contains("album")) { 72 | db.exec("CREATE TABLE album (" 73 | "id integer primary key AUTOINCREMENT, " 74 | "id_artist int, " 75 | "title varchar(120) NOT NULL" 76 | ");"); 77 | } 78 | 79 | if (!tables.contains("music")) { 80 | db.exec("CREATE TABLE music (" 81 | "id integer primary key AUTOINCREMENT, " 82 | "id_album int, " 83 | "track_number int, " 84 | "title varchar(120) NOT NULL," 85 | "path varchar(2048) UNIQUE NOT NULL, " 86 | "last_modified int " 87 | ");"); 88 | } 89 | 90 | if (!views.contains("collection")) { 91 | db.exec("CREATE VIEW collection AS " 92 | "SELECT artist.name as artist, album.title as album, music.title as music, music.track_number as track_number, music.path as path, artist.id as id_artist, album.id as id_album, music.id as id_music " 93 | "FROM artist, album, music " 94 | "WHERE album.id_artist = artist.id AND music.id_album = album.id " 95 | "ORDER BY artist.name, album.title, music.track_number;"); 96 | } 97 | 98 | if (db.lastError().type() != QSqlError::NoError) { 99 | qDebug("ERROR! " + db.lastError().text().toUtf8()); 100 | } 101 | 102 | 103 | } 104 | 105 | void CollectionDatabase::addArtist(QString artistName) { 106 | if (!db.isOpen()) { 107 | if (!db.open()) qDebug("ERROR! " + db.lastError().text().toUtf8()); 108 | return; 109 | } 110 | 111 | QSqlQuery query(db); 112 | query.prepare("SELECT count('name') FROM artist WHERE name=:artist;"); 113 | query.bindValue(":artist", artistName); 114 | query.exec(); 115 | if (query.first()) { 116 | int total = query.value(0).toInt(); 117 | if (total == 0) { 118 | QSqlQuery insertQuery(db); 119 | insertQuery.prepare("INSERT INTO artist (name) VALUES (:artist);"); 120 | insertQuery.bindValue(":artist", artistName); 121 | insertQuery.exec(); 122 | } 123 | } 124 | } 125 | 126 | void CollectionDatabase::addAlbum(QString artistName, QString albumName) { 127 | // Since addArtist tests if artist already exists, it's safe to call it here 128 | addArtist(artistName); 129 | 130 | if (!db.isOpen()) { 131 | if (!db.open()) qDebug("ERROR! " + db.lastError().text().toUtf8()); 132 | return; 133 | } 134 | 135 | QSqlQuery query(db); 136 | query.prepare("SELECT count('title') FROM album WHERE title=:album AND id_artist=(SELECT id FROM artist WHERE name=:artist);"); 137 | query.bindValue(":album", albumName); 138 | query.bindValue(":artist", artistName); 139 | query.exec(); 140 | if (query.first()) { 141 | int total = query.value(0).toInt(); 142 | if (total == 0) { 143 | QSqlQuery insertQuery(db); 144 | insertQuery.prepare("INSERT INTO album (id_artist, title) VALUES ((SELECT id FROM artist WHERE name=:artist),:album);"); 145 | insertQuery.bindValue(":album", albumName); 146 | insertQuery.bindValue(":artist", artistName); 147 | insertQuery.exec(); 148 | } 149 | } 150 | query.clear(); 151 | 152 | if (db.lastError().type() != QSqlError::NoError) { 153 | qDebug("ERROR! " + db.lastError().text().toUtf8()); 154 | } 155 | } 156 | 157 | bool CollectionDatabase::addOrUpdateMusic(Music *music) { 158 | if (!music->isValid()) return false; 159 | 160 | QString artist = music->getArtist(); 161 | QString album = music->getAlbum(); 162 | QString title = music->getTitle(); 163 | QString path = music->getFileUrl().path(); 164 | unsigned int trackNumber = music->getTrackNumber(); 165 | unsigned int lastModified = QFileInfo(path).lastModified().toTime_t(); 166 | 167 | if (!db.isOpen()) { 168 | if (!db.open()) qDebug("ERROR! " + db.lastError().text().toUtf8()); 169 | return false; 170 | } 171 | 172 | QSqlQuery query; 173 | query.prepare("SELECT count(*) FROM music WHERE path=?"); 174 | query.bindValue(":path", path); 175 | query.exec(); 176 | query.first(); 177 | unsigned int total = query.value(0).toUInt(); 178 | if (total == 0) { 179 | /* 180 | * Assert that artist and album exists. 181 | * addAlbum() asserts artist existence. 182 | * 183 | */ 184 | addAlbum(artist, album); 185 | 186 | QSqlQuery insertQuery(db); 187 | insertQuery.prepare( 188 | "INSERT INTO music (id_album, track_number, title, path, last_modified) VALUES (" 189 | "(SELECT id FROM album WHERE title=:album AND id_artist = (SELECT id FROM artist WHERE name = :artist))," 190 | ":tracknumber, :title, :path, :lastmodified);" 191 | ); 192 | insertQuery.bindValue(":album", album); 193 | insertQuery.bindValue(":artist", artist); 194 | insertQuery.bindValue(":tracknumber", trackNumber); 195 | insertQuery.bindValue(":title", title); 196 | insertQuery.bindValue(":path", path); 197 | insertQuery.bindValue(":lastmodified", lastModified); 198 | insertQuery.exec(); 199 | 200 | if (db.lastError().type() != QSqlError::NoError) { 201 | qDebug("ERROR! " + db.lastError().text().toUtf8()); 202 | qDebug(insertQuery.executedQuery().toUtf8()); 203 | return false; 204 | } 205 | return true; 206 | 207 | } 208 | else { 209 | // If the file was updated, we must update the database. 210 | query.prepare("SELECT last_modified FROM music WHERE path=:path"); 211 | query.bindValue(":path", path); 212 | query.exec(); 213 | query.first(); 214 | unsigned int lastModifiedInDb = query.value(0).toUInt(); 215 | if (lastModified > lastModifiedInDb) { 216 | QSqlQuery updateQuery(db); 217 | updateQuery.prepare( 218 | "UPDATE music SET " 219 | "track_number = :tracknumber, " 220 | "title = :title, " 221 | "id_album = (SELECT id FROM album WHERE title=:album AND id_artist = (SELECT id FROM artist WHERE name = :artist)), " 222 | "last_modified = :lastmodified " 223 | "WHERE path = :path" 224 | ); 225 | updateQuery.bindValue(":album", album); 226 | updateQuery.bindValue(":artist", artist); 227 | updateQuery.bindValue(":tracknumber", trackNumber); 228 | updateQuery.bindValue(":title", title); 229 | updateQuery.bindValue(":path", path); 230 | updateQuery.bindValue(":lastmodified", lastModified); 231 | updateQuery.exec(); 232 | 233 | if (db.lastError().type() != QSqlError::NoError) { 234 | qDebug("ERROR! " + db.lastError().text().toUtf8()); 235 | qDebug(path.toUtf8()); 236 | return false; 237 | } 238 | return true; 239 | } 240 | } 241 | return false; 242 | } 243 | 244 | 245 | void CollectionDatabase::removeArtist(QString artistName) { 246 | if (!db.open()) { 247 | qDebug("ERROR! " + db.lastError().text().toUtf8()); 248 | return; 249 | } 250 | db.exec("DELETE FROM artist WHERE name='" + artistName + "'"); 251 | } 252 | 253 | void CollectionDatabase::removeAlbum(QString artistName, QString albumName) { 254 | if (!db.open()) { 255 | qDebug("ERROR! " + db.lastError().text().toUtf8()); 256 | return; 257 | } 258 | db.exec("DELETE FROM album WHERE name='" + albumName + "' AND id_artist=(SELECT id FROM artist WHERE name='" + artistName + "')"); 259 | } 260 | 261 | void CollectionDatabase::removeMusic(QString path) { 262 | if (!db.open()) { 263 | qDebug("ERROR! " + db.lastError().text().toUtf8()); 264 | return; 265 | } 266 | db.exec("DELETE FROM music WHERE path='" + path + "'"); 267 | } 268 | 269 | void CollectionDatabase::removeMusic(Music *music) { 270 | removeMusic(music->getFileUrl().path()); 271 | } 272 | -------------------------------------------------------------------------------- /icons/gnome/index.theme: -------------------------------------------------------------------------------- 1 | [Icon Theme] 2 | Name=GNOME 3 | Name[af]=GNOME 4 | Name[ar]=جنوم 5 | Name[as]=GNOME 6 | Name[ast]=GNOME 7 | Name[az]=GNOME 8 | Name[be]=GNOME 9 | Name[be@latin]=GNOME 10 | Name[bg]=GNOME 11 | Name[bn]=GNOME 12 | Name[bn_IN]=GNOME 13 | Name[br]=GNOME 14 | Name[bs]=GNOME 15 | Name[ca]=GNOME 16 | Name[ca@valencia]=GNOME 17 | Name[crh]=GNOME 18 | Name[cs]=GNOME 19 | Name[cy]=GNOME 20 | Name[da]=Gnome 21 | Name[de]=GNOME 22 | Name[dz]=ཇི་ནོམ། 23 | Name[el]=GNOME 24 | Name[en@shaw]=·𐑜𐑯𐑴𐑥 25 | Name[en_CA]=GNOME 26 | Name[en_GB]=GNOME 27 | Name[es]=GNOME 28 | Name[et]=GNOME 29 | Name[eu]=GNOME 30 | Name[fa]=گنوم 31 | Name[fi]=GNOME 32 | Name[fr]=GNOME 33 | Name[fur]=GNOME 34 | Name[ga]=GNOME 35 | Name[gl]=GNOME 36 | Name[gu]=જીનોમ 37 | Name[he]=GNOME 38 | Name[hi]=गनोम 39 | Name[hr]=GNOME 40 | Name[hu]=GNOME 41 | Name[id]=GNOME 42 | Name[is]=GNOME 43 | Name[it]=GNOME 44 | Name[ja]=GNOME 45 | Name[ka]=გნომი 46 | Name[kk]=GNOME ортасы 47 | Name[kn]=GNOME 48 | Name[ko]=그놈 49 | Name[lt]=GNOME 50 | Name[lv]=GNOME 51 | Name[mai]=गनोम 52 | Name[mg]=GNOME 53 | Name[mk]=GNOME 54 | Name[ml]=ഗ്നോം 55 | Name[mn]=ГНОМЕ 56 | Name[mr]=GNOME 57 | Name[ms]=GNOME 58 | Name[nb]=GNOME 59 | Name[nds]=GNOME 60 | Name[ne]=जिनोम 61 | Name[nl]=GNOME 62 | Name[nn]=GNOME 63 | Name[oc]=GNOME 64 | Name[or]=GNOME 65 | Name[pa]=ਗਨੋਮ 66 | Name[pl]=GNOME 67 | Name[ps]=جنومي 68 | Name[pt]=GNOME 69 | Name[pt_BR]=GNOME 70 | Name[ro]=GNOME 71 | Name[ru]=Среда GNOME 72 | Name[si]=GNOME 73 | Name[sk]=GNOME 74 | Name[sl]=GNOME 75 | Name[sq]=GNOME 76 | Name[sr]=Гном 77 | Name[sr@latin]=Gnom 78 | Name[sv]=GNOME 79 | Name[ta]=GNOME 80 | Name[te]=గ్నోమ్ 81 | Name[th]=GNOME 82 | Name[tk]=GNOME 83 | Name[tr]=GNOME 84 | Name[uk]=GNOME 85 | Name[uz]=GNOME 86 | Name[uz@cyrillic]=GNOME 87 | Name[vi]=GNOME 88 | Name[xh]=i-GNOME 89 | Name[zh_CN]=GNOME 90 | Name[zh_HK]=GNOME 91 | Name[zh_TW]=GNOME 92 | Comment=Default GNOME Theme 93 | Comment[af]=Verstek-GNOME-tema 94 | Comment[ar]=سِمة جنوم الإفتراضية 95 | Comment[as]=অবিকল্পিত GNOME থিম 96 | Comment[ast]=Tema predetermináu de Gnome 97 | Comment[az]=Ön Qurğulu Gnome Örtüyü 98 | Comment[be]=Прадвызначаная тэма GNOME 99 | Comment[be@latin]=Zmoŭčany matyŭ GNOME 100 | Comment[bg]=Стандартна тема на GNOME 101 | Comment[bn]=ডিফল্ট GNOME থীম 102 | Comment[bn_IN]=ডিফল্ট GNOME থিম 103 | Comment[br]=Neuz GNOME dre-ziouer 104 | Comment[bs]=Uobičajena GNOME Tema 105 | Comment[ca]=Tema del GNOME per defecte 106 | Comment[ca@valencia]=Tema del GNOME per defecte 107 | Comment[crh]=Ög-belgilengen GNOME Teması 108 | Comment[cs]=Výchozí motiv GNOME 109 | Comment[cy]=Thema diofyn GNOME 110 | Comment[da]=Standardtema til Gnome 111 | Comment[de]=GNOME-Vorgabethema 112 | Comment[dz]=སྔོན་སྒྲིག་ཇི་ནོམ་བརྗོད་དོན། 113 | Comment[el]=Εξ ορισμού θέμα του Gnome 114 | Comment[en@shaw]=𐑛𐑦𐑓𐑷𐑤𐑑 ·𐑜𐑯𐑴𐑥 𐑔𐑰𐑥 115 | Comment[en_CA]=Default GNOME Theme 116 | Comment[en_GB]=Default GNOME Theme 117 | Comment[es]=Tema predeterminado de GNOME 118 | Comment[et]=Gnome'i vaiketeema 119 | Comment[eu]=Lehenetsitako GNOMEren gaia 120 | Comment[fa]=تم پیش‌فرض گنوم 121 | Comment[fi]=Gnomen oletusteema 122 | Comment[fr]=Thème GNOME par défaut 123 | Comment[fur]=Teme predefinît di GNOME 124 | Comment[ga]=Téama GNOME Réamhshocraithe 125 | Comment[gl]=Tema predefinido do GNOME 126 | Comment[gu]=મૂળભૂત જીનોમ થીમ 127 | Comment[he]=ערכת הנושא ברירת המחדל של Gnome 128 | Comment[hi]=डिफ़ॉल्ट गनोम प्रसंग 129 | Comment[hr]=Uobičajena tema GNOME sustava 130 | Comment[hu]=Alapértelmezett GNOME-téma 131 | Comment[id]=Tema GNOME Standar 132 | Comment[is]=Sjálfgefið GNOME Þema 133 | Comment[it]=Tema predefinito di GNOME 134 | Comment[ja]=デフォルトの GNOME テーマ 135 | Comment[ka]=ნაგულისხმევი გნომის გაფორმება 136 | Comment[kk]=GNOME ортасының стандартты тақырыбы 137 | Comment[kn]=ಪೂರ್ವನಿಯೋಜಿತ GNOME ಥೀಮ್ 138 | Comment[ko]=기본 그놈 테마 139 | Comment[lt]=Standartinė GNOME tema 140 | Comment[lv]=Noklusētā Gnome tēma 141 | Comment[mai]=मूलभूत गनोम प्रसंग 142 | Comment[mg]=Endrika GNOME lasitra 143 | Comment[mk]=Основна тема на GNOME 144 | Comment[ml]=ഗ്നോമിലെ സഹജമായ രംഗവിധാനം 145 | Comment[mn]=ГНОМЕ-стандарт загварууд 146 | Comment[mr]=मुलभूत GNOME थीम 147 | Comment[ms]=Tema GNOME Default 148 | Comment[nb]=Forvalgt GNOME tema 149 | Comment[nds]=Standard GNOME Utsehn 150 | Comment[ne]=पूर्वनिर्धारित जिनोम विषयवस्तु 151 | Comment[nl]=Standaard GNOME-thema 152 | Comment[nn]=Vanleg GNOME-drakt 153 | Comment[oc]=Tèma per defaut de GNOME 154 | Comment[or]=ପୂର୍ବ ନିର୍ଦ୍ଧାରିତ GNOME ପ୍ରସଙ୍ଗ 155 | Comment[pa]=ਡਿਫਾਲਟ ਗਨੋਮ ਥੀਮ 156 | Comment[pl]=Domyślny motyw GNOME 157 | Comment[ps]=د جنومي تلواله ويينه 158 | Comment[pt]=Tema GNOME por Omissão 159 | Comment[pt_BR]=Tema Padrão do GNOME 160 | Comment[ro]=Temă implicită GNOME 161 | Comment[ru]=Стандартная тема среды GNOME 162 | Comment[si]=සාමාන්‍ය GNOME තේමාව 163 | Comment[sk]=Štandardná téma GNOME 164 | Comment[sl]=Privzeta tema GNOME 165 | Comment[sq]=Tema e prezgjedhur e GNOME 166 | Comment[sr]=Подразумевана тема Гнома 167 | Comment[sr@latin]=Podrazumevana tema Gnoma 168 | Comment[sv]=Standardtema för GNOME 169 | Comment[ta]=முன்னிருப்பு GNOME பொருள் 170 | Comment[te]=అప్రమెయ గ్నొమ్ వైవిద్యాంశం 171 | Comment[th]=ชุดตกแต่งปริยายของ Gnome 172 | Comment[tk]=GNOMEniň oň bellenen temi 173 | Comment[tr]=Öntanımlı GNOME Teması 174 | Comment[uk]=Типова тема середовища GNOME 175 | Comment[uz]=Andoza GNOME mavzusi 176 | Comment[uz@cyrillic]=Андоза GNOME мавзуси 177 | Comment[vi]=Sắc thái GNOME Mặc Định 178 | Comment[xh]=Misela Umxholo we GNOME 179 | Comment[zh_CN]=GNOME 默认主题 180 | Comment[zh_HK]=預設 GNOME 佈景主題 181 | Comment[zh_TW]=預設 GNOME 佈景主題 182 | Example=start-here 183 | 184 | # KDE Specific Stuff 185 | DisplayDepth=32 186 | LinkOverlay=link_overlay 187 | LockOverlay=lock_overlay 188 | ZipOverlay=zip_overlay 189 | DesktopDefault=48 190 | DesktopSizes=16,22,32,48,64,72,96,128 191 | ToolbarDefault=22 192 | ToolbarSizes=16,22,32,48 193 | MainToolbarDefault=22 194 | MainToolbarSizes=16,22,32,48 195 | SmallDefault=16 196 | SmallSizes=16 197 | PanelDefault=32 198 | PanelSizes=16,22,32,48,64,72,96,128 199 | 200 | # Directory list 201 | Directories=8x8/emblems,16x16/actions,16x16/animations,16x16/apps,16x16/categories,16x16/devices,16x16/emblems,16x16/emotes,16x16/mimetypes,16x16/places,16x16/status,22x22/actions,22x22/animations,22x22/apps,22x22/categories,22x22/devices,22x22/emblems,22x22/emotes,22x22/mimetypes,22x22/places,22x22/status,24x24/actions,24x24/apps,24x24/categories,24x24/devices,24x24/emblems,24x24/emotes,24x24/mimetypes,24x24/places,24x24/status,32x32/actions,32x32/animations,32x32/apps,32x32/categories,32x32/devices,32x32/emblems,32x32/emotes,32x32/mimetypes,32x32/places,32x32/status,48x48/actions,48x48/animations,48x48/apps,48x48/categories,48x48/devices,48x48/emblems,48x48/emotes,48x48/mimetypes,48x48/places,48x48/status,256x256/actions,256x256/apps,256x256/categories,256x256/devices,256x256/emblems,256x256/emotes,256x256/mimetypes,256x256/places,256x256/status 202 | 203 | [8x8/emblems] 204 | Context=Emblems 205 | Size=8 206 | Type=Fixed 207 | 208 | [16x16/actions] 209 | Context=Actions 210 | Size=16 211 | Type=Fixed 212 | 213 | [16x16/animations] 214 | Context=Animations 215 | Size=16 216 | Type=Fixed 217 | 218 | [16x16/apps] 219 | Context=Applications 220 | Size=16 221 | Type=Fixed 222 | 223 | [16x16/categories] 224 | Context=Categories 225 | Size=16 226 | Type=Fixed 227 | 228 | [16x16/devices] 229 | Context=Devices 230 | Size=16 231 | Type=Fixed 232 | 233 | [16x16/emblems] 234 | Context=Emblems 235 | Size=16 236 | Type=Fixed 237 | 238 | [16x16/emotes] 239 | Context=Emotes 240 | Size=16 241 | Type=Fixed 242 | 243 | [16x16/mimetypes] 244 | Context=MimeTypes 245 | Size=16 246 | Type=Fixed 247 | 248 | [16x16/places] 249 | Context=Places 250 | Size=16 251 | Type=Fixed 252 | 253 | [16x16/status] 254 | Context=Status 255 | Size=16 256 | Type=Fixed 257 | 258 | [22x22/actions] 259 | Context=Actions 260 | Size=22 261 | Type=Fixed 262 | 263 | [22x22/animations] 264 | Context=Animations 265 | Size=22 266 | Type=Fixed 267 | 268 | [22x22/apps] 269 | Context=Applications 270 | Size=22 271 | Type=Fixed 272 | 273 | [22x22/categories] 274 | Context=Categories 275 | Size=22 276 | Type=Fixed 277 | 278 | [22x22/devices] 279 | Context=Devices 280 | Size=22 281 | Type=Fixed 282 | 283 | [22x22/emblems] 284 | Context=Emblems 285 | Size=22 286 | Type=Fixed 287 | 288 | [22x22/emotes] 289 | Context=Emotes 290 | Size=22 291 | Type=Fixed 292 | 293 | [22x22/mimetypes] 294 | Context=MimeTypes 295 | Size=22 296 | Type=Fixed 297 | 298 | [22x22/places] 299 | Context=Places 300 | Size=22 301 | Type=Fixed 302 | 303 | [22x22/status] 304 | Context=Status 305 | Size=22 306 | Type=Fixed 307 | 308 | [24x24/actions] 309 | Context=Actions 310 | Size=24 311 | Type=Fixed 312 | 313 | [24x24/apps] 314 | Context=Applications 315 | Size=24 316 | Type=Fixed 317 | 318 | [24x24/categories] 319 | Context=Categories 320 | Size=24 321 | Type=Fixed 322 | 323 | [24x24/devices] 324 | Context=Devices 325 | Size=24 326 | Type=Fixed 327 | 328 | [24x24/emblems] 329 | Context=Emblems 330 | Size=24 331 | Type=Fixed 332 | 333 | [24x24/emotes] 334 | Context=Emotes 335 | Size=24 336 | Type=Fixed 337 | 338 | [24x24/mimetypes] 339 | Context=MimeTypes 340 | Size=24 341 | Type=Fixed 342 | 343 | [24x24/places] 344 | Context=Places 345 | Size=24 346 | Type=Fixed 347 | 348 | [24x24/status] 349 | Context=Status 350 | Size=24 351 | Type=Fixed 352 | 353 | [32x32/actions] 354 | Context=Actions 355 | Size=32 356 | Type=Fixed 357 | 358 | [32x32/animations] 359 | Context=Animations 360 | Size=32 361 | Type=Fixed 362 | 363 | [32x32/apps] 364 | Context=Applications 365 | Size=32 366 | Type=Fixed 367 | 368 | [32x32/categories] 369 | Context=Categories 370 | Size=32 371 | Type=Fixed 372 | 373 | [32x32/devices] 374 | Context=Devices 375 | Size=32 376 | Type=Fixed 377 | 378 | [32x32/emblems] 379 | Context=Emblems 380 | Size=32 381 | Type=Fixed 382 | 383 | [32x32/emotes] 384 | Context=Emotes 385 | Size=32 386 | Type=Fixed 387 | 388 | [32x32/mimetypes] 389 | Context=MimeTypes 390 | Size=32 391 | Type=Fixed 392 | 393 | [32x32/places] 394 | Context=Places 395 | Size=32 396 | Type=Fixed 397 | 398 | [32x32/status] 399 | Context=Status 400 | Size=32 401 | Type=Fixed 402 | 403 | [48x48/actions] 404 | Context=Actions 405 | Size=48 406 | Type=Fixed 407 | 408 | [48x48/animations] 409 | Context=Animations 410 | Size=48 411 | Type=Fixed 412 | 413 | [48x48/apps] 414 | Context=Applications 415 | Size=48 416 | Type=Fixed 417 | 418 | [48x48/categories] 419 | Context=Categories 420 | Size=48 421 | Type=Fixed 422 | 423 | [48x48/devices] 424 | Context=Devices 425 | Size=48 426 | Type=Fixed 427 | 428 | [48x48/emblems] 429 | Context=Emblems 430 | Size=48 431 | Type=Fixed 432 | 433 | [48x48/emotes] 434 | Context=Emotes 435 | Size=48 436 | Type=Fixed 437 | 438 | [48x48/mimetypes] 439 | Context=MimeTypes 440 | Size=48 441 | Type=Fixed 442 | 443 | [48x48/places] 444 | Context=Places 445 | Size=48 446 | Type=Fixed 447 | 448 | [48x48/status] 449 | Context=Status 450 | Size=48 451 | Type=Fixed 452 | 453 | [256x256/actions] 454 | Context=Actions 455 | Size=256 456 | MinSize=56 457 | MaxSize=512 458 | Type=Scalable 459 | 460 | [256x256/apps] 461 | Context=Applications 462 | Size=256 463 | MinSize=56 464 | MaxSize=512 465 | Type=Scalable 466 | 467 | [256x256/categories] 468 | Context=Categories 469 | Size=256 470 | MinSize=56 471 | MaxSize=512 472 | Type=Scalable 473 | 474 | [256x256/devices] 475 | Context=Devices 476 | Size=256 477 | MinSize=56 478 | MaxSize=512 479 | Type=Scalable 480 | 481 | [256x256/emblems] 482 | Context=Emblems 483 | Size=256 484 | MinSize=56 485 | MaxSize=512 486 | Type=Scalable 487 | 488 | [256x256/emotes] 489 | Context=Emotes 490 | Size=256 491 | MinSize=56 492 | MaxSize=512 493 | Type=Scalable 494 | 495 | [256x256/mimetypes] 496 | Context=MimeTypes 497 | Size=256 498 | MinSize=56 499 | MaxSize=512 500 | Type=Scalable 501 | 502 | [256x256/places] 503 | Context=Places 504 | Size=256 505 | MinSize=56 506 | MaxSize=512 507 | Type=Scalable 508 | 509 | [256x256/status] 510 | Context=Status 511 | Size=256 512 | MinSize=56 513 | MaxSize=512 514 | Type=Scalable 515 | 516 | -------------------------------------------------------------------------------- /src/mainwindow/playlist/playlistwidget.cpp: -------------------------------------------------------------------------------- 1 | #include "playlistwidget.h" 2 | #include 3 | #include 4 | 5 | /* 6 | * TODO: 7 | * - Fix columns size 8 | * - more columns, like rating 9 | */ 10 | 11 | PlaylistWidget::PlaylistWidget(QWidget *parent, Phonon::MediaObject *mediaObject) : QTreeWidget(parent) 12 | { 13 | setObjectName("PlaylistWidget"); 14 | setColumnCount(4); 15 | 16 | currentSong = NULL; 17 | nextSong = NULL; 18 | 19 | setSelectionMode(QAbstractItemView::ExtendedSelection); 20 | 21 | // Enable drag and drop 22 | setAcceptDrops(true); 23 | setDragEnabled(true); 24 | setDropIndicatorShown(true); 25 | 26 | // Alternating row colors are good 27 | setAlternatingRowColors(true); 28 | 29 | /* 30 | * Connect our local media object to application's mediaObject 31 | * It would be nice to have a global access to it... How could we do that? 32 | */ 33 | mainMediaObject = mediaObject; 34 | 35 | // Define titles for labels 36 | QStringList playlistWidgetHeaders; 37 | playlistWidgetHeaders.append("Track"); 38 | playlistWidgetHeaders.append("Title"); 39 | playlistWidgetHeaders.append("Album"); 40 | playlistWidgetHeaders.append("Artist"); 41 | playlistWidgetHeaders.append("Duration"); 42 | setHeaderLabels(playlistWidgetHeaders); 43 | 44 | // Set alignment for the columns' headers 45 | QHeaderView *headers = header(); 46 | headers->setDefaultAlignment(Qt::AlignCenter); 47 | headers->setSortIndicatorShown(true); 48 | 49 | // Assign signals to add items or Phonon controls 50 | connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(playSong(QTreeWidgetItem*))); 51 | connect(mainMediaObject, SIGNAL(aboutToFinish()), this, SLOT(enqueueNextSong())); 52 | connect(mainMediaObject, SIGNAL(finished()), this, SLOT(removeBold())); 53 | connect(mainMediaObject, SIGNAL(stateChanged(Phonon::State,Phonon::State)), this, SLOT(handleStateChange(Phonon::State))); 54 | connect(mainMediaObject, SIGNAL(currentSourceChanged(Phonon::MediaSource)), this, SLOT(fileChanged())); 55 | } 56 | 57 | void PlaylistWidget::playSong(QTreeWidgetItem *doubleClickedItem) { 58 | PlaylistItem *item = (PlaylistItem*) doubleClickedItem; 59 | if (currentSong != NULL) { 60 | currentSong->removeBold(); 61 | } 62 | currentSong = item; 63 | item->setBold(); 64 | mainMediaObject->setCurrentSource(item->getMusic()->getFileUrl()); 65 | mainMediaObject->play(); 66 | emitSongInformationUpdated(); 67 | } 68 | 69 | void PlaylistWidget::playPreviousSong() { 70 | QModelIndex index = indexAbove(indexFromItem(currentSong)); 71 | if (index.isValid()) { 72 | playSong(itemFromIndex(index)); 73 | } 74 | } 75 | 76 | void PlaylistWidget::playNextSong() { 77 | QModelIndex index = indexBelow(indexFromItem(currentSong)); 78 | if (index.isValid()) { 79 | playSong(itemFromIndex(index)); 80 | } 81 | } 82 | 83 | void PlaylistWidget::enqueueNextSong() { 84 | nextSong = (PlaylistItem*)itemBelow(currentSong); 85 | if (nextSong != NULL) { 86 | mainMediaObject->enqueue(nextSong->getMusic()->getFileUrl()); 87 | } 88 | } 89 | 90 | void PlaylistWidget::removeBold() { 91 | if (currentSong != NULL) { 92 | currentSong->removeBold(); 93 | } 94 | } 95 | 96 | void PlaylistWidget::fileChanged() { 97 | if (nextSong != NULL) { 98 | currentSong->removeBold(); 99 | currentSong = nextSong; 100 | nextSong = NULL; 101 | } 102 | 103 | } 104 | 105 | void PlaylistWidget::handleStateChange(Phonon::State newState) { 106 | if (newState > Phonon::StoppedState) { 107 | currentSong->setBold(); 108 | emitSongInformationUpdated(); 109 | } 110 | else { 111 | if (currentSong != NULL) { 112 | currentSong->removeBold(); 113 | } 114 | } 115 | } 116 | 117 | void PlaylistWidget::emitSongInformationUpdated() { 118 | QMap songInfo; 119 | songInfo.insert("artist", currentSong->getMusic()->getArtist()); 120 | songInfo.insert("title", currentSong->getMusic()->getTitle()); 121 | emit songInformationUpdated(songInfo); 122 | } 123 | 124 | void PlaylistWidget::addSong(PlaylistItem *newItem, int index) { 125 | newItem->index = index; 126 | 127 | if (newItem->isValid()) { 128 | insertValidItem(newItem); 129 | } 130 | else { 131 | delete newItem; 132 | } 133 | } 134 | 135 | void PlaylistWidget::addSong(QUrl url, int index) { 136 | PlaylistItem *newItem = new PlaylistItem(url); 137 | addSong(newItem, index); 138 | } 139 | 140 | void PlaylistWidget::addSong(QList urlList) { 141 | foreach (QUrl url, urlList) { 142 | addSong(url); 143 | } 144 | } 145 | 146 | void PlaylistWidget::insertValidItem(PlaylistItem *newItem) { 147 | if (mainMediaObject->currentSource().type() == Phonon::MediaSource::Empty) { 148 | currentSong = newItem; 149 | mainMediaObject->enqueue(newItem->getMusic()->getFileUrl()); 150 | } 151 | 152 | int index = newItem->index; 153 | if (index == -1) { 154 | addTopLevelItem(newItem); 155 | } 156 | else { 157 | insertTopLevelItem(index, newItem); 158 | } 159 | } 160 | 161 | void PlaylistWidget::deleteInvalidItem(PlaylistItem *invalidItem) { 162 | delete invalidItem; 163 | } 164 | 165 | void PlaylistWidget::addFolder(QUrl url, int &index) { 166 | // You shouldn't call this to add files, man. 167 | if (QFileInfo(url.path()).isFile()) return; 168 | 169 | QDir directory(url.path()); 170 | foreach (QString fileEntry, directory.entryList(directory.AllEntries | directory.NoDotAndDotDot, directory.DirsFirst | directory.Name)) { 171 | // Change file entry to a full path 172 | fileEntry = directory.absolutePath() + directory.separator() + fileEntry; 173 | if (QFileInfo(fileEntry).isDir()) { 174 | addFolder(QUrl(fileEntry), index); 175 | } 176 | else if (QFileInfo(fileEntry).isFile()) { 177 | addSong(QUrl(fileEntry), index); 178 | 179 | /* 180 | * If index is -1, we must append all songs to the end, so there's no need 181 | * to change indexes. But if its >= 0, we must sum +1 to it in order to put 182 | * songs in the right place. 183 | */ 184 | if (index != -1) index++; 185 | } 186 | } 187 | } 188 | 189 | void PlaylistWidget::dragEnterEvent(QDragEnterEvent *event) { 190 | if (event->mimeData()->hasFormat("text/uri-list")) { 191 | event->accept(); 192 | } 193 | else { 194 | event->ignore(); 195 | } 196 | } 197 | 198 | void PlaylistWidget::dragMoveEvent(QDragMoveEvent *event) 199 | { 200 | if (event->mimeData()->hasFormat("text/uri-list")) { 201 | event->accept(); 202 | } 203 | } 204 | 205 | 206 | void PlaylistWidget::mouseMoveEvent(QMouseEvent *event) 207 | { 208 | // if not left button - return 209 | if (!(event->buttons() & Qt::LeftButton)) return; 210 | 211 | // if no item selected, return (else it would crash) 212 | if (currentItem() == NULL) return; 213 | 214 | drag = new QDrag(this); 215 | connect(drag, SIGNAL(actionChanged(Qt::DropAction)), this, SLOT(dndActionChanged(Qt::DropAction))); 216 | QMimeData *mimeData = new QMimeData; 217 | 218 | // construct list of QUrls 219 | // other widgets accept this mime type, we can drop to them 220 | QList list; 221 | 222 | foreach (QTreeWidgetItem *currentItem, selectedItems()) { 223 | PlaylistItem *playlistCurrentItem = static_cast(currentItem); 224 | list.append(playlistCurrentItem->getMusic()->getFileUrl()); 225 | } 226 | 227 | // mime stuff 228 | mimeData->setUrls(list); 229 | drag->setMimeData(mimeData); 230 | 231 | // start drag 232 | QList itemsToRemove = selectedItems(); 233 | drag->exec(Qt::CopyAction | Qt::MoveAction); 234 | } 235 | 236 | 237 | void PlaylistWidget::dropEvent(QDropEvent *event) { 238 | if (event->mimeData()->hasFormat("text/uri-list")) { 239 | /* 240 | * If the dropped objects come from outside, we parse them as 241 | * a list of files, if they are a list of files. 242 | */ 243 | 244 | // TODO: change mouse cursor 245 | 246 | if (event->source() != this) { 247 | QList urlList = event->mimeData()->urls(); 248 | int indexToInsert = indexOfTopLevelItem(itemAt(event->pos())); 249 | foreach (QUrl url, urlList) { 250 | if (!QFileInfo(url.path()).exists()) { 251 | qDebug("FILE DO NOT EXISTS"); 252 | // TODO: emit error; 253 | continue; 254 | } 255 | 256 | // If it's not a dir, add it using addSong 257 | if (QFileInfo(url.path()).isFile()) { 258 | addSong(url, indexToInsert); 259 | } 260 | // If it's a folder, add it recursively 261 | else if (QFileInfo(url.path()).isDir()) { 262 | addFolder(url, indexToInsert); 263 | } 264 | } 265 | } 266 | 267 | /* 268 | * If the dropped objects come from the playlist widget, 269 | * so we are moving the items from list. 270 | */ 271 | else { 272 | QList itemsToInsert; 273 | 274 | // Create the list of items to add... 275 | foreach (QTreeWidgetItem *currentItem, selectedItems()) { 276 | QTreeWidgetItem *playlistCurrentItem = takeTopLevelItem(indexOfTopLevelItem(currentItem)); 277 | itemsToInsert.append(playlistCurrentItem); 278 | } 279 | 280 | // ...and insert them 281 | if (itemsToInsert.size() > 0) { 282 | int indexToInsert = indexOfTopLevelItem(itemAt(event->pos())); 283 | /* 284 | * If the index the cursor is over is -1, there's no item under 285 | * the cursor, so we must add the selected items to the end of 286 | * the list. 287 | */ 288 | if (indexToInsert != -1) { insertTopLevelItems(indexToInsert, itemsToInsert); } 289 | else { addTopLevelItems(itemsToInsert); } 290 | } 291 | } 292 | 293 | } 294 | 295 | else { 296 | QTreeWidget::dropEvent(event); 297 | } 298 | 299 | 300 | 301 | } 302 | 303 | Qt::DropActions PlaylistWidget::supportedDropActions() const 304 | { 305 | return Qt::CopyAction | Qt::MoveAction; 306 | } 307 | 308 | 309 | void PlaylistWidget::dndActionChanged(Qt::DropAction newAction) { 310 | dndAction = newAction; 311 | if (newAction == Qt::MoveAction) { 312 | qDebug("Move action"); 313 | } 314 | else if (newAction == Qt::CopyAction) { 315 | qDebug("Copy action"); 316 | } 317 | } 318 | 319 | void PlaylistWidget::removeSelectedItems() { 320 | if (selectedItems().count() > 0) { 321 | foreach(QTreeWidgetItem *item, selectedItems()) { 322 | // Avoid wrong references 323 | if (currentSong == item) { currentSong = NULL; } 324 | else if (nextSong == item) { 325 | if (itemBelow(nextSong) != NULL) nextSong = (PlaylistItem*)itemBelow(nextSong); 326 | else nextSong = NULL; 327 | } 328 | delete item; 329 | } 330 | } 331 | } 332 | 333 | void PlaylistWidget::selectAll() { 334 | int total = topLevelItemCount(); 335 | for (int i = 0; i < total; i++) { 336 | QTreeWidgetItem *item = topLevelItem(i); 337 | item->setSelected(true); 338 | } 339 | } 340 | 341 | 342 | /* 343 | * Add a context menu to playlist 344 | */ 345 | void PlaylistWidget::contextMenuEvent(QContextMenuEvent *event) { 346 | QMenu menu(this); 347 | menu.addAction("&Select all", this, SLOT(selectAll()), QKeySequence::SelectAll); 348 | menu.addAction("&Remove selected songs", this, SLOT(removeSelectedItems()), QKeySequence::Delete); 349 | menu.exec(event->globalPos()); 350 | 351 | } 352 | 353 | /* 354 | * 355 | * Handle keyboard shortcuts to playlist widget. 356 | * 357 | * QKeySequence::Delete = Removes selected items. 358 | * QKeySequence::SelectAll = Select all 359 | * 360 | * Qt will take the responsabily to assign the key sequences 361 | * to their correspondent keyboard shorcuts, like Ctrl+A or Delete/Backspace 362 | * 363 | */ 364 | void PlaylistWidget::keyPressEvent(QKeyEvent *event) { 365 | if (event->matches(QKeySequence::Delete)) { 366 | event->accept(); 367 | removeSelectedItems(); 368 | } 369 | else if (event->matches(QKeySequence::SelectAll)) { 370 | event->accept(); 371 | selectAll(); 372 | } 373 | else { 374 | event->ignore(); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/mainwindow/mainnotebook/collection/collectiontreewidget.cpp: -------------------------------------------------------------------------------- 1 | #include "collectiontreewidget.h" 2 | #include 3 | #include 4 | 5 | // TODO: cleanup 6 | 7 | /// @brief Constructor 8 | CollectionTreeWidget::CollectionTreeWidget() 9 | { 10 | setColumnCount(1); 11 | header()->hide(); // hide headers 12 | setDragEnabled(true); 13 | setAcceptDrops(true); 14 | setSelectionMode(QAbstractItemView::ExtendedSelection); 15 | 16 | service = new CollectionService(); 17 | 18 | // Add songs that currently exist on database 19 | QSqlTableModel *artistModel = service->artistModel(); 20 | artistModel->select(); 21 | 22 | // TODO: verify if we can put fetchmore() inside the for loop. 23 | // TODO: put this task in background... URGENT 24 | while (artistModel->canFetchMore()) artistModel->fetchMore(); 25 | int total = artistModel->rowCount(); 26 | 27 | for (int i = 0; i < total; i++) { 28 | QString artist = artistModel->record(i).value(artistModel->fieldIndex("name")).toString(); 29 | unsigned int id = artistModel->record(i).value(artistModel->fieldIndex("id")).toInt(); 30 | addArtist(artist, id); 31 | } 32 | delete artistModel; 33 | 34 | /* 35 | * TODO: modify the slots in order to add only the artist, not the music. 36 | * The album and song must be shown only if the node is already expanded. 37 | */ 38 | connect(service, SIGNAL(songAdded(Music*)), this, SLOT(addMusic(Music*))); 39 | connect(service, SIGNAL(songRemoved(unsigned int)), this, SLOT(removeMusic(uint))); 40 | connect(this, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(doubleClickAt(QModelIndex))); 41 | connect(this, SIGNAL(expanded(QModelIndex)), this, SLOT(showChildrenOf(QModelIndex))); 42 | 43 | /* 44 | * We shall emit those signals to show or hide the widget that will 45 | * give the feedback that the collection is being scanned. 46 | */ 47 | connect(service, SIGNAL(scanning()), this, SIGNAL(scanning())); 48 | connect(service, SIGNAL(listUpdated()), this, SIGNAL(listUpdated())); 49 | 50 | // Start service to find new songs and remove the inexistent ones 51 | service->start(QThread::LowestPriority); 52 | } 53 | 54 | QStringList CollectionTreeWidget::toColumns(QString string) { 55 | QStringList columns; 56 | columns.append(string); 57 | columns.append(QString::number(topLevelItemCount() + 1)); 58 | return columns; 59 | } 60 | 61 | void CollectionTreeWidget::showChildrenOf(QModelIndex index) { 62 | CollectionTreeWidgetItem *item = (CollectionTreeWidgetItem*)itemFromIndex(index); 63 | 64 | // If the item pressed was an artist, add albums 65 | if (item->getNodeLevel() == LevelArtist) { 66 | QString artist = item->text(0).toUtf8(); 67 | 68 | // Looks for artist id 69 | QString artistId = QString::number(item->getId()); 70 | 71 | // Looks for artist albums 72 | QSqlTableModel *albumModel = service->albumModel(); 73 | albumModel->setFilter("id_artist = " + artistId); 74 | albumModel->select(); 75 | 76 | while (albumModel->canFetchMore()) albumModel->fetchMore(); 77 | int total = albumModel->rowCount(); 78 | 79 | for (int i = 0; i < total; i++) { 80 | QString album = albumModel->record(i).value(albumModel->fieldIndex("title")).toString(); 81 | unsigned int id = albumModel->record(i).value(albumModel->fieldIndex("id")).toInt(); 82 | addAlbum(artist, album, id); 83 | } 84 | 85 | delete albumModel; 86 | } 87 | // If the item pressed was an album, add songs 88 | else if (item->getNodeLevel() == LevelAlbum) { 89 | QString albumId = QString::number(item->getId()); 90 | 91 | QSqlTableModel *musicModel = service->musicModel(); 92 | musicModel->setFilter("id_album = " + albumId); 93 | musicModel->setSort(musicModel->fieldIndex("track_number"), Qt::AscendingOrder); 94 | musicModel->select(); 95 | 96 | while (musicModel->canFetchMore()) musicModel->fetchMore(); 97 | int total = musicModel->rowCount(); 98 | 99 | for (int i = 0; i < total; i++) { 100 | QString path = musicModel->record(i).value(musicModel->fieldIndex("path")).toString(); 101 | unsigned int id = musicModel->record(i).value(musicModel->fieldIndex("id")).toInt(); 102 | 103 | Music *music = new Music(QUrl(path)); 104 | addMusic(music, id); 105 | delete music; 106 | } 107 | } 108 | 109 | expand(index); 110 | } 111 | 112 | QTreeWidgetItem *CollectionTreeWidget::addArtist(QString artist, unsigned int id) { 113 | if (id == 0) { 114 | QSqlTableModel *model = service->artistModel(); 115 | 116 | // SQLite uses two single quotes to escape a single quote! :) 117 | QString filter = "name = '" + QString(artist).replace("'","''") + "'"; 118 | model->setFilter(filter); 119 | model->select(); 120 | 121 | while (model->canFetchMore()) model->fetchMore(); 122 | int total = model->rowCount(); 123 | if (total > 0) { 124 | id = model->record(0).value(model->fieldIndex("id")).toInt(); 125 | } 126 | else { 127 | qDebug("ERROR: no artist found! -- " + model->filter().toUtf8()); 128 | return NULL; 129 | } 130 | } 131 | 132 | QList artistList = findItems(artist, Qt::MatchExactly, 0); 133 | if (artistList.isEmpty()) { 134 | CollectionTreeWidgetItem *item = new CollectionTreeWidgetItem(LevelArtist, id, (QTreeWidget*)0); 135 | item->setText(0, artist); 136 | item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); 137 | 138 | // Set font to bold 139 | QFont font = item->font(0); 140 | font.setBold(true); 141 | item->setFont(0, font); 142 | 143 | // Set icon 144 | item->setIcon(0, IconFactory::fromTheme("folder")); 145 | 146 | // Insert item 147 | insertTopLevelItem(0, item); 148 | sortItems(0, Qt::AscendingOrder); 149 | 150 | return item; 151 | } 152 | else { 153 | return artistList.first(); 154 | } 155 | } 156 | 157 | bool CollectionTreeWidget::removeArtist(QString artist) { 158 | QList artistList = findItems(artist, Qt::MatchExactly, 0); 159 | if (!artistList.isEmpty()) { 160 | delete artistList.first(); 161 | return true; 162 | } 163 | return false; 164 | } 165 | 166 | 167 | QTreeWidgetItem *CollectionTreeWidget::addAlbum(QString artist, QString album, unsigned int albumId) { 168 | // Find id in database if we don't have it 169 | if (albumId == 0) { 170 | QSqlTableModel *model = service->collectionModel(); 171 | 172 | // SQLite used two single quotes to escape a single quote! :) 173 | QString filter = "artist = '" + QString(artist).replace("'","''") + "' AND " 174 | "album = '" + QString(album).replace("'","''") + "'"; 175 | model->setFilter(filter); 176 | model->select(); 177 | 178 | while (model->canFetchMore()) model->fetchMore(); 179 | int total = model->rowCount(); 180 | if (total > 0) { 181 | albumId = model->record(0).value(model->fieldIndex("id_album")).toInt(); 182 | } 183 | else { 184 | qDebug("ERROR: no album found! -- " + model->filter().toUtf8()); 185 | return NULL; 186 | } 187 | } 188 | 189 | // Looks for the artist 190 | QTreeWidgetItem *newAlbumNode; // The node with the album, whether it exists or not 191 | QTreeWidgetItem *artistItem; 192 | artistItem = addArtist(artist); 193 | 194 | // Look for album 195 | for (int i = 0; i < artistItem->childCount(); i++) { 196 | if (artistItem->child(i)->text(0) == album) { 197 | newAlbumNode = artistItem->child(i); 198 | return newAlbumNode; 199 | } 200 | } 201 | 202 | // Create our new album node and add it if it was not found 203 | newAlbumNode = new CollectionTreeWidgetItem(LevelAlbum, albumId, (QTreeWidget*)0); 204 | newAlbumNode->setText(0, album); 205 | newAlbumNode->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); 206 | 207 | // Set icon 208 | newAlbumNode->setIcon(0, IconFactory::fromTheme("media-cdrom")); 209 | 210 | artistItem->addChild(newAlbumNode); 211 | artistItem->sortChildren(0, Qt::AscendingOrder); 212 | 213 | return newAlbumNode; 214 | } 215 | 216 | 217 | bool CollectionTreeWidget::removeAlbum(QString artist, QString album) { 218 | QList artistList = findItems(artist, Qt::MatchExactly, 0); 219 | // If we have no artist, we have no album. 220 | if (artistList.isEmpty()) { 221 | return false; 222 | } 223 | 224 | // Looks for the artist 225 | QTreeWidgetItem *artistItem = artistList.first(); 226 | // Look for album 227 | for (int i = 0; i < artistItem->childCount(); i++) { 228 | if (artistItem->child(i)->text(0) == album) { 229 | delete artistItem->child(i); 230 | cleanUp(artistItem, CollectionTreeWidget::LevelArtist); 231 | return true; 232 | } 233 | } 234 | return false; 235 | } 236 | 237 | 238 | CollectionTreeWidgetItem *CollectionTreeWidget::addMusic(Music *music, unsigned int id) { 239 | // Find id in database if we don't have it 240 | if (id == 0) { 241 | QSqlTableModel *model = service->collectionModel(); 242 | 243 | // SQLite used two single quotes to escape a single quote! :) 244 | QString filter = "artist = '" + music->getArtist().replace("'","''") + "' AND " 245 | "album = '" + music->getAlbum().replace("'","''") + "' AND " 246 | "music = '" + music->getTitle().replace("'","''") + "'"; 247 | model->setFilter(filter); 248 | model->select(); 249 | 250 | while (model->canFetchMore()) model->fetchMore(); 251 | int total = model->rowCount(); 252 | if (total > 0) { 253 | id = model->record(0).value(model->fieldIndex("id_music")).toInt(); 254 | } 255 | else { 256 | qDebug("ERROR: no songs found! -- " + model->filter().toUtf8()); 257 | return NULL; 258 | } 259 | } 260 | 261 | // Looks for the album 262 | QTreeWidgetItem *albumItem = addAlbum(music->getArtist(), music->getAlbum()); 263 | 264 | // Create our new music node and add it if it was not found 265 | removeMusic(id); 266 | 267 | CollectionTreeWidgetItem *newMusicNode = new CollectionTreeWidgetItem(LevelMusic, id, (QTreeWidget*)0); 268 | newMusicNode->setText(0, music->getTitle()); 269 | newMusicNode->setIcon(0, IconFactory::fromTheme("sound")); 270 | albumItem->addChild(newMusicNode); 271 | musicList.append(newMusicNode); 272 | 273 | return newMusicNode; 274 | } 275 | 276 | bool CollectionTreeWidget::removeMusic(unsigned int id) { 277 | int total = musicList.count(); 278 | 279 | if (total == 0) return false; 280 | for (int i = 0; i < total; i++) { 281 | CollectionTreeWidgetItem *item = musicList[i]; 282 | 283 | if (item->getId() == id) { 284 | CollectionTreeWidgetItem *album = (CollectionTreeWidgetItem*)item->parent(); 285 | musicList.removeAt(i); 286 | delete item; 287 | cleanUp(album, CollectionTreeWidget::LevelAlbum); 288 | return true; 289 | } 290 | } 291 | 292 | return false; 293 | } 294 | 295 | void CollectionTreeWidget::cleanUp(QTreeWidgetItem *parent = NULL, int level = CollectionTreeWidget::LevelNone) { 296 | // If parent is null, process all artists and its children nodes 297 | if (parent == NULL) { 298 | cleanUp(invisibleRootItem(), CollectionTreeWidget::LevelNone); 299 | } 300 | 301 | // if we have a parent, process it 302 | else { 303 | 304 | // If it's an artist, enter it 305 | if (level < CollectionTreeWidget::LevelAlbum) { 306 | int childCount = parent->childCount(); 307 | for (int i = 0; i < childCount; i++) { 308 | TreeLevel newLevel = (TreeLevel)(level + 1); 309 | cleanUp(parent->child(i), newLevel); 310 | } 311 | } 312 | 313 | // If it's an album and it has no songs, remove it! 314 | else { 315 | if (parent->childCount() == 0) { 316 | QTreeWidgetItem *parentParent = parent->parent(); 317 | delete parent; 318 | 319 | // If artist has no more albums, remove it too 320 | if (parentParent->childCount() == 0) delete parentParent; 321 | } 322 | } 323 | } 324 | 325 | } 326 | 327 | 328 | void CollectionTreeWidget::mouseMoveEvent(QMouseEvent *event) { 329 | // if not left button - return 330 | if (!(event->buttons() & Qt::LeftButton)) return; 331 | 332 | QDrag *drag = new QDrag(this); 333 | QMimeData *mimeData = new QMimeData; 334 | 335 | // construct list of QUrls 336 | // other widgets accept this mime type, we can drop to them 337 | QList list; 338 | 339 | foreach (QTreeWidgetItem *currentItem, selectedItems()) { 340 | CollectionTreeWidgetItem *collectionCurrentItem = (CollectionTreeWidgetItem *)currentItem; 341 | list.append(collectionCurrentItem->getUrlList(service->collectionModel())); 342 | } 343 | 344 | // mime stuff 345 | mimeData->setUrls(list); 346 | drag->setMimeData(mimeData); 347 | 348 | // start drag 349 | drag->exec(Qt::CopyAction | Qt::MoveAction); 350 | } 351 | 352 | void CollectionTreeWidget::doubleClickAt(QModelIndex index) { 353 | CollectionTreeWidgetItem *item = (CollectionTreeWidgetItem *)itemFromIndex(index); 354 | if (item->getNodeLevel() == LevelMusic) { 355 | emit askToAddItemToPlaylist(item->getUrlList(service->collectionModel())); 356 | } 357 | } 358 | --------------------------------------------------------------------------------