Auto-install: Open Harmony's settings, themes tab, and simply copy-paste the raw .css file url in Harmony then hit install. For example
Manual install: Simply put the .css in:
Harmonys's themes are simple CSS files so very easy to make! You can see examples here.
Below is a list of the selectors most commonly used. Keep in mind that other undocumented selectors are available, you can find them with the developer tools (Cmd/Ctrl + Alt + i)
#header
the header bar (containing the player)#footer
the footer bar (containing buttons)#sidebar
the sidebar (containing the playlists)#settings
the settings of the app.playlistTitle
a playlist in the sidebar.playlistTitle .icon
a playlist icon in the sidebar.serviceTitle
a service name in the sidebar (above its playlists)#playerControls
the div grouping the player buttons#playerControls .icon
the icons composing the player buttons#playerTrackTitle
the currently playing track title#playerTrackArtist
the currently playing track artist#playerTrackCover
the currently playing track cover#playerProgress
the div containing the buffer & progress bar#playerProgressBar
the progress bar#playerBufferBar
the buffer bar.rectInput
a text input (search included) or select input.rectInput ~ .icon
the icons composing the inputs.listFilter
when in listview, the headers used to filter.listTrack
a track when shown as a list.listTrack:nth-child(even)
one of two tracks when shown a list (to make list effect with 2 colors for even & odd items).coverElement
a track (or album) when shown using Coverview.tracksContainer
a container of tracks.tabItem
a tab in the settings::-webkit-input-placeholder
the text/search input placeholders#specialViewHeader
the header showing informations when in special view (ex: when view artist).icon.icon-play.playing
the icon indicating the playlist currently playing
You can also customize the scrollbar like this:
::-webkit-scrollbar {
width: 7px;
background: #4d4d4d;
}
::-webkit-scrollbar:hover {
background: rgba(0, 0, 0, 0.09);
}
::-webkit-scrollbar-thumb {
background: #666;
background-clip: padding-box;
border: 1px solid #444;
min-height: 10px;
}
::-webkit-scrollbar-thumb:active {
background: rgba(0,0,0,0.61);
}
Auto-install: Open Harmony's settings, plugin tab, and simply enter a github repo or an npm module.
You may have to enable the plugin / connect your account.
I really recommend that you inspire yourself and take snippets from the already available plugins source codes. It'll be much faster/easier than writing everything from scratch.
Once your plugin is done, you may consider publishing it on github with the harmony-plugins
tag. In the future, it will make it easier to reference all the plugins directly in the app or on the website.
If you need any assistance, or would like to see another method or hook to expand the plugin system, do not hesitate to contact me [email protected]
Also as you'll probably need to debug stuff, you can open the devtools by pressing Cmd/Ctrl+Alt+i. Here you can see output of your console.log
and other commands.
A plugin for harmony is a Node.js module. That means that you have to respect the structure of Node.js modules, as explained here. Its name has to start with harmony-
, for example harmony-soundcloud
In your package.json
, you also have to specify the fields pluginName
(the full name of the plugin, without the harmony-) and pluginColor
(an HTML hex color representing the most your plugin).
Optionally, drop your icon as icon.png
in the directory of your plugin.
A plugin is defined by static properties and public functions ( in the class ExamplePlugin
). Functions you put outside this class will be accessible by only your plugin and won't interfere with the rest of the app.
ExamplePlugin.fullName = "Example Plugin"
ExamplePlugin.favsLocation = "example,favs"
ExamplePlugin.isGeneralPlugin = false
ExamplePlugin.scrobbling = true
ExamplePlugin.settings = {
active: false,
specialOption: true
}
ExamplePlugin.contextmenuItems = [
{
title: 'Example item',
fn: () => console.log(tracks)
}
]
ExamplePlugin.settingsItems = [
{
description: 'This is a special option',
type: 'checkbox',
id: 'specialOption'
}
]
class ExamplePLugin {
/*
* Called when the user click the button to login to the service
*
*/
static login (callback) {
}
/*
* Called when the library is refreshed
*
*/
static fetchData () {
return new Promise((resolve, reject) => {
})
}
/*
* Called when the user wants to like a track from this plugin
*
*/
static like (track, callback) {
}
/*
* Called when the user wants to unlikeda track from this plugin
*
*/
static unlike (track, callback) {
}
/*
* Called when the user wants to add tracks to a playlist
*
*/
static addToPlaylist (tracks, playlistId, callback) {
}
/*
* Called when the user wants to remove tracks to a playlist
*
*/
static removeFromPlaylist (tracks, playlistId, callback) {
}
/*
* Called to obtain the stream URL of a specific track
*
*/
static getStreamUrl (track, callback) {
}
/*
* Called when the user starts a global search on this plugin
*
*/
static searchTracks (query, callback) {
}
/*
* Called when a track is played, if scrobbling for the track's plugin is enabled
*
*/
static onTrackPlay (track) {
}
/*
* Called when a track finished playin, if scrobbling for the track's plugin is enabled
*
*/
static onTrackEnded (track) {
}
/**
* Called right after the app started, useful to init stuff
*
*/
static appStarted () {
}
}
module.exports = ExamplePlugin
.fullName
(String)The complete name of your plugin with all the original ponctuation and characters.
.isGeneralPlugin
(Boolean) default: falseShould your plugin be always accessible in every playlists or not ?
As an example, Last.fm is a general plugin while all the others integrated within Harmony are not.
If a plugin is not general, its contextmenuItems
will only show on the tracks from this plugin.
If a plugin is general, its contextmenuItems
will always show, on every tracks from every playlists of the user.
If your plugin is a service bringing tracks to the user, it will almost always be set to false
.
.favsLocation
(String)If your plugin is a service bringing tracks to the user, tracks which can be liked, you must provide the playlist containing these liked tracks.
It is used so once a user like a track, it is immediately added to this playlist.
It is simply composed by the service id followed by a comma and the playlist id. For example for SoundCloud: soundcloud,favs
.scrobbling
(Boolean) default:If your plugin bring tracks, should these tracks be scrobbled to services like Last.fm ?
For example, as SoundCloud contains a lot of remixes & indies song
.color
(String)The color (HTML hexadecimal form) representing the best your plugin. Should be as unique as possible. For now only used in the search, but will be used for more stuff in the future.
.settings
(Object)This contains the default value of your plugin's settings. When the app is reset or started for the first time, these values will be used.
To store user preferences, your plugin must use the settings
object available from everywhere in the app (and by other plugins). For example, local directories to be added to library by the Local file plugin is accessible by settings.local.paths
.
If your plugin needs to be enabled/configured (if it is a service like SoundCloud, Spotify), you must a least define active: false
:
Local.settings = {
active: false
}
You don't need the active
property if your plugin only needs to be installed and no configuration/connection is needed.
.settingsItems
(Array)Here you put the items you want to show in the settings of the app.
These items can be of different type
:
checkbox
textbox
-> you can then set the placeholder
propertyselect
-> along with array options
Here is an example taken from the YouTube plugin:
Youtube.settings = {
active: false,
quality: 'normal',
onlyMusicCategory: true
}
Youtube.settingsItems = [
{
description: 'Only fetch videos with music category',
type: 'checkbox',
id: 'onlyMusicCategory'
},
{
description: 'Playback quality',
type: 'select',
id: 'quality',
options: [{
value: 'lowest', title: 'Lowest'
},{
value: 'normal', title: 'Normal'
},{
value: 'best', title: 'Best'
}]
}
]
.contextmenuItems
(Array)Items you want to show in the context menu of tracks. If your plugin is general, it will show with every tracks. If not, only with the tracks from your plugin.
.loginBtnHtml
(String)Warning: this will soon be deprecated
The HTML code for your login/connection button.
.loginBtnCss
(String)Warning: this will soon be deprecated
The CSS code for your login/connection button.
login
Called when the user click the button to login to the service
Parameters:
callback('stopped' or err or null
): Return 'stopped' if the user cancelled during the login process.
fetchData
Called when the library is refreshed
Returns a promise (see examples)
like
Called when the user wants to like a track from this plugin
Parameters:
track
: a track object/the track to be liked
callback(err or null
)
unlike
Called when the user wants to like a track from this plugin
Parameters:
track
: a track object/the track to be unliked
callback(err or null
)
addToPlaylist
Called when the user wants to add tracks to a playlist
Parameters:
tracks
: array of track objects to be added to the playlist
playlistId
: the id of the playlist to modify
callback(err or null
)
removeFromPlaylist
Called when the user wants to remove tracks from a playlist
Parameters:
tracks
: array of track objects to be added to the playlist
playlistId
: the id of the playlist to modify
callback(err or null
)
getStreamUrl
Called to obtain the stream URL of a specific track
Parameters:
track
: a track object
callback(err or null
, the stream url, track.id)
searchTracks
Called when the user starts a global search on this plugin
Parameters:
query
: the query used to seerch
callback(tracks
, query
): with tracks an array of tracks corresponding to this query
onTrackPlay
Called when a track is played, if scrobbling for the track's plugin is enabled
Parameters:
track
: a track object/the track starting to play
onTrackEnded
Called when a track is played, if scrobbling for the track's plugin is enabled
Parameters:
track
: a track object/the track which ended
appStarted
Called when the app is started, once the basic funcionalities have been loaded. Useful if you need to init things.
track
objectTracks inside Harmony are all described the same way as an object with the following structure:
service: -> **Required** The track origin service eg: 'spotify'
title: -> **Required** The track title
share_url: -> An URL for sharing the track
album: {
name: -> The name of the album
id: -> An unique Id for this album
},
trackNumber: -> If inside an album, the position
artist: {
name: -> The name of the artist
id: -> An unique Id for this artist
},
id: -> **Required** An unique Id for this track
duration: -> The track duration in milliseconds
artwork: -> URL to an artwork
If you are missing some information, assign null
to it.
Every time the user refresh the library, all playlists are removed and the fetchData()
(if it exists) for every services is called.
In your fetchData()
you can add playlists with:
Data.addPlaylist({
service: -> **Required** The playlist origin service eg: 'spotify',
editable: --> Boolean, Can we add/remove tracks from this playlist
title: -> **Required** The playlist title
id: -> **Required** An unique Id for this playlist
icon: -> If you want an icon different from the default one, set it here
artwork: -> (Useless for now) URL to an artwork
tracks: -> An array with the playlist's tracks
})
Harmony makes it easy to implement OAuth login using the oauthLogin
method.
Here is an example of a login
function using OAuth:
static login (callback) {
oauthLogin(oauthUrl, (code) => {
if (!code) return callback('stopped')
auth( code, (err, data) => {
if (err) return callback(err)
settings.exampleplugins.access_token = data.access_token
callback()
})
})
}
oauthUrl
is the oauth connect url to open
auth()
is a function local to your plugin for transforming the returned token into an access token or refresh token.
For now are supported OAuth authentifications returning a token either as a code
or token
parameter along with error
for any errors. If your OAuth returns in a different parameter, get in touch with me.