├── pages
├── i18n
│ └── en-US.po
├── devices
│ └── devices.page.js
├── library
│ ├── library.page.js
│ └── library.style.js
├── playlist
│ ├── playlist.page.js
│ └── playlist.style.js
└── home
│ ├── index.page.js
│ └── index.style.js
├── app-side
├── i18n
│ └── en-US.po
└── index.js
├── setting
├── i18n
│ └── en-US.po
└── index.js
├── utils
├── config
│ ├── client.js
│ ├── device.js
│ └── constants.js
└── storage.js
├── assets
├── band7
│ ├── icon.png
│ ├── next.png
│ ├── play.png
│ ├── arrow.png
│ ├── liked.png
│ ├── pause.png
│ ├── vol_up.png
│ ├── noShuffle.png
│ ├── notLiked.png
│ ├── previous.png
│ ├── shuffle.png
│ ├── vol_down.png
│ ├── player_icon.png
│ ├── next_disabled.png
│ ├── playlistPlay.png
│ └── previous_disabled.png
├── gtr4
│ ├── arrow.png
│ ├── icon.png
│ ├── liked.png
│ ├── next.png
│ ├── pause.png
│ ├── play.png
│ ├── shuffle.png
│ ├── vol_up.png
│ ├── noShuffle.png
│ ├── notLiked.png
│ ├── previous.png
│ ├── vol_down.png
│ ├── player_icon.png
│ ├── playlistPlay.png
│ ├── next_disabled.png
│ └── previous_disabled.png
├── gts3
│ ├── arrow.png
│ ├── icon.png
│ ├── liked.png
│ ├── next.png
│ ├── pause.png
│ ├── play.png
│ ├── shuffle.png
│ ├── vol_up.png
│ ├── noShuffle.png
│ ├── notLiked.png
│ ├── previous.png
│ ├── vol_down.png
│ ├── player_icon.png
│ ├── playlistPlay.png
│ ├── next_disabled.png
│ └── previous_disabled.png
├── gts4
│ ├── arrow.png
│ ├── icon.png
│ ├── liked.png
│ ├── next.png
│ ├── pause.png
│ ├── play.png
│ ├── shuffle.png
│ ├── vol_up.png
│ ├── noShuffle.png
│ ├── notLiked.png
│ ├── previous.png
│ ├── vol_down.png
│ ├── player_icon.png
│ ├── playlistPlay.png
│ ├── next_disabled.png
│ └── previous_disabled.png
├── falcon
│ ├── arrow.png
│ ├── icon.png
│ ├── liked.png
│ ├── next.png
│ ├── pause.png
│ ├── play.png
│ ├── notLiked.png
│ ├── previous.png
│ ├── shuffle.png
│ ├── vol_down.png
│ ├── vol_up.png
│ ├── noShuffle.png
│ ├── next_disabled.png
│ ├── player_icon.png
│ ├── playlistPlay.png
│ └── previous_disabled.png
├── gtr3pro
│ ├── icon.png
│ ├── next.png
│ ├── play.png
│ ├── arrow.png
│ ├── liked.png
│ ├── pause.png
│ ├── shuffle.png
│ ├── vol_up.png
│ ├── noShuffle.png
│ ├── notLiked.png
│ ├── previous.png
│ ├── vol_down.png
│ ├── player_icon.png
│ ├── playlistPlay.png
│ ├── next_disabled.png
│ └── previous_disabled.png
├── common
│ ├── heart-64.png
│ ├── playBase.png
│ ├── arrowBase.png
│ ├── pauseBase.png
│ ├── playlistPlayBase.png
│ ├── Spotify_Icon_RGB_Black.png
│ ├── Spotify_Icon_RGB_Green.png
│ └── Spotify_Icon_RGB_White.png
├── gtr3-trex2
│ ├── icon.png
│ ├── next.png
│ ├── play.png
│ ├── arrow.png
│ ├── liked.png
│ ├── pause.png
│ ├── vol_up.png
│ ├── noShuffle.png
│ ├── notLiked.png
│ ├── previous.png
│ ├── shuffle.png
│ ├── vol_down.png
│ ├── player_icon.png
│ ├── next_disabled.png
│ ├── playlistPlay.png
│ └── previous_disabled.png
├── gts4mini
│ ├── arrow.png
│ ├── icon.png
│ ├── liked.png
│ ├── next.png
│ ├── pause.png
│ ├── play.png
│ ├── vol_up.png
│ ├── notLiked.png
│ ├── play_old.png
│ ├── previous.png
│ ├── shuffle.png
│ ├── vol_down.png
│ ├── noShuffle.png
│ ├── player_icon.png
│ ├── next_disabled.png
│ ├── playlistPlay.png
│ └── previous_disabled.png
└── mi-band7
│ ├── arrow.png
│ ├── icon.png
│ ├── liked.png
│ ├── next.png
│ ├── pause.png
│ ├── play.png
│ ├── vol_up.png
│ ├── notLiked.png
│ ├── previous.png
│ ├── shuffle.png
│ ├── vol_down.png
│ ├── noShuffle.png
│ ├── player_icon.png
│ ├── next_disabled.png
│ ├── playlistPlay.png
│ └── previous_disabled.png
├── shared
├── device-polyfill.js
├── buffer.js
├── global.js
├── logger.js
├── js-module.js
├── defer.js
├── event.js
├── data.js
├── setTimeout.js
├── player.js
├── fs.js
├── promise.js
└── message.js
├── .gitignore
├── app.js
├── README.md
├── app.json
└── LICENCE.md
/pages/i18n/en-US.po:
--------------------------------------------------------------------------------
1 | msgid "example"
2 | msgstr "This is an example in device"
--------------------------------------------------------------------------------
/app-side/i18n/en-US.po:
--------------------------------------------------------------------------------
1 | msgid "example"
2 | msgstr "This is an example in app-side"
--------------------------------------------------------------------------------
/setting/i18n/en-US.po:
--------------------------------------------------------------------------------
1 | msgid "example"
2 | msgstr "This is an example in setting"
--------------------------------------------------------------------------------
/utils/config/client.js:
--------------------------------------------------------------------------------
1 | export const client_id = "";
2 | export const client_secret = "";
3 |
--------------------------------------------------------------------------------
/assets/band7/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/icon.png
--------------------------------------------------------------------------------
/assets/band7/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/next.png
--------------------------------------------------------------------------------
/assets/band7/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/play.png
--------------------------------------------------------------------------------
/assets/gtr4/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/arrow.png
--------------------------------------------------------------------------------
/assets/gtr4/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/icon.png
--------------------------------------------------------------------------------
/assets/gtr4/liked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/liked.png
--------------------------------------------------------------------------------
/assets/gtr4/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/next.png
--------------------------------------------------------------------------------
/assets/gtr4/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/pause.png
--------------------------------------------------------------------------------
/assets/gtr4/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/play.png
--------------------------------------------------------------------------------
/assets/gts3/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/arrow.png
--------------------------------------------------------------------------------
/assets/gts3/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/icon.png
--------------------------------------------------------------------------------
/assets/gts3/liked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/liked.png
--------------------------------------------------------------------------------
/assets/gts3/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/next.png
--------------------------------------------------------------------------------
/assets/gts3/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/pause.png
--------------------------------------------------------------------------------
/assets/gts3/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/play.png
--------------------------------------------------------------------------------
/assets/gts4/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/arrow.png
--------------------------------------------------------------------------------
/assets/gts4/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/icon.png
--------------------------------------------------------------------------------
/assets/gts4/liked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/liked.png
--------------------------------------------------------------------------------
/assets/gts4/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/next.png
--------------------------------------------------------------------------------
/assets/gts4/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/pause.png
--------------------------------------------------------------------------------
/assets/gts4/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/play.png
--------------------------------------------------------------------------------
/assets/band7/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/arrow.png
--------------------------------------------------------------------------------
/assets/band7/liked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/liked.png
--------------------------------------------------------------------------------
/assets/band7/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/pause.png
--------------------------------------------------------------------------------
/assets/band7/vol_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/vol_up.png
--------------------------------------------------------------------------------
/assets/falcon/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/arrow.png
--------------------------------------------------------------------------------
/assets/falcon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/icon.png
--------------------------------------------------------------------------------
/assets/falcon/liked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/liked.png
--------------------------------------------------------------------------------
/assets/falcon/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/next.png
--------------------------------------------------------------------------------
/assets/falcon/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/pause.png
--------------------------------------------------------------------------------
/assets/falcon/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/play.png
--------------------------------------------------------------------------------
/assets/gtr3pro/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/icon.png
--------------------------------------------------------------------------------
/assets/gtr3pro/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/next.png
--------------------------------------------------------------------------------
/assets/gtr3pro/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/play.png
--------------------------------------------------------------------------------
/assets/gtr4/shuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/shuffle.png
--------------------------------------------------------------------------------
/assets/gtr4/vol_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/vol_up.png
--------------------------------------------------------------------------------
/assets/gts3/shuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/shuffle.png
--------------------------------------------------------------------------------
/assets/gts3/vol_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/vol_up.png
--------------------------------------------------------------------------------
/assets/gts4/shuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/shuffle.png
--------------------------------------------------------------------------------
/assets/gts4/vol_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/vol_up.png
--------------------------------------------------------------------------------
/assets/band7/noShuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/noShuffle.png
--------------------------------------------------------------------------------
/assets/band7/notLiked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/notLiked.png
--------------------------------------------------------------------------------
/assets/band7/previous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/previous.png
--------------------------------------------------------------------------------
/assets/band7/shuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/shuffle.png
--------------------------------------------------------------------------------
/assets/band7/vol_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/vol_down.png
--------------------------------------------------------------------------------
/assets/common/heart-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/common/heart-64.png
--------------------------------------------------------------------------------
/assets/common/playBase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/common/playBase.png
--------------------------------------------------------------------------------
/assets/falcon/notLiked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/notLiked.png
--------------------------------------------------------------------------------
/assets/falcon/previous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/previous.png
--------------------------------------------------------------------------------
/assets/falcon/shuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/shuffle.png
--------------------------------------------------------------------------------
/assets/falcon/vol_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/vol_down.png
--------------------------------------------------------------------------------
/assets/falcon/vol_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/vol_up.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/icon.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/next.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/play.png
--------------------------------------------------------------------------------
/assets/gtr3pro/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/arrow.png
--------------------------------------------------------------------------------
/assets/gtr3pro/liked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/liked.png
--------------------------------------------------------------------------------
/assets/gtr3pro/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/pause.png
--------------------------------------------------------------------------------
/assets/gtr3pro/shuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/shuffle.png
--------------------------------------------------------------------------------
/assets/gtr3pro/vol_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/vol_up.png
--------------------------------------------------------------------------------
/assets/gtr4/noShuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/noShuffle.png
--------------------------------------------------------------------------------
/assets/gtr4/notLiked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/notLiked.png
--------------------------------------------------------------------------------
/assets/gtr4/previous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/previous.png
--------------------------------------------------------------------------------
/assets/gtr4/vol_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/vol_down.png
--------------------------------------------------------------------------------
/assets/gts3/noShuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/noShuffle.png
--------------------------------------------------------------------------------
/assets/gts3/notLiked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/notLiked.png
--------------------------------------------------------------------------------
/assets/gts3/previous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/previous.png
--------------------------------------------------------------------------------
/assets/gts3/vol_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/vol_down.png
--------------------------------------------------------------------------------
/assets/gts4/noShuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/noShuffle.png
--------------------------------------------------------------------------------
/assets/gts4/notLiked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/notLiked.png
--------------------------------------------------------------------------------
/assets/gts4/previous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/previous.png
--------------------------------------------------------------------------------
/assets/gts4/vol_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/vol_down.png
--------------------------------------------------------------------------------
/assets/gts4mini/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/arrow.png
--------------------------------------------------------------------------------
/assets/gts4mini/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/icon.png
--------------------------------------------------------------------------------
/assets/gts4mini/liked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/liked.png
--------------------------------------------------------------------------------
/assets/gts4mini/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/next.png
--------------------------------------------------------------------------------
/assets/gts4mini/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/pause.png
--------------------------------------------------------------------------------
/assets/gts4mini/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/play.png
--------------------------------------------------------------------------------
/assets/gts4mini/vol_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/vol_up.png
--------------------------------------------------------------------------------
/assets/mi-band7/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/arrow.png
--------------------------------------------------------------------------------
/assets/mi-band7/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/icon.png
--------------------------------------------------------------------------------
/assets/mi-band7/liked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/liked.png
--------------------------------------------------------------------------------
/assets/mi-band7/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/next.png
--------------------------------------------------------------------------------
/assets/mi-band7/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/pause.png
--------------------------------------------------------------------------------
/assets/mi-band7/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/play.png
--------------------------------------------------------------------------------
/assets/mi-band7/vol_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/vol_up.png
--------------------------------------------------------------------------------
/shared/device-polyfill.js:
--------------------------------------------------------------------------------
1 | import './logger'
2 | import './buffer'
3 | import './setTimeout'
4 | import './promise'
--------------------------------------------------------------------------------
/assets/band7/player_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/player_icon.png
--------------------------------------------------------------------------------
/assets/common/arrowBase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/common/arrowBase.png
--------------------------------------------------------------------------------
/assets/common/pauseBase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/common/pauseBase.png
--------------------------------------------------------------------------------
/assets/falcon/noShuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/noShuffle.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/arrow.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/liked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/liked.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/pause.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/vol_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/vol_up.png
--------------------------------------------------------------------------------
/assets/gtr3pro/noShuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/noShuffle.png
--------------------------------------------------------------------------------
/assets/gtr3pro/notLiked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/notLiked.png
--------------------------------------------------------------------------------
/assets/gtr3pro/previous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/previous.png
--------------------------------------------------------------------------------
/assets/gtr3pro/vol_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/vol_down.png
--------------------------------------------------------------------------------
/assets/gtr4/player_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/player_icon.png
--------------------------------------------------------------------------------
/assets/gtr4/playlistPlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/playlistPlay.png
--------------------------------------------------------------------------------
/assets/gts3/player_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/player_icon.png
--------------------------------------------------------------------------------
/assets/gts3/playlistPlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/playlistPlay.png
--------------------------------------------------------------------------------
/assets/gts4/player_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/player_icon.png
--------------------------------------------------------------------------------
/assets/gts4/playlistPlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/playlistPlay.png
--------------------------------------------------------------------------------
/assets/gts4mini/notLiked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/notLiked.png
--------------------------------------------------------------------------------
/assets/gts4mini/play_old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/play_old.png
--------------------------------------------------------------------------------
/assets/gts4mini/previous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/previous.png
--------------------------------------------------------------------------------
/assets/gts4mini/shuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/shuffle.png
--------------------------------------------------------------------------------
/assets/gts4mini/vol_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/vol_down.png
--------------------------------------------------------------------------------
/assets/mi-band7/notLiked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/notLiked.png
--------------------------------------------------------------------------------
/assets/mi-band7/previous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/previous.png
--------------------------------------------------------------------------------
/assets/mi-band7/shuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/shuffle.png
--------------------------------------------------------------------------------
/assets/mi-band7/vol_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/vol_down.png
--------------------------------------------------------------------------------
/utils/config/device.js:
--------------------------------------------------------------------------------
1 | export const { width: DEVICE_WIDTH, height: DEVICE_HEIGHT } = hmSetting.getDeviceInfo()
2 |
--------------------------------------------------------------------------------
/assets/band7/next_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/next_disabled.png
--------------------------------------------------------------------------------
/assets/band7/playlistPlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/playlistPlay.png
--------------------------------------------------------------------------------
/assets/falcon/next_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/next_disabled.png
--------------------------------------------------------------------------------
/assets/falcon/player_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/player_icon.png
--------------------------------------------------------------------------------
/assets/falcon/playlistPlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/playlistPlay.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/noShuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/noShuffle.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/notLiked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/notLiked.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/previous.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/previous.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/shuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/shuffle.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/vol_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/vol_down.png
--------------------------------------------------------------------------------
/assets/gtr3pro/player_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/player_icon.png
--------------------------------------------------------------------------------
/assets/gtr3pro/playlistPlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/playlistPlay.png
--------------------------------------------------------------------------------
/assets/gtr4/next_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/next_disabled.png
--------------------------------------------------------------------------------
/assets/gts3/next_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/next_disabled.png
--------------------------------------------------------------------------------
/assets/gts4/next_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/next_disabled.png
--------------------------------------------------------------------------------
/assets/gts4mini/noShuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/noShuffle.png
--------------------------------------------------------------------------------
/assets/gts4mini/player_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/player_icon.png
--------------------------------------------------------------------------------
/assets/mi-band7/noShuffle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/noShuffle.png
--------------------------------------------------------------------------------
/assets/mi-band7/player_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/player_icon.png
--------------------------------------------------------------------------------
/utils/config/constants.js:
--------------------------------------------------------------------------------
1 | export const DEFAULT_COLOR = 0xfc6950
2 | export const DEFAULT_COLOR_TRANSPARENT = 0xfeb4a8
3 |
--------------------------------------------------------------------------------
/assets/gtr3-trex2/player_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/player_icon.png
--------------------------------------------------------------------------------
/assets/gtr3pro/next_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/next_disabled.png
--------------------------------------------------------------------------------
/assets/gtr4/previous_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr4/previous_disabled.png
--------------------------------------------------------------------------------
/assets/gts3/previous_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts3/previous_disabled.png
--------------------------------------------------------------------------------
/assets/gts4/previous_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4/previous_disabled.png
--------------------------------------------------------------------------------
/assets/gts4mini/next_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/next_disabled.png
--------------------------------------------------------------------------------
/assets/gts4mini/playlistPlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/playlistPlay.png
--------------------------------------------------------------------------------
/assets/mi-band7/next_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/next_disabled.png
--------------------------------------------------------------------------------
/assets/mi-band7/playlistPlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/playlistPlay.png
--------------------------------------------------------------------------------
/assets/band7/previous_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/band7/previous_disabled.png
--------------------------------------------------------------------------------
/assets/common/playlistPlayBase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/common/playlistPlayBase.png
--------------------------------------------------------------------------------
/assets/falcon/previous_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/falcon/previous_disabled.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/next_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/next_disabled.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/playlistPlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/playlistPlay.png
--------------------------------------------------------------------------------
/assets/gtr3pro/previous_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3pro/previous_disabled.png
--------------------------------------------------------------------------------
/assets/gts4mini/previous_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gts4mini/previous_disabled.png
--------------------------------------------------------------------------------
/assets/mi-band7/previous_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/mi-band7/previous_disabled.png
--------------------------------------------------------------------------------
/assets/common/Spotify_Icon_RGB_Black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/common/Spotify_Icon_RGB_Black.png
--------------------------------------------------------------------------------
/assets/common/Spotify_Icon_RGB_Green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/common/Spotify_Icon_RGB_Green.png
--------------------------------------------------------------------------------
/assets/common/Spotify_Icon_RGB_White.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/common/Spotify_Icon_RGB_White.png
--------------------------------------------------------------------------------
/assets/gtr3-trex2/previous_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melianmiko/ZeppOS-Spotify/HEAD/assets/gtr3-trex2/previous_disabled.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/**
3 | dist/*
4 | npm-debug.log
5 | yarn-debug.log*
6 | yarn-error.log*
7 | yarn.lock
8 | package-lock.json
9 | selenium-debug.log
10 | .idea
11 | .vscode
12 | *.suo
13 | *.ntvs*
14 | *.njsproj
15 | *.sln
--------------------------------------------------------------------------------
/shared/buffer.js:
--------------------------------------------------------------------------------
1 | import { getGlobal } from './global'
2 |
3 | let globalNS = getGlobal()
4 |
5 | if (!globalNS.Buffer) {
6 | if (typeof Buffer !== 'undefined') {
7 | globalNS.Buffer = Buffer
8 | } else {
9 | globalNS.Buffer = DeviceRuntimeCore.Buffer
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/shared/global.js:
--------------------------------------------------------------------------------
1 |
2 | export function getGlobal () {
3 | if (typeof self !== 'undefined') {
4 | return self
5 | }
6 | if (typeof window !== 'undefined') {
7 | return window
8 | }
9 | if (typeof global !== 'undefined') {
10 | return global
11 | }
12 | if (typeof globalThis !== 'undefined') {
13 | return globalThis
14 | }
15 |
16 | throw new Error('unable to locate global object')
17 | }
18 |
--------------------------------------------------------------------------------
/shared/logger.js:
--------------------------------------------------------------------------------
1 | import { getGlobal } from './global'
2 |
3 | let globalNS = getGlobal()
4 |
5 | if (!globalNS.Logger) {
6 | if (typeof DeviceRuntimeCore !== 'undefined') {
7 | globalNS.Logger = DeviceRuntimeCore.HmLogger
8 | } else {
9 | if (typeof Logger !== 'undefined') {
10 |
11 | }
12 | // console.connect = () => {
13 | // // pass
14 | // }
15 | // globalNS.Logger = {
16 | // getLogger() {
17 | // return console
18 | // },
19 | // }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/pages/devices/devices.page.js:
--------------------------------------------------------------------------------
1 | import { DEVICE_HEIGHT, DEVICE_WIDTH } from "../../utils/config/device";
2 | //import { getStyles } from "./devices.style";
3 |
4 | //const styles = getStyles(hmSetting.getDeviceInfo().deviceName);
5 | const { messageBuilder } = getApp()._options.globalData;
6 | const vibrate = hmSensor.createSensor(hmSensor.id.VIBRATE);
7 |
8 | Page({
9 | onInit(params) {},
10 | build() {
11 | hmUI.updateStatusBarTitle("devices");
12 | },
13 | onDestroy() {
14 | vibrate && vibrate.stop();
15 | hmUI.setStatusBarVisible(true);
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/shared/js-module.js:
--------------------------------------------------------------------------------
1 | export function isHmUIDefined() {
2 | return typeof hmUI !== 'undefined'
3 | }
4 |
5 | export function isHmBleDefined() {
6 | return typeof hmBle !== 'undefined'
7 | }
8 |
9 | export function isHmTimerDefined() {
10 | return typeof timer !== 'undefined'
11 | }
12 |
13 | export function isHmFsDefined() {
14 | return typeof hmFS !== 'undefined'
15 | }
16 |
17 | export function isHmAppDefined() {
18 | return typeof hmApp !== 'undefined'
19 | }
20 |
21 | export function isHmSensorDefined() {
22 | return typeof hmSensor !== 'undefined'
23 | }
24 |
25 | export function isHmSettingDefined() {
26 | return typeof hmSetting !== 'undefined'
27 | }
28 |
--------------------------------------------------------------------------------
/shared/defer.js:
--------------------------------------------------------------------------------
1 | export function Deferred() {
2 | const defer = {}
3 |
4 | defer.promise = new Promise(function (resolve, reject) {
5 | defer.resolve = resolve
6 | defer.reject = reject
7 | })
8 |
9 | return defer
10 | }
11 |
12 | export function delay(ms) {
13 | const defer = Deferred()
14 |
15 | setTimeout(defer.resolve, ms)
16 |
17 | return defer.promise
18 | }
19 |
20 | export function timeout(ms, cb) {
21 | const defer = Deferred()
22 | ms = ms || 1000
23 |
24 | const wait = setTimeout(() => {
25 | clearTimeout(wait)
26 |
27 | if (cb) {
28 | cb && cb(defer.resolve, defer.reject)
29 | } else {
30 | defer.reject('Timed out in ' + ms + 'ms.')
31 | }
32 | }, ms)
33 |
34 | return defer.promise
35 | }
36 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | import "./shared/device-polyfill";
2 | import { MessageBuilder } from "./shared/message";
3 | import { PlayerControl } from "./shared/player";
4 |
5 | const appId = 1017560;
6 | const messageBuilder = new MessageBuilder({ appId });
7 | const playerControl = new PlayerControl(messageBuilder);
8 |
9 | App({
10 | globalData: {
11 | messageBuilder: messageBuilder,
12 | playerControl: playerControl,
13 | },
14 | onCreate(options) {
15 | console.log("app on create invoke");
16 | messageBuilder.connect();
17 | playerControl.connect();
18 | hmSetting.setBrightScreen(60);
19 | // constantly refresh player info
20 | timer.createTimer(10, 10000, () => {
21 | playerControl.update();
22 | });
23 | },
24 |
25 | onDestroy(options) {
26 | console.log("app on destroy invoke");
27 | hmSetting.setBrightScreenCancel();
28 | messageBuilder.disConnect();
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/shared/event.js:
--------------------------------------------------------------------------------
1 | export class EventBus {
2 | constructor() {
3 | this.map = new Map()
4 | }
5 |
6 | on(type, cb) {
7 | if (this.map.has(type)) {
8 | this.map.get(type).push(cb)
9 | } else {
10 | this.map.set(type, [cb])
11 | }
12 | }
13 |
14 | off(type, cb) {
15 | if (type) {
16 | if (cb) {
17 | const cbs = this.map.get(type)
18 |
19 | if (!cbs) return
20 | const index = cbs.findIndex((i) => i === cb)
21 |
22 | if (index >= 0) {
23 | cbs.splice(index, 1)
24 | }
25 | } else {
26 | this.map.delete(type)
27 | }
28 | } else {
29 | this.map.clear()
30 | }
31 | }
32 |
33 | emit(type, ...args) {
34 | for (let cb of(this.map.get(type) ? this.map.get(type) : [])) {
35 | cb && cb(...args)
36 | }
37 | }
38 |
39 | count(type) {
40 | return this.map.get(type) ? this.map.get(type).length : 0
41 | }
42 | }
--------------------------------------------------------------------------------
/shared/data.js:
--------------------------------------------------------------------------------
1 | export function json2Buf(json) {
2 | return str2buf(json2str(json))
3 | }
4 |
5 | export function len(binOrBuf) {
6 | return binOrBuf.byteLength
7 | }
8 |
9 | export function buf2Json(buf) {
10 | return str2json(buf2str(buf))
11 | }
12 |
13 | export function str2json(str) {
14 | return JSON.parse(str)
15 | }
16 |
17 | export function json2str(json) {
18 | return JSON.stringify(json)
19 | }
20 |
21 | export function str2buf(str) {
22 | return Buffer.from(str, 'utf-8')
23 | }
24 |
25 | export function buf2str(buf) {
26 | return buf.toString('utf-8')
27 | }
28 |
29 | export function bin2buf(bin) {
30 | return Buffer.from(bin)
31 | }
32 |
33 | export function buf2bin(buf) {
34 | return buf.buffer
35 | }
36 |
37 | export function buf2hex(buf) {
38 | return buf.toString('hex')
39 | }
40 |
41 | export function bin2hex(bin) {
42 | return buf2hex(bin2buf(bin))
43 | }
44 |
45 | export function bin2json(bin) {
46 | return buf2Json(bin2buf(bin))
47 | }
48 |
49 | export function bin2str(bin) {
50 | return buf2str(bin2buf(bin))
51 | }
52 |
53 | export function str2bin(str) {
54 | return buf2bin(str2buf(str))
55 | }
56 |
57 | export function allocOfBin(size = 0) {
58 | return Buffer.alloc(size).buffer
59 | }
60 |
61 | export function allocOfBuf(size = 0) {
62 | return Buffer.alloc(size)
63 | }
64 |
--------------------------------------------------------------------------------
/utils/storage.js:
--------------------------------------------------------------------------------
1 | function str2ab(str) {
2 | const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char
3 | const bufView = new Uint16Array(buf);
4 | for (let i = 0, strLen = str.length; i < strLen; i++) {
5 | bufView[i] = str.charCodeAt(i);
6 | }
7 | return buf;
8 | }
9 |
10 | export default class LocalStorage {
11 | constructor(fileName = "") {
12 | this.fileName = fileName;
13 | this.contentObj = {};
14 | }
15 |
16 | set(obj) {
17 | const file = hmFS.open(this.fileName, hmFS.O_RDWR | hmFS.O_TRUNC);
18 | const contentBuffer = str2ab(JSON.stringify(obj));
19 |
20 | hmFS.write(file, contentBuffer, 0, contentBuffer.byteLength);
21 | hmFS.close(file);
22 | }
23 |
24 | get() {
25 | const [fsStat, err] = hmFS.stat(this.fileName);
26 | if (err === 0) {
27 | const { size } = fsStat;
28 | const fileContentUnit = new Uint16Array(new ArrayBuffer(size));
29 | const file = hmFS.open(this.fileName, hmFS.O_RDONLY | hmFS.O_CREAT);
30 | hmFS.seek(file, 0, hmFS.SEEK_SET);
31 | hmFS.read(file, fileContentUnit.buffer, 0, size);
32 | hmFS.close(file);
33 |
34 | try {
35 | const val = String.fromCharCode.apply(null, fileContentUnit);
36 | this.contentObj = val ? JSON.parse(val) : {};
37 | } catch (error) {
38 | this.contentObj = {};
39 | }
40 | }
41 |
42 | return this.contentObj;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/shared/setTimeout.js:
--------------------------------------------------------------------------------
1 | import { isHmTimerDefined } from './js-module'
2 | import { getGlobal } from './global'
3 |
4 | let globalNS = getGlobal()
5 |
6 | if (typeof setTimeout === 'undefined' && isHmTimerDefined()) {
7 | globalNS.clearTimeout = function clearTimeout(timerRef) {
8 | timerRef && timer.stopTimer(timerRef)
9 | }
10 |
11 | globalNS.setTimeout = function setTimeout(func, ns) {
12 | const timer1 = timer.createTimer(
13 | ns || 1,
14 | Number.MAX_SAFE_INTEGER,
15 | function () {
16 | globalNS.clearTimeout(timer1)
17 | func && func()
18 | },
19 | {},
20 | )
21 |
22 | return timer1
23 | }
24 |
25 | globalNS.clearImmediate = function clearImmediate(timerRef) {
26 | timerRef && timer.stopTimer(timerRef)
27 | }
28 |
29 | globalNS.setImmediate = function setImmediate(func) {
30 | const timer1 = timer.createTimer(
31 | 1,
32 | Number.MAX_SAFE_INTEGER,
33 | function () {
34 | globalNS.clearImmediate(timer1)
35 | func && func()
36 | },
37 | {},
38 | )
39 |
40 | return timer1
41 | }
42 |
43 | globalNS.clearInterval = function clearInterval(timerRef) {
44 | timerRef && timer.stopTimer(timerRef)
45 | }
46 |
47 | globalNS.setInterval = function setInterval(func, ms) {
48 | const timer1 = timer.createTimer(
49 | 1,
50 | ms,
51 | function () {
52 | func && func()
53 | },
54 | {},
55 | )
56 |
57 | return timer1
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pages/library/library.page.js:
--------------------------------------------------------------------------------
1 | import { DEVICE_HEIGHT, DEVICE_WIDTH } from "../../utils/config/device";
2 | import { getStyles } from "./library.style";
3 |
4 | const styles = getStyles(hmSetting.getDeviceInfo().deviceName);
5 | const { messageBuilder } = getApp()._options.globalData;
6 |
7 | Page({
8 | state: {},
9 | build() {
10 | hmUI.updateStatusBarTitle("Library");
11 |
12 | hmUI.createWidget(hmUI.widget.TEXT, {
13 | ...styles.LIBRARY_HEADER,
14 | text: "Playlists",
15 | });
16 |
17 | this.getAllPlaylists();
18 | },
19 | getAllPlaylists() {
20 | messageBuilder
21 | .request({
22 | func: "getAllPlaylists",
23 | })
24 | .then((data) => {
25 | const { playLists = [] } = data;
26 |
27 | playLists.forEach((playList, i) => {
28 | const { name = "", id = "" } = playList;
29 | const widget = hmUI.createWidget(hmUI.widget.TEXT, {
30 | y: px(DEVICE_HEIGHT * 0.36 + 60 * i),
31 | text: name,
32 | ...styles.TITLE,
33 | });
34 |
35 | const img = hmUI.createWidget(hmUI.widget.IMG, {
36 | x: DEVICE_WIDTH - 36,
37 | y: px(DEVICE_HEIGHT * 0.36 + 60 * i),
38 | src: "arrow.png",
39 | });
40 | img.addEventListener(hmUI.event.CLICK_DOWN, () => {
41 | hmApp.gotoPage({
42 | url: "pages/playlist/playlist.page",
43 | param: JSON.stringify({
44 | name: name,
45 | playlistId: id,
46 | }),
47 | });
48 | });
49 | });
50 | });
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/pages/playlist/playlist.page.js:
--------------------------------------------------------------------------------
1 | import { DEVICE_HEIGHT, DEVICE_WIDTH } from "../../utils/config/device";
2 | import { getStyles } from "./playlist.style";
3 |
4 | const styles = getStyles(hmSetting.getDeviceInfo().deviceName);
5 | const { messageBuilder } = getApp()._options.globalData;
6 | const vibrate = hmSensor.createSensor(hmSensor.id.VIBRATE);
7 |
8 | Page({
9 | state: {
10 | name: "",
11 | playlistId: "",
12 | songList: [],
13 | },
14 | onInit(params) {
15 | const paramsObj = JSON.parse(params);
16 | const { name = "", playlistId = "" } = paramsObj;
17 | (this.state.name = name), (this.state.playlistId = playlistId);
18 | hmUI.setStatusBarVisible(false);
19 | },
20 | build() {
21 | hmUI.updateStatusBarTitle(this.state.name);
22 | //const isVertical = true;
23 | //hmUI.setScrollView(true, DEVICE_HEIGHT, 12, isVertical);
24 |
25 | const playBtn = hmUI.createWidget(hmUI.widget.IMG, {
26 | ...styles.PLAY_BUTTON,
27 | });
28 | playBtn.addEventListener(hmUI.event.CLICK_DOWN, () => {
29 | vibrate.stop();
30 | vibrate.scene = 23;
31 | this.startPlaylist();
32 | vibrate.start();
33 | });
34 |
35 | hmUI.createWidget(hmUI.widget.TEXT, {
36 | ...styles.PLAYLIST_TITLE,
37 | text: this.state.name,
38 | });
39 |
40 | this.getPlaylistsInfo();
41 | },
42 | getPlaylistsInfo() {
43 | messageBuilder
44 | .request({
45 | func: "playlistInfo",
46 | playlistId: this.state.playlistId,
47 | })
48 | .then((data) => {
49 | const { songList = [] } = data;
50 |
51 | songList.forEach((track, i) => {
52 | const { name = "", artistNames = "" } = track;
53 | hmUI.createWidget(hmUI.widget.TEXT, {
54 | ...styles.SONG,
55 | y: px(DEVICE_HEIGHT * 0.53 + 55 * i),
56 | text: name,
57 | });
58 |
59 | hmUI.createWidget(hmUI.widget.TEXT, {
60 | ...styles.ARTIST,
61 | y: px(DEVICE_HEIGHT * 0.6 + 55 * i),
62 | text: artistNames,
63 | });
64 | });
65 | });
66 | },
67 | startPlaylist() {
68 | messageBuilder
69 | .request({
70 | func: "startPlaylist",
71 | playlistId: this.state.playlistId,
72 | })
73 | .then((data) => {
74 | console.log(data);
75 | });
76 | },
77 | onDestroy() {
78 | vibrate && vibrate.stop();
79 | hmUI.setStatusBarVisible(true);
80 | },
81 | });
82 |
--------------------------------------------------------------------------------
/shared/player.js:
--------------------------------------------------------------------------------
1 | const toggle = {
2 | play: "pause",
3 | pause: "play",
4 | like: "notLike",
5 | notLike: "like",
6 | shuffle: "noShuffle",
7 | noShuffle: "shuffle",
8 | };
9 |
10 | export class PlayerControl {
11 | constructor(messageBuilder) {
12 | this.messageBuilder = messageBuilder;
13 | this.player = {
14 | isPlaying: false, // this is temporary until the music sensor is fixed
15 | };
16 | this.song = "";
17 | this.artist = "";
18 | this.playState = "play";
19 | this.likeState = "notLiked";
20 | this.shuffleState = "noShuffle";
21 | this.songId = "";
22 | this.queue = [];
23 | this.progress = 0;
24 | this.devices = [];
25 | }
26 |
27 | connect() {
28 | /*this.player.addEventListener(hmSensor.event.CHANGE, function () {
29 | this.song = this.player.song;
30 | this.artist = this.player.artist;
31 | this.playbackState = this.player.playbackState;
32 | });*/
33 | }
34 |
35 | play() {
36 | if (this.player.isPlaying) {
37 | // foo
38 | return;
39 | }
40 |
41 | this.messageBuilder.request({
42 | func: "player",
43 | method: toggle[this.playState],
44 | });
45 |
46 | this.playState = toggle[this.playState];
47 | }
48 |
49 | next() {
50 | if (this.player.isPlaying) {
51 | // foo
52 | return;
53 | }
54 |
55 | this.messageBuilder.request({
56 | func: "player",
57 | method: "next",
58 | });
59 | this.update();
60 | }
61 |
62 | previous() {
63 | if (this.player.isPlaying) {
64 | // foo
65 | return;
66 | }
67 |
68 | this.messageBuilder.request({
69 | func: "player",
70 | method: "previous",
71 | });
72 | this.update();
73 | }
74 |
75 | toggleLike() {
76 | if (this.likeState == "notLiked") {
77 | this.likeState = "liked";
78 | this.messageBuilder.request({
79 | func: "tracks",
80 | method: "liked",
81 | curSongId: this.songId,
82 | });
83 | return;
84 | }
85 |
86 | this.likeState = "notLiked";
87 | this.messageBuilder.request({
88 | func: "tracks",
89 | method: "notLiked",
90 | curSongId: this.songId,
91 | });
92 | }
93 |
94 | toggleShuffle() {
95 | this.messageBuilder.request({
96 | func: "player",
97 | method: "shuffle",
98 | args: `state=${this.shuffleState != "shuffle"}`,
99 | });
100 | this.shuffleState = toggle[this.shuffleState];
101 | }
102 |
103 | update() {
104 | this.messageBuilder
105 | .request({
106 | func: "player",
107 | })
108 | .then((data) => {
109 | const {
110 | songName = "No content playing",
111 | artistNames = "check if any device is streaming",
112 | isPlaying = false,
113 | isLiked = false,
114 | isShuffled = false,
115 | progress = 0,
116 | songId = "",
117 | queue = [],
118 | } = data;
119 |
120 | if (!this.player.isPlaying) {
121 | this.song = songName;
122 | this.artist = artistNames;
123 | isPlaying ? (this.playState = "play") : (this.playState = "pause");
124 | }
125 |
126 | this.songId = songId;
127 | this.progress = progress;
128 | this.queue = queue;
129 | isLiked ? (this.likeState = "liked") : (this.likeState = "notLiked");
130 | isShuffled
131 | ? (this.shuffleState = "shuffle")
132 | : (this.shuffleState = "noShuffle");
133 | });
134 | }
135 |
136 | disconnect() {}
137 | }
138 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ZeppOS-Spotify
2 | Spotify manager for ZeppOS using Console api calls
3 |
4 | 
5 | 
6 |
7 | # How to use
8 |
9 | ## Requirements
10 |
11 | * [Node.js](https://youtu.be/MrJkkG-yt7A?t=23)
12 | * [ZeusCLI](https://docs.zepp.com/docs/guides/tools/cli/#installing-the-zeus-cli)
13 | * __Spotify premium account__
14 | * The zepp account you are using must be directly registered (no google, mi fit, etc login).
15 |
16 | ## Installation process
17 |
18 | #### 1. Go to [Spotify dashboard for developers](https://developer.spotify.com/dashboard/applications) and select "create an app".
19 |
20 | #### 2. Click on "Show client secret" to see both your client id and secret.
21 | 
22 |
23 | #### 3. Select Edit Settings, then paste the following URI as shown on 'Redirect URIs' then save
24 | `http://zepp-os-staging.zepp.com/app-settings/v1.0.0/index.html?appId=1017560`
25 |
26 | 
27 | 
28 | 
29 |
30 | #### 4. Download the repository as a zip file
31 | 
32 |
33 | #### 5. Extract it's folder to the desktop
34 | 
35 |
36 | #### 6. From the main directory go to utils, then config, right click on client.js and open it with any text editor
37 | 
38 |
39 | #### 7. Paste your `client id` and `client secret` accordingly then save the file
40 | 
41 |
42 | #### 8. Go back to the main directory, then right click on any empty space to open Windows Terminal
43 | 
44 |
45 | #### 9. Run `zeus preview` on the terminal, and select the device to build for
46 | 
47 |
48 | #### 8. Enable developer mode on the Zepp App then scan the generated QR, go to the app settings to log in
49 | 
50 |
51 | #### If you see your user info on screen you are all set, enjoy the app!
52 |
53 | Important note: the log in process may sometimes fail retrieving user information, if this happens to you log out then log in again
54 |
55 | (if you see User: fail you are also logged, it's just the user info that's displayed wrongly)
56 |
57 | # More screenshots
58 |
59 | 
60 | 
61 | 
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/shared/fs.js:
--------------------------------------------------------------------------------
1 | const logger = DeviceRuntimeCore.HmLogger.getLogger('fs.js')
2 |
3 | function ab2str(buf) {
4 | return String.fromCharCode.apply(null, new Uint16Array(buf))
5 | }
6 |
7 | function str2ab(str) {
8 | var buf = new ArrayBuffer(str.length * 2) // 2 bytes for each char
9 | var bufView = new Uint16Array(buf)
10 | for (var i = 0, strLen = str.length; i < strLen; i++) {
11 | bufView[i] = str.charCodeAt(i)
12 | }
13 | return buf
14 | }
15 |
16 | /**
17 | * Get metadata of a file.
18 | * @param {} filename
19 | * @returns
20 | */
21 | export function statSync(filename) {
22 | logger.log('statSync',filename)
23 | //获取文件信息
24 | const [fs_stat, err] = hmFS.stat(filename)
25 | logger.log('res',fs_stat, err)
26 | if (err == 0) {
27 | logger.log('fs--->size:', fs_stat.size)
28 | return fs_stat
29 | } else {
30 | logger.log('fs--->err:', err)
31 | return null
32 | }
33 | }
34 |
35 | /**
36 | * Write data to a file in a single operation. If a file with that name already exists, it is overwritten; otherwise, a new file is created.
37 | * @param {*} filename
38 | * @param {*} data
39 | * @param {*} options
40 | */
41 | export function writeFileSync(filename, data, options) {
42 | logger.log('writeFileSync begin -->', filename)
43 |
44 | const stringBuffer = str2ab(data)
45 | const source_buf = new Uint8Array(stringBuffer)
46 |
47 | //打开/创建文件
48 | const file = hmFS.open(filename, hmFS.O_CREAT | hmFS.O_RDWR | hmFS.O_TRUNC)
49 | logger.log('writeFileSync file open success -->', file)
50 | //定位到文件开始位置
51 | hmFS.seek(file, 0, hmFS.SEEK_SET)
52 | //写入buffer
53 | hmFS.write(file, source_buf.buffer, 0, source_buf.length)
54 | //关闭文件
55 | hmFS.close(file)
56 | logger.log('writeFileSync success -->', filename)
57 | }
58 |
59 | /**
60 | * Read an entire file into a buffer in a single operation.
61 | * @param {*} filename
62 | * @param {*} options
63 | * @returns
64 | */
65 | export function readFileSync(filename, options) {
66 | logger.log('readFileSync fiename:', filename)
67 |
68 | const fs_stat = statSync(filename)
69 | if (!fs_stat) return undefined
70 |
71 | const destination_buf = new Uint8Array(fs_stat.size)
72 | //打开/创建文件
73 | const file = hmFS.open(filename, hmFS.O_RDONLY)
74 | //定位到文件开始位置
75 | hmFS.seek(file, 0, hmFS.SEEK_SET)
76 | //读取buffer
77 | hmFS.read(file, destination_buf.buffer, 0, fs_stat.size)
78 | //关闭文件
79 | hmFS.close(file)
80 |
81 | const content = ab2str(destination_buf.buffer)
82 | //读取结果打印
83 | logger.log('readFileSync', content)
84 | return content
85 | }
86 |
87 | /**
88 | * Delete a file.
89 | * @param {*} filename
90 | */
91 | export function unlinkSync(filename) {
92 | logger.log('unlinkSync begin -->', filename)
93 | const result = hmFS.remove(filename)
94 | logger.log('unlinkSync result -->', result)
95 | return result
96 | }
97 |
98 | /**
99 | * Rename a file.
100 | * @param {*} filename
101 | */
102 | export function renameSync(oldFilename, newFilename) {
103 | logger.log('renameSync begin -->', filename)
104 | hmFS.rename(oldFilename, newFilename)
105 | logger.log('renameSync success -->', filename)
106 | }
107 |
108 | /**
109 | * Synchronously creates a directory.
110 | * @param {*} path
111 | * @param {*} options
112 | */
113 | export function mkdirSync(path, options) {
114 | logger.log('mkdirSync begin -->', path)
115 | hmFS.mkdir(path)
116 | logger.log('mkdirSync success -->', path)
117 | }
118 |
119 | /**
120 | * Reads the contents of the directory.
121 | * @param {*} path
122 | * @param {*} options
123 | */
124 | export function readdirSync(path, options) {
125 | logger.log('readdirSync begin -->', path)
126 | hmFS.readdirSync(path)
127 | logger.log('readdirSync success -->', path)
128 | }
129 |
130 | /**
131 | * Just to test the fs module
132 | */
133 | export function test(fileName, dataString) {
134 | logger.log('saveData begin')
135 |
136 | writeFileSync(fileName, dataString)
137 |
138 | logger.log('fs_writeFileSync -> ', dataString)
139 |
140 | const content = readFileSync(fileName)
141 |
142 | logger.log('fs_readFileSync -> ', content)
143 | }
144 |
--------------------------------------------------------------------------------
/pages/library/library.style.js:
--------------------------------------------------------------------------------
1 | const deviceInfo = hmSetting.getDeviceInfo();
2 | const {
3 | width: DEVICE_WIDTH,
4 | height: DEVICE_HEIGHT,
5 | screenShape,
6 | deviceName,
7 | } = deviceInfo;
8 |
9 | const LIBRARY_HEADER = {
10 | band7: {
11 | x: 0,
12 | y: px(DEVICE_HEIGHT * 0.15),
13 | w: px(DEVICE_WIDTH),
14 | h: px(48),
15 | color: 0x1fdf64,
16 | text_size: px(32),
17 | align_h: hmUI.align.CENTER_H,
18 | align_v: hmUI.align.CENTER_V,
19 | text_style: hmUI.text_style.NONE,
20 | },
21 | gts4mini: {
22 | x: 0,
23 | y: px(DEVICE_HEIGHT * 0.15),
24 | w: px(DEVICE_WIDTH),
25 | h: px(48),
26 | color: 0x1fdf64,
27 | text_size: px(32),
28 | align_h: hmUI.align.CENTER_H,
29 | align_v: hmUI.align.CENTER_V,
30 | text_style: hmUI.text_style.NONE,
31 | },
32 | gts3: {
33 | x: 0,
34 | y: px(DEVICE_HEIGHT * 0.15),
35 | w: px(DEVICE_WIDTH),
36 | h: px(52),
37 | color: 0x1fdf64,
38 | text_size: px(42),
39 | align_h: hmUI.align.CENTER_H,
40 | align_v: hmUI.align.CENTER_V,
41 | text_style: hmUI.text_style.NONE,
42 | },
43 | gtr3: {
44 | x: 0,
45 | y: px(DEVICE_HEIGHT * 0.15),
46 | w: px(DEVICE_WIDTH),
47 | h: px(52),
48 | color: 0x1fdf64,
49 | text_size: px(42),
50 | align_h: hmUI.align.CENTER_H,
51 | align_v: hmUI.align.CENTER_V,
52 | text_style: hmUI.text_style.NONE,
53 | },
54 | gts4: {
55 | x: 0,
56 | y: px(DEVICE_HEIGHT * 0.15),
57 | w: px(DEVICE_WIDTH),
58 | h: px(52),
59 | color: 0x1fdf64,
60 | text_size: px(42),
61 | align_h: hmUI.align.CENTER_H,
62 | align_v: hmUI.align.CENTER_V,
63 | text_style: hmUI.text_style.NONE,
64 | },
65 | };
66 |
67 | const TITLE = {
68 | band7: {
69 | x: 0,
70 | //y: px(DEVICE_HEIGHT * 0.35 + 60 * i),
71 | w: px(DEVICE_WIDTH),
72 | h: px(36),
73 | color: 0xffffff,
74 | text_size: px(24),
75 | align_h: hmUI.align.LEFT,
76 | align_v: hmUI.align.CENTER_V,
77 | text_style: hmUI.text_style.ELLIPSIS,
78 | //text: name,
79 | },
80 | gts4mini: {
81 | x: 0,
82 | //y: px(DEVICE_HEIGHT * 0.35 + 60 * i),
83 | w: px(DEVICE_WIDTH),
84 | h: px(42),
85 | color: 0xffffff,
86 | text_size: px(30),
87 | align_h: hmUI.align.LEFT,
88 | align_v: hmUI.align.CENTER_V,
89 | text_style: hmUI.text_style.ELLIPSIS,
90 | //text: name,
91 | },
92 | gts3: {
93 | x: 0,
94 | //y: px(DEVICE_HEIGHT * 0.35 + 60 * i),
95 | w: px(DEVICE_WIDTH),
96 | h: px(48),
97 | color: 0xffffff,
98 | text_size: px(36),
99 | align_h: hmUI.align.LEFT,
100 | align_v: hmUI.align.CENTER_V,
101 | text_style: hmUI.text_style.ELLIPSIS,
102 | //text: name,
103 | },
104 | gtr3: {
105 | x: 0,
106 | //y: px(DEVICE_HEIGHT * 0.35 + 60 * i),
107 | w: px(DEVICE_WIDTH),
108 | h: px(48),
109 | color: 0xffffff,
110 | text_size: px(36),
111 | align_h: hmUI.align.LEFT,
112 | align_v: hmUI.align.CENTER_V,
113 | text_style: hmUI.text_style.ELLIPSIS,
114 | //text: name,
115 | },
116 | gts4: {
117 | x: 0,
118 | //y: px(DEVICE_HEIGHT * 0.35 + 60 * i),
119 | w: px(DEVICE_WIDTH),
120 | h: px(48),
121 | color: 0xffffff,
122 | text_size: px(36),
123 | align_h: hmUI.align.LEFT,
124 | align_v: hmUI.align.CENTER_V,
125 | text_style: hmUI.text_style.ELLIPSIS,
126 | //text: name,
127 | },
128 | };
129 |
130 | export const getStyles = (deviceName) => {
131 | if (deviceName == "Amazfit Band 7")
132 | return {
133 | LIBRARY_HEADER: LIBRARY_HEADER.band7,
134 | TITLE: TITLE.band7,
135 | };
136 | else if (deviceName == "GTS 4 mini")
137 | return {
138 | LIBRARY_HEADER: LIBRARY_HEADER.gts4mini,
139 | TITLE: TITLE.gts4mini,
140 | };
141 | else if (deviceName == "GTS 3")
142 | return {
143 | LIBRARY_HEADER: LIBRARY_HEADER.gts3,
144 | TITLE: TITLE.gts3,
145 | };
146 | else if (deviceName == "GTS 4")
147 | return {
148 | LIBRARY_HEADER: LIBRARY_HEADER.gts4,
149 | TITLE: TITLE.gts4,
150 | };
151 | // default - gtr3
152 | return {
153 | LIBRARY_HEADER: LIBRARY_HEADER.gtr3,
154 | TITLE: TITLE.gtr3,
155 | };
156 | };
157 |
--------------------------------------------------------------------------------
/pages/home/index.page.js:
--------------------------------------------------------------------------------
1 | import { DEVICE_HEIGHT, DEVICE_WIDTH } from "../../utils/config/device";
2 | import { getStyles } from "./index.style";
3 |
4 | const { playerControl } = getApp()._options.globalData;
5 | const styles = getStyles(hmSetting.getDeviceInfo().deviceName);
6 | const vibrate = hmSensor.createSensor(hmSensor.id.VIBRATE);
7 | const logger = DeviceRuntimeCore.HmLogger.getLogger("spotify-for-zepp");
8 | const QUEUE_LENGHT = 15;
9 |
10 | // Empty initializations - used for references
11 | let song;
12 | let artist;
13 | let playBtn;
14 | let progressBar;
15 | let likeBtn;
16 | let shuffleBtn;
17 | let queueList = []; // references widgets
18 |
19 | Page({
20 | state: {},
21 | build() {
22 | hmUI.updateStatusBarTitle("player");
23 | hmApp.setLayerY(-DEVICE_HEIGHT);
24 | this.refresh();
25 | hmApp.setScreenKeep(true);
26 | const isVertical = true;
27 | hmUI.setScrollView(false, DEVICE_HEIGHT, 4, isVertical);
28 |
29 | devices = hmUI.createWidget(hmUI.widget.BUTTON, {
30 | ...styles.DEVICES_BUTTON,
31 | });
32 |
33 | song = hmUI.createWidget(hmUI.widget.TEXT, {
34 | ...styles.SONG,
35 | });
36 |
37 | artist = hmUI.createWidget(hmUI.widget.TEXT, {
38 | ...styles.ARTIST,
39 | });
40 |
41 | playBtn = hmUI.createWidget(hmUI.widget.IMG, {
42 | ...styles.PLAY_BUTTON,
43 | src: `play.png`,
44 | });
45 | playBtn.addEventListener(hmUI.event.CLICK_DOWN, () => {
46 | vibrate.stop();
47 | vibrate.scene = 23;
48 | playerControl.play();
49 | vibrate.start();
50 | });
51 |
52 | const nextBtn = hmUI.createWidget(hmUI.widget.IMG, {
53 | ...styles.NEXT_BUTTON,
54 | });
55 | nextBtn.addEventListener(hmUI.event.CLICK_DOWN, () => {
56 | vibrate.stop();
57 | vibrate.scene = 23;
58 | playerControl.next();
59 | vibrate.start();
60 | });
61 |
62 | const previousBtn = hmUI.createWidget(hmUI.widget.IMG, {
63 | ...styles.PREV_BUTTON,
64 | });
65 | previousBtn.addEventListener(hmUI.event.CLICK_DOWN, () => {
66 | vibrate.stop();
67 | vibrate.scene = 23;
68 | playerControl.previous();
69 | vibrate.start();
70 | });
71 |
72 | hmUI.createWidget(hmUI.widget.FILL_RECT, {
73 | ...styles.PROGRESS_BAR_BKG,
74 | });
75 | progressBar = hmUI.createWidget(hmUI.widget.FILL_RECT, {
76 | ...styles.PROGRESS_BAR,
77 | w: px(0),
78 | });
79 |
80 | spotifyIcon = hmUI.createWidget(hmUI.widget.IMG, {
81 | ...styles.SPOTIFY_ICON,
82 | });
83 |
84 | likeBtn = hmUI.createWidget(hmUI.widget.IMG, {
85 | ...styles.LIKE_BUTTON,
86 | });
87 | likeBtn.addEventListener(hmUI.event.CLICK_DOWN, () => {
88 | vibrate.stop();
89 | vibrate.scene = 23;
90 | playerControl.toggleLike();
91 | vibrate.start();
92 | });
93 |
94 | shuffleBtn = hmUI.createWidget(hmUI.widget.IMG, {
95 | ...styles.SHUFFLE_BUTTON,
96 | });
97 | shuffleBtn.addEventListener(hmUI.event.CLICK_DOWN, () => {
98 | vibrate.stop();
99 | vibrate.scene = 23;
100 | playerControl.toggleShuffle();
101 | vibrate.start();
102 | });
103 |
104 | // Queue
105 | for (let i = 0; i <= QUEUE_LENGHT; i++) {
106 | let queuedSong = hmUI.createWidget(hmUI.widget.TEXT, {
107 | y: px(DEVICE_HEIGHT * 2 + 70 * (i + 1)),
108 | ...styles.QUEUED_SONG,
109 | });
110 | queuedSong.addEventListener(hmUI.event.CLICK_DOWN, () => {
111 | // unreliable
112 | //for (let j = 0; j <= i; j++) this.player("next");
113 | });
114 | queueList.push(queuedSong);
115 | }
116 |
117 | hmApp.registerGestureEvent((event) => {
118 | switch (event) {
119 | case hmApp.gesture.LEFT:
120 | hmApp.gotoPage({ url: "pages/library/library.page" });
121 | break;
122 | default:
123 | break;
124 | }
125 | });
126 | },
127 | refresh() {
128 | // Refresh UI
129 | timer.createTimer(0, 150, () => {
130 | song.setProperty(hmUI.prop.MORE, {
131 | text: playerControl.song,
132 | });
133 |
134 | artist.setProperty(hmUI.prop.MORE, {
135 | text: playerControl.artist,
136 | });
137 | playBtn.setProperty(hmUI.prop.MORE, {
138 | src: `${playerControl.playState}.png`,
139 | });
140 | likeBtn.setProperty(hmUI.prop.MORE, {
141 | src: `${playerControl.likeState}.png`,
142 | });
143 | shuffleBtn.setProperty(hmUI.prop.MORE, {
144 | src: `${playerControl.shuffleState}.png`,
145 | });
146 |
147 | progressBar.setProperty(hmUI.prop.MORE, {
148 | ...styles.PROGRESS_BAR,
149 | w: px(DEVICE_WIDTH * playerControl.progress - 8),
150 | });
151 |
152 | const queue = playerControl.queue;
153 | for (let i = 0; i <= QUEUE_LENGHT; i++) {
154 | const name = queue[i] ? queue[i] : "";
155 | queueList[i].setProperty(hmUI.prop.MORE, {
156 | text: `${i + 1}. ${name}`,
157 | });
158 | }
159 | });
160 | },
161 | onDestroy() {
162 | vibrate && vibrate.stop();
163 | },
164 | });
165 |
--------------------------------------------------------------------------------
/pages/playlist/playlist.style.js:
--------------------------------------------------------------------------------
1 | const deviceInfo = hmSetting.getDeviceInfo();
2 | const {
3 | width: DEVICE_WIDTH,
4 | height: DEVICE_HEIGHT,
5 | screenShape,
6 | deviceName,
7 | } = deviceInfo;
8 |
9 | const PLAY_BUTTON = {
10 | band7: {
11 | x: DEVICE_WIDTH / 2 - px(32),
12 | y: px(DEVICE_HEIGHT * 0.15),
13 | src: `playlistPlay.png`,
14 | },
15 | gts4mini: {
16 | x: DEVICE_WIDTH / 2 - px(43),
17 | y: px(DEVICE_HEIGHT * 0.15),
18 | src: `playlistPlay.png`,
19 | },
20 | gts3: {
21 | x: DEVICE_WIDTH / 2 - px(47),
22 | y: px(DEVICE_HEIGHT * 0.15),
23 | src: `playlistPlay.png`,
24 | },
25 | gtr3: {
26 | x: DEVICE_WIDTH / 2 - px(47),
27 | y: px(DEVICE_HEIGHT * 0.15),
28 | src: `playlistPlay.png`,
29 | },
30 | gts4: {
31 | x: DEVICE_WIDTH / 2 - px(47),
32 | y: px(DEVICE_HEIGHT * 0.15),
33 | src: `playlistPlay.png`,
34 | },
35 | };
36 |
37 | const PLAYLIST_TITLE = {
38 | band7: {
39 | x: 0,
40 | y: px(DEVICE_HEIGHT * 0.35),
41 | w: px(DEVICE_WIDTH),
42 | h: px(48),
43 | color: 0x1fdf64,
44 | text_size: px(32),
45 | align_h: hmUI.align.CENTER_H,
46 | align_v: hmUI.align.CENTER_V,
47 | text_style: hmUI.text_style.NONE,
48 | },
49 | gts4mini: {
50 | x: 0,
51 | y: px(DEVICE_HEIGHT * 0.37),
52 | w: px(DEVICE_WIDTH),
53 | h: px(48),
54 | color: 0x1fdf64,
55 | text_size: px(38),
56 | align_h: hmUI.align.CENTER_H,
57 | align_v: hmUI.align.CENTER_V,
58 | text_style: hmUI.text_style.NONE,
59 | },
60 | gts3: {
61 | x: 0,
62 | y: px(DEVICE_HEIGHT * 0.37),
63 | w: px(DEVICE_WIDTH),
64 | h: px(50),
65 | color: 0x1fdf64,
66 | text_size: px(40),
67 | align_h: hmUI.align.CENTER_H,
68 | align_v: hmUI.align.CENTER_V,
69 | text_style: hmUI.text_style.NONE,
70 | },
71 | gtr3: {
72 | x: 0,
73 | y: px(DEVICE_HEIGHT * 0.37),
74 | w: px(DEVICE_WIDTH),
75 | h: px(50),
76 | color: 0x1fdf64,
77 | text_size: px(40),
78 | align_h: hmUI.align.CENTER_H,
79 | align_v: hmUI.align.CENTER_V,
80 | text_style: hmUI.text_style.NONE,
81 | },
82 | gts4: {
83 | x: 0,
84 | y: px(DEVICE_HEIGHT * 0.37),
85 | w: px(DEVICE_WIDTH),
86 | h: px(50),
87 | color: 0x1fdf64,
88 | text_size: px(40),
89 | align_h: hmUI.align.CENTER_H,
90 | align_v: hmUI.align.CENTER_V,
91 | text_style: hmUI.text_style.NONE,
92 | },
93 | };
94 |
95 | const SONG = {
96 | band7: {
97 | x: 0,
98 | //y: px(DEVICE_HEIGHT * 0.5 + 40 * i),
99 | w: px(DEVICE_WIDTH),
100 | h: px(28),
101 | color: 0xffffff,
102 | text_size: px(24),
103 | align_h: hmUI.align.LEFT,
104 | align_v: hmUI.align.CENTER_V,
105 | text_style: hmUI.text_style.ELLIPSIS,
106 | },
107 | gts4mini: {
108 | x: 0,
109 | //y: px(DEVICE_HEIGHT * 0.5 + 40 * i),
110 | w: px(DEVICE_WIDTH),
111 | h: px(32),
112 | color: 0xffffff,
113 | text_size: px(30),
114 | align_h: hmUI.align.LEFT,
115 | align_v: hmUI.align.CENTER_V,
116 | text_style: hmUI.text_style.ELLIPSIS,
117 | },
118 | gts3: {
119 | x: 0,
120 | //y: px(DEVICE_HEIGHT * 0.5 + 40 * i),
121 | w: px(DEVICE_WIDTH),
122 | h: px(32),
123 | color: 0xffffff,
124 | text_size: px(30),
125 | align_h: hmUI.align.LEFT,
126 | align_v: hmUI.align.CENTER_V,
127 | text_style: hmUI.text_style.ELLIPSIS,
128 | },
129 | gtr3: {
130 | x: 0,
131 | //y: px(DEVICE_HEIGHT * 0.5 + 40 * i),
132 | w: px(DEVICE_WIDTH),
133 | h: px(32),
134 | color: 0xffffff,
135 | text_size: px(30),
136 | align_h: hmUI.align.LEFT,
137 | align_v: hmUI.align.CENTER_V,
138 | text_style: hmUI.text_style.ELLIPSIS,
139 | },
140 | gts4: {
141 | x: 0,
142 | //y: px(DEVICE_HEIGHT * 0.5 + 40 * i),
143 | w: px(DEVICE_WIDTH),
144 | h: px(32),
145 | color: 0xffffff,
146 | text_size: px(30),
147 | align_h: hmUI.align.LEFT,
148 | align_v: hmUI.align.CENTER_V,
149 | text_style: hmUI.text_style.ELLIPSIS,
150 | },
151 | };
152 |
153 | const ARTIST = {
154 | band7: {
155 | x: 0,
156 | //y: px(DEVICE_HEIGHT * 0.55 + 40 * i),
157 | w: px(DEVICE_WIDTH),
158 | h: px(20),
159 | color: 0xb3b3b3,
160 | text_size: px(16),
161 | align_h: hmUI.align.LEFT,
162 | align_v: hmUI.align.CENTER_V,
163 | text_style: hmUI.text_style.ELLIPSIS,
164 | },
165 | gts4mini: {
166 | x: 0,
167 | //y: px(DEVICE_HEIGHT * 0.55 + 40 * i),
168 | w: px(DEVICE_WIDTH),
169 | h: px(24),
170 | color: 0xb3b3b3,
171 | text_size: px(20),
172 | align_h: hmUI.align.LEFT,
173 | align_v: hmUI.align.CENTER_V,
174 | text_style: hmUI.text_style.ELLIPSIS,
175 | },
176 | gts3: {
177 | x: 0,
178 | //y: px(DEVICE_HEIGHT * 0.55 + 40 * i),
179 | w: px(DEVICE_WIDTH),
180 | h: px(24),
181 | color: 0xb3b3b3,
182 | text_size: px(20),
183 | align_h: hmUI.align.LEFT,
184 | align_v: hmUI.align.CENTER_V,
185 | text_style: hmUI.text_style.ELLIPSIS,
186 | },
187 | gtr3: {
188 | x: 0,
189 | //y: px(DEVICE_HEIGHT * 0.55 + 40 * i),
190 | w: px(DEVICE_WIDTH),
191 | h: px(24),
192 | color: 0xb3b3b3,
193 | text_size: px(20),
194 | align_h: hmUI.align.LEFT,
195 | align_v: hmUI.align.CENTER_V,
196 | text_style: hmUI.text_style.ELLIPSIS,
197 | },
198 | gts4: {
199 | x: 0,
200 | //y: px(DEVICE_HEIGHT * 0.55 + 40 * i),
201 | w: px(DEVICE_WIDTH),
202 | h: px(24),
203 | color: 0xb3b3b3,
204 | text_size: px(20),
205 | align_h: hmUI.align.LEFT,
206 | align_v: hmUI.align.CENTER_V,
207 | text_style: hmUI.text_style.ELLIPSIS,
208 | },
209 | };
210 |
211 | export const getStyles = (deviceName) => {
212 | if (deviceName == "Amazfit Band 7")
213 | return {
214 | PLAY_BUTTON: PLAY_BUTTON.band7,
215 | PLAYLIST_TITLE: PLAYLIST_TITLE.band7,
216 | SONG: SONG.band7,
217 | ARTIST: ARTIST.band7,
218 | };
219 | else if (deviceName == "GTS 4 mini")
220 | return {
221 | PLAY_BUTTON: PLAY_BUTTON.gts4mini,
222 | PLAYLIST_TITLE: PLAYLIST_TITLE.gts4mini,
223 | SONG: SONG.gts4mini,
224 | ARTIST: ARTIST.gts4mini,
225 | };
226 | else if (deviceName == "GTS 3")
227 | return {
228 | PLAY_BUTTON: PLAY_BUTTON.gts3,
229 | PLAYLIST_TITLE: PLAYLIST_TITLE.gts3,
230 | SONG: SONG.gts3,
231 | ARTIST: ARTIST.gts3,
232 | };
233 | else if (deviceName == "GTS 4")
234 | return {
235 | PLAY_BUTTON: PLAY_BUTTON.gts4,
236 | PLAYLIST_TITLE: PLAYLIST_TITLE.gts4,
237 | SONG: SONG.gts4,
238 | ARTIST: ARTIST.gts4,
239 | };
240 | return {
241 | // gtr 3
242 | PLAY_BUTTON: PLAY_BUTTON.gtr3,
243 | PLAYLIST_TITLE: PLAYLIST_TITLE.gtr3,
244 | SONG: SONG.gtr3,
245 | ARTIST: ARTIST.gtr3,
246 | };
247 | };
248 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "configVersion": "v2",
3 | "app": {
4 | "appId": 1017560,
5 | "appName": "Spotify for ZeppOS",
6 | "appType": "app",
7 | "version": {
8 | "code": 1,
9 | "name": "1.0.1"
10 | },
11 | "icon": "icon.png",
12 | "vender": "juan518munoz",
13 | "description": "Spotify console player"
14 | },
15 | "permissions": [],
16 | "runtime": {
17 | "apiVersion": {
18 | "compatible": "1.0.0",
19 | "target": "1.0.1",
20 | "minVersion": "1.0.0"
21 | }
22 | },
23 | "targets": {
24 | "band7": {
25 | "module": {
26 | "page": {
27 | "pages": [
28 | "pages/home/index.page",
29 | "pages/library/library.page",
30 | "pages/playlist/playlist.page",
31 | "pages/devices/devices.page"
32 | ]
33 | },
34 | "app-side": {
35 | "path": "app-side/index"
36 | },
37 | "setting": {
38 | "path": "setting/index"
39 | }
40 | },
41 | "platforms": [
42 | {
43 | "name": "amazfit-band7",
44 | "deviceSource": 253
45 | },
46 | {
47 | "name": "amazfit-band7-w",
48 | "deviceSource": 254
49 | }
50 | ],
51 | "designWidth": 194
52 | },
53 | "mi-band7": {
54 | "module": {
55 | "page": {
56 | "pages": [
57 | "pages/home/index.page",
58 | "pages/library/library.page",
59 | "pages/playlist/playlist.page",
60 | "pages/devices/devices.page"
61 | ]
62 | },
63 | "app-side": {
64 | "path": "app-side/index"
65 | },
66 | "setting": {
67 | "path": "setting/index"
68 | }
69 | },
70 | "platforms": [
71 | {
72 | "name": "amazfit-band7",
73 | "deviceSource": 253
74 | },
75 | {
76 | "name": "amazfit-band7-w",
77 | "deviceSource": 254
78 | },
79 | {
80 | "name": "l66",
81 | "deviceSource": 260
82 | },
83 | {
84 | "name": "l66w",
85 | "deviceSource": 261
86 | },
87 | {
88 | "name": "l66_1",
89 | "deviceSource": 262
90 | },
91 | {
92 | "name": "l66w_2",
93 | "deviceSource": 263
94 | },
95 | {
96 | "name": "l66_3",
97 | "deviceSource": 264
98 | },
99 | {
100 | "name": "l66_4",
101 | "deviceSource": 265
102 | },
103 | {
104 | "name": "l66_5",
105 | "deviceSource": 266
106 | }
107 | ],
108 | "designWidth": 194
109 | },
110 | "gts4mini": {
111 | "module": {
112 | "page": {
113 | "pages": [
114 | "pages/home/index.page",
115 | "pages/library/library.page",
116 | "pages/playlist/playlist.page",
117 | "pages/devices/devices.page"
118 | ]
119 | },
120 | "app-side": {
121 | "path": "app-side/index"
122 | },
123 | "setting": {
124 | "path": "setting/index"
125 | }
126 | },
127 | "platforms": [
128 | {
129 | "name": "gts-4-mini",
130 | "deviceSource": 246
131 | },
132 | {
133 | "name": "gts-4-mini-w",
134 | "deviceSource": 247
135 | }
136 | ],
137 | "designWidth": 336
138 | },
139 | "gts3": {
140 | "module": {
141 | "page": {
142 | "pages": [
143 | "pages/home/index.page",
144 | "pages/library/library.page",
145 | "pages/playlist/playlist.page",
146 | "pages/devices/devices.page"
147 | ]
148 | },
149 | "app-side": {
150 | "path": "app-side/index"
151 | },
152 | "setting": {
153 | "path": "setting/index"
154 | }
155 | },
156 | "platforms": [
157 | {
158 | "name": "gts-3",
159 | "deviceSource": 224
160 | },
161 | {
162 | "name": "gts-3-w",
163 | "deviceSource": 225
164 | }
165 | ],
166 | "designWidth": 390
167 | },
168 | "gtr3-trex2": {
169 | "module": {
170 | "page": {
171 | "pages": [
172 | "pages/home/index.page",
173 | "pages/library/library.page",
174 | "pages/playlist/playlist.page",
175 | "pages/devices/devices.page"
176 | ]
177 | },
178 | "app-side": {
179 | "path": "app-side/index"
180 | },
181 | "setting": {
182 | "path": "setting/index"
183 | }
184 | },
185 | "platforms": [
186 | {
187 | "name": "gtr-3",
188 | "deviceSource": 226
189 | },
190 | {
191 | "name": "gtr-3-w",
192 | "deviceSource": 227
193 | },
194 | {
195 | "name": "trex-2",
196 | "deviceSource": 418
197 | },
198 | {
199 | "name": "trex-2-w",
200 | "deviceSource": 419
201 | }
202 | ],
203 | "designWidth": 454
204 | },
205 | "gtr3pro": {
206 | "module": {
207 | "page": {
208 | "pages": [
209 | "pages/home/index.page",
210 | "pages/library/library.page",
211 | "pages/playlist/playlist.page",
212 | "pages/devices/devices.page"
213 | ]
214 | },
215 | "app-side": {
216 | "path": "app-side/index"
217 | },
218 | "setting": {
219 | "path": "setting/index"
220 | }
221 | },
222 | "platforms": [
223 | {
224 | "name": "gtr-3-pro",
225 | "deviceSource": 229
226 | },
227 | {
228 | "name": "gtr-3-pro-w",
229 | "deviceSource": 230
230 | },
231 | {
232 | "name": "gtr-3-pro-x",
233 | "deviceSource": 6095106
234 | }
235 | ],
236 | "designWidth": 480
237 | },
238 | "falcon": {
239 | "module": {
240 | "page": {
241 | "pages": [
242 | "pages/home/index.page",
243 | "pages/library/library.page",
244 | "pages/playlist/playlist.page",
245 | "pages/devices/devices.page"
246 | ]
247 | },
248 | "app-side": {
249 | "path": "app-side/index"
250 | },
251 | "setting": {
252 | "path": "setting/index"
253 | }
254 | },
255 | "platforms": [
256 | {
257 | "name": "falcon",
258 | "deviceSource": 414
259 | },
260 | {
261 | "name": "falcon-w",
262 | "deviceSource": 415
263 | }
264 | ],
265 | "designWidth": 416
266 | },
267 | "gts4": {
268 | "module": {
269 | "page": {
270 | "pages": [
271 | "pages/home/index.page",
272 | "pages/library/library.page",
273 | "pages/playlist/playlist.page",
274 | "pages/devices/devices.page"
275 | ]
276 | },
277 | "app-side": {
278 | "path": "app-side/index"
279 | },
280 | "setting": {
281 | "path": "setting/index"
282 | }
283 | },
284 | "platforms": [
285 | {
286 | "name": "gts4",
287 | "deviceSource": 7995648
288 | },
289 | {
290 | "name": "gts4-w",
291 | "deviceSource": 7995649
292 | }
293 | ],
294 | "designWidth": 390
295 | },
296 | "gtr4": {
297 | "module": {
298 | "page": {
299 | "pages": [
300 | "pages/home/index.page",
301 | "pages/library/library.page",
302 | "pages/playlist/playlist.page",
303 | "pages/devices/devices.page"
304 | ]
305 | },
306 | "app-side": {
307 | "path": "app-side/index"
308 | },
309 | "setting": {
310 | "path": "setting/index"
311 | }
312 | },
313 | "platforms": [
314 | {
315 | "name": "gtr-4",
316 | "deviceSource": 7930112
317 | },
318 | {
319 | "name": "gtr-4-w",
320 | "deviceSource": 7930113
321 | }
322 | ],
323 | "designWidth": 466
324 | }
325 | },
326 | "i18n": {
327 | "en-US": {
328 | "appName": "Spotify"
329 | }
330 | },
331 | "defaultLanguage": "en-US"
332 | }
--------------------------------------------------------------------------------
/app-side/index.js:
--------------------------------------------------------------------------------
1 | import { client_id, client_secret } from "../utils/config/client";
2 | import { MessageBuilder } from "../shared/message";
3 |
4 | const messageBuilder = new MessageBuilder();
5 | let SPOTIFY_AUTH_TOKEN = "";
6 |
7 | const http = {
8 | "": "GET",
9 | play: "PUT",
10 | pause: "PUT",
11 | next: "POST",
12 | previous: "POST",
13 | liked: "PUT",
14 | notLiked: "DELETE",
15 | shuffle: "PUT",
16 | };
17 |
18 | const refreshBearerToken = async () => {
19 | try {
20 | let urlencoded = new URLSearchParams();
21 | urlencoded.append("grant_type", "refresh_token");
22 | urlencoded.append("client_id", client_id);
23 | urlencoded.append("client_secret", client_secret);
24 | urlencoded.append(
25 | "refresh_token",
26 | settings.settingsStorage.getItem("refreshToken")
27 | );
28 |
29 | const res = await fetch({
30 | url: "https://accounts.spotify.com/api/token",
31 | method: "POST",
32 | headers: {
33 | "Content-Type": "application/x-www-form-urlencoded",
34 | },
35 | body: urlencoded.toString(),
36 | });
37 | const { status } = res;
38 | if (status == 400) {
39 | return;
40 | }
41 |
42 | const { body = {} } = res;
43 | const { access_token = "" } = body; //JSON.parse(body); // body
44 |
45 | SPOTIFY_AUTH_TOKEN = access_token;
46 | } catch (error) {
47 | SPOTIFY_AUTH_TOKEN = error;
48 | }
49 | };
50 |
51 | const isSongLiked = async (currID) => {
52 | try {
53 | let isLiked = false;
54 | const res = await fetch({
55 | url: `https://api.spotify.com/v1/me/tracks`,
56 | method: "GET",
57 | headers: {
58 | Authorization: `Bearer ${SPOTIFY_AUTH_TOKEN}`,
59 | },
60 | });
61 |
62 | const { body } = res;
63 | const { items = [] } = body; //JSON.parse(body); // body
64 | items.forEach((item) => {
65 | const { track: { id = "" } = {} } = item;
66 | if (id == currID) isLiked = true;
67 | });
68 |
69 | return isLiked;
70 | } catch (error) {
71 | return false;
72 | }
73 | };
74 |
75 | const getQueue = async (ctx) => {
76 | try {
77 | const res = await fetch({
78 | url: `https://api.spotify.com/v1/me/player/queue`,
79 | method: "GET",
80 | headers: {
81 | Authorization: `Bearer ${SPOTIFY_AUTH_TOKEN}`,
82 | },
83 | });
84 | const { status } = res;
85 | if (status != 200) throw "Error";
86 |
87 | let q = [];
88 | const { body = {} } = res;
89 | const { queue } = body; //JSON.parse(body); // body
90 | queue.forEach((item) => {
91 | const { name = "" } = item;
92 | q.push(name);
93 | });
94 |
95 | return q;
96 | } catch (error) {
97 | return [];
98 | }
99 | };
100 |
101 | const getAllPlaylists = async (ctx) => {
102 | try {
103 | const res = await fetch({
104 | url: `https://api.spotify.com/v1/me/playlists?limit=10`,
105 | method: "GET",
106 | headers: {
107 | Authorization: `Bearer ${SPOTIFY_AUTH_TOKEN}`,
108 | },
109 | });
110 | const { status } = res;
111 | if (status >= 400) return;
112 |
113 | const { body = {} } = res;
114 | const { items = [] } = body; //JSON.parse(body); // body
115 |
116 | let playLists = [];
117 | items.forEach((item) => {
118 | const { name = "", id = "" } = item;
119 | playLists.push({ name, id });
120 | });
121 | ctx.response({
122 | data: {
123 | playLists: playLists,
124 | },
125 | });
126 | } catch (error) {
127 | console.log(error);
128 | }
129 | };
130 |
131 | const startPlaylist = async (playlistId = "") => {
132 | const body = {
133 | context_uri: `spotify:playlist:${playlistId}`,
134 | };
135 | try {
136 | await fetch({
137 | url: `https://api.spotify.com/v1/me/player/play`,
138 | method: "PUT",
139 | headers: {
140 | Authorization: `Bearer ${SPOTIFY_AUTH_TOKEN}`,
141 | },
142 | body: JSON.stringify(body),
143 | });
144 | } catch (error) {
145 | console.log(error);
146 | }
147 | };
148 |
149 | const playlist = async (ctx, playlistId = "", func = "") => {
150 | try {
151 | const res = await fetch({
152 | url: `https://api.spotify.com/v1/playlists/${playlistId}/${func}`,
153 | method: http[func],
154 | headers: {
155 | Authorization: `Bearer ${SPOTIFY_AUTH_TOKEN}`,
156 | },
157 | });
158 | const { status } = res;
159 | if (status >= 400) return;
160 |
161 | const { body = {} } = res;
162 | const { tracks: { items = [] } = {} } = body; //JSON.parse(body); // body
163 |
164 | let songList = [];
165 | items.forEach((item) => {
166 | const { track: { name = "", artists = [] } = {} } = item;
167 | let artistNames = artists.map((artist) => artist.name).join(", ");
168 |
169 | songList.push({ name, artistNames });
170 | });
171 | ctx.response({
172 | data: {
173 | songList: songList.slice(0, 20),
174 | },
175 | });
176 | } catch (error) {
177 | console.log(error);
178 | }
179 | };
180 |
181 | const tracks = async (ctx, func = "", curSongId = "") => {
182 | try {
183 | const res = await fetch({
184 | url: `https://api.spotify.com/v1/me/tracks?ids=${curSongId}`,
185 | method: http[func],
186 | headers: {
187 | Authorization: `Bearer ${SPOTIFY_AUTH_TOKEN}`,
188 | },
189 | });
190 | const { status } = res;
191 | if (status == 400 || status == 401) {
192 | await refreshBearerToken();
193 | return await player(ctx);
194 | }
195 | } catch (error) {}
196 | };
197 |
198 | const player = async (ctx, func = "", args = "") => {
199 | try {
200 | const res = await fetch({
201 | url: `https://api.spotify.com/v1/me/player/${func}?${args}`,
202 | method: http[func],
203 | headers: {
204 | Authorization: `Bearer ${SPOTIFY_AUTH_TOKEN}`,
205 | },
206 | });
207 | const { status } = res;
208 | if (status == 400 || status == 401) {
209 | await refreshBearerToken();
210 | ctx.response({
211 | data: {
212 | songName: name,
213 | artistNames: artistNames,
214 | isPlaying: false,
215 | isLiked: false,
216 | isShuffled: false,
217 | progress: 0,
218 | songId: "id",
219 | queue: [],
220 | },
221 | });
222 | return;
223 | }
224 | if (func != "") return;
225 | else if (status == 204) {
226 | ctx.response({
227 | songName: "No device playing",
228 | artistNames: "start streaming in any device",
229 | isPlaying: false,
230 | isLiked: false,
231 | progress: 0,
232 | songId: "",
233 | });
234 | }
235 |
236 | const { body = {} } = res;
237 | const {
238 | shuffle_state = false,
239 | progress_ms = 0,
240 | item: { name = "", artists = [], duration_ms = 0, id = "" } = {},
241 | is_playing = false,
242 | } = body; //JSON.parse(body); // body
243 |
244 | let artistNames = artists.map((artist) => artist.name).join(", ");
245 | const isLiked = await isSongLiked(id);
246 | const progress = (progress_ms * 100) / duration_ms / 100;
247 | const queue = await getQueue();
248 | ctx.response({
249 | data: {
250 | songName: name,
251 | artistNames: artistNames,
252 | isPlaying: is_playing,
253 | isLiked: isLiked,
254 | isShuffled: shuffle_state,
255 | progress: progress,
256 | songId: id,
257 | queue: queue.slice(0, 16),
258 | },
259 | });
260 | } catch (error) {
261 | ctx.response({
262 | data: {
263 | songName: "No connection",
264 | artistNames: "make sure a device is streaming",
265 | },
266 | });
267 | }
268 | };
269 |
270 | AppSideService({
271 | onInit() {
272 | messageBuilder.listen(() => {});
273 |
274 | messageBuilder.on("request", (ctx) => {
275 | if (
276 | (settings.settingsStorage.getItem("refreshToken") == null ||
277 | settings.settingsStorage.getItem("refreshToken") == "") &&
278 | SPOTIFY_AUTH_TOKEN == ""
279 | ) {
280 | ctx.response({
281 | data: {
282 | songName: "refresh token not set",
283 | artistNames: "set refresh token on the Zepp app",
284 | isPlaying: false,
285 | isLiked: false,
286 | isShuffled: false,
287 | progress: 0,
288 | songId: "id",
289 | queue: [],
290 | },
291 | });
292 | return;
293 | }
294 |
295 | const jsonRpc = messageBuilder.buf2Json(ctx.request.payload);
296 | if (jsonRpc.func == "player") {
297 | return player(ctx, jsonRpc.method, jsonRpc.args);
298 | } else if (jsonRpc.func == "tracks") {
299 | return tracks(ctx, jsonRpc.method, jsonRpc.curSongId);
300 | } else if (jsonRpc.func == "getAllPlaylists") {
301 | return getAllPlaylists(ctx);
302 | } else if (jsonRpc.func == "playlistInfo") {
303 | return playlist(ctx, jsonRpc.playlistId);
304 | } else if (jsonRpc.func == "startPlaylist") {
305 | return startPlaylist(jsonRpc.playlistId);
306 | }
307 | });
308 | },
309 |
310 | onRun() {},
311 |
312 | onDestroy() {},
313 | });
314 |
--------------------------------------------------------------------------------
/shared/promise.js:
--------------------------------------------------------------------------------
1 | ;(function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined'
3 | ? factory()
4 | : typeof define === 'function' && define.amd
5 | ? define(factory)
6 | : factory()
7 | })(globalThis, function () {
8 | 'use strict'
9 |
10 | /**
11 | * @this {Promise}
12 | */
13 | function finallyConstructor(callback) {
14 | var constructor = this.constructor
15 | return this.then(
16 | function (value) {
17 | // @ts-ignore
18 | return constructor.resolve(callback()).then(function () {
19 | return value
20 | })
21 | },
22 | function (reason) {
23 | // @ts-ignore
24 | return constructor.resolve(callback()).then(function () {
25 | // @ts-ignore
26 | return constructor.reject(reason)
27 | })
28 | },
29 | )
30 | }
31 |
32 | function allSettled(arr) {
33 | var P = this
34 | return new P(function (resolve, reject) {
35 | if (!(arr && typeof arr.length !== 'undefined')) {
36 | return reject(
37 | new TypeError(
38 | typeof arr +
39 | ' ' +
40 | arr +
41 | ' is not iterable(cannot read property Symbol(Symbol.iterator))',
42 | ),
43 | )
44 | }
45 | var args = Array.prototype.slice.call(arr)
46 | if (args.length === 0) return resolve([])
47 | var remaining = args.length
48 |
49 | function res(i, val) {
50 | if (val && (typeof val === 'object' || typeof val === 'function')) {
51 | var then = val.then
52 | if (typeof then === 'function') {
53 | then.call(
54 | val,
55 | function (val) {
56 | res(i, val)
57 | },
58 | function (e) {
59 | args[i] = { status: 'rejected', reason: e }
60 | if (--remaining === 0) {
61 | resolve(args)
62 | }
63 | },
64 | )
65 | return
66 | }
67 | }
68 | args[i] = { status: 'fulfilled', value: val }
69 | if (--remaining === 0) {
70 | resolve(args)
71 | }
72 | }
73 |
74 | for (var i = 0; i < args.length; i++) {
75 | res(i, args[i])
76 | }
77 | })
78 | }
79 |
80 | // Store setTimeout reference so promise-polyfill will be unaffected by
81 | // other code modifying setTimeout (like sinon.useFakeTimers())
82 | var setTimeoutFunc = setTimeout
83 |
84 | function isArray(x) {
85 | return Boolean(x && typeof x.length !== 'undefined')
86 | }
87 |
88 | function noop() {}
89 |
90 | // Polyfill for Function.prototype.bind
91 | function bind(fn, thisArg) {
92 | return function () {
93 | fn.apply(thisArg, arguments)
94 | }
95 | }
96 |
97 | /**
98 | * @constructor
99 | * @param {Function} fn
100 | */
101 | function Promise(fn) {
102 | if (!(this instanceof Promise)) throw new TypeError('Promises must be constructed via new')
103 | if (typeof fn !== 'function') throw new TypeError('not a function')
104 | /** @type {!number} */
105 | this._state = 0
106 | /** @type {!boolean} */
107 | this._handled = false
108 | /** @type {Promise|undefined} */
109 | this._value = undefined
110 | /** @type {!Array} */
111 | this._deferreds = []
112 |
113 | doResolve(fn, this)
114 | }
115 |
116 | function handle(self, deferred) {
117 | while (self._state === 3) {
118 | self = self._value
119 | }
120 | if (self._state === 0) {
121 | self._deferreds.push(deferred)
122 | return
123 | }
124 | self._handled = true
125 | Promise._immediateFn(function () {
126 | var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected
127 | if (cb === null) {
128 | ;(self._state === 1 ? resolve : reject)(deferred.promise, self._value)
129 | return
130 | }
131 | var ret
132 | try {
133 | ret = cb(self._value)
134 | } catch (e) {
135 | reject(deferred.promise, e)
136 | return
137 | }
138 | resolve(deferred.promise, ret)
139 | })
140 | }
141 |
142 | function resolve(self, newValue) {
143 | try {
144 | // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
145 | if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.')
146 | if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
147 | var then = newValue.then
148 | if (newValue instanceof Promise) {
149 | self._state = 3
150 | self._value = newValue
151 | finale(self)
152 | return
153 | } else if (typeof then === 'function') {
154 | doResolve(bind(then, newValue), self)
155 | return
156 | }
157 | }
158 | self._state = 1
159 | self._value = newValue
160 | finale(self)
161 | } catch (e) {
162 | reject(self, e)
163 | }
164 | }
165 |
166 | function reject(self, newValue) {
167 | self._state = 2
168 | self._value = newValue
169 | finale(self)
170 | }
171 |
172 | function finale(self) {
173 | if (self._state === 2 && self._deferreds.length === 0) {
174 | Promise._immediateFn(function () {
175 | if (!self._handled) {
176 | Promise._unhandledRejectionFn(self._value)
177 | }
178 | })
179 | }
180 |
181 | for (var i = 0, len = self._deferreds.length; i < len; i++) {
182 | handle(self, self._deferreds[i])
183 | }
184 | self._deferreds = null
185 | }
186 |
187 | /**
188 | * @constructor
189 | */
190 | function Handler(onFulfilled, onRejected, promise) {
191 | this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null
192 | this.onRejected = typeof onRejected === 'function' ? onRejected : null
193 | this.promise = promise
194 | }
195 |
196 | /**
197 | * Take a potentially misbehaving resolver function and make sure
198 | * onFulfilled and onRejected are only called once.
199 | *
200 | * Makes no guarantees about asynchrony.
201 | */
202 | function doResolve(fn, self) {
203 | var done = false
204 | try {
205 | fn(
206 | function (value) {
207 | if (done) return
208 | done = true
209 | resolve(self, value)
210 | },
211 | function (reason) {
212 | if (done) return
213 | done = true
214 | reject(self, reason)
215 | },
216 | )
217 | } catch (ex) {
218 | if (done) return
219 | done = true
220 | reject(self, ex)
221 | }
222 | }
223 |
224 | Promise.prototype['catch'] = function (onRejected) {
225 | return this.then(null, onRejected)
226 | }
227 |
228 | Promise.prototype.then = function (onFulfilled, onRejected) {
229 | // @ts-ignore
230 | var prom = new this.constructor(noop)
231 |
232 | handle(this, new Handler(onFulfilled, onRejected, prom))
233 | return prom
234 | }
235 |
236 | Promise.prototype['finally'] = finallyConstructor
237 |
238 | Promise.all = function (arr) {
239 | return new Promise(function (resolve, reject) {
240 | if (!isArray(arr)) {
241 | return reject(new TypeError('Promise.all accepts an array'))
242 | }
243 |
244 | var args = Array.prototype.slice.call(arr)
245 | if (args.length === 0) return resolve([])
246 | var remaining = args.length
247 |
248 | function res(i, val) {
249 | try {
250 | if (val && (typeof val === 'object' || typeof val === 'function')) {
251 | var then = val.then
252 | if (typeof then === 'function') {
253 | then.call(
254 | val,
255 | function (val) {
256 | res(i, val)
257 | },
258 | reject,
259 | )
260 | return
261 | }
262 | }
263 | args[i] = val
264 | if (--remaining === 0) {
265 | resolve(args)
266 | }
267 | } catch (ex) {
268 | reject(ex)
269 | }
270 | }
271 |
272 | for (var i = 0; i < args.length; i++) {
273 | res(i, args[i])
274 | }
275 | })
276 | }
277 |
278 | Promise.allSettled = allSettled
279 |
280 | Promise.resolve = function (value) {
281 | if (value && typeof value === 'object' && value.constructor === Promise) {
282 | return value
283 | }
284 |
285 | return new Promise(function (resolve) {
286 | resolve(value)
287 | })
288 | }
289 |
290 | Promise.reject = function (value) {
291 | return new Promise(function (resolve, reject) {
292 | reject(value)
293 | })
294 | }
295 |
296 | Promise.race = function (arr) {
297 | return new Promise(function (resolve, reject) {
298 | if (!isArray(arr)) {
299 | return reject(new TypeError('Promise.race accepts an array'))
300 | }
301 |
302 | for (var i = 0, len = arr.length; i < len; i++) {
303 | Promise.resolve(arr[i]).then(resolve, reject)
304 | }
305 | })
306 | }
307 |
308 | // Use polyfill for setImmediate for performance gains
309 | Promise._immediateFn =
310 | // @ts-ignore
311 | (typeof setImmediate === 'function' &&
312 | function (fn) {
313 | // @ts-ignore
314 | setImmediate(fn)
315 | }) ||
316 | function (fn) {
317 | setTimeoutFunc(fn, 0)
318 | }
319 |
320 | Promise._unhandledRejectionFn = function _unhandledRejectionFn(err) {
321 | if (typeof console !== 'undefined' && console) {
322 | console.log('[jsfwk.error ] Possible Unhandled Promise Rejection:', err) // eslint-disable-line no-console
323 | }
324 | }
325 |
326 | /** @suppress {undefinedVars} */
327 | var globalNS = (function () {
328 | // the only reliable means to get the global object is
329 | // `Function('return this')()`
330 | // However, this causes CSP violations in Chrome apps.
331 | if (typeof self !== 'undefined') {
332 | return self
333 | }
334 | if (typeof window !== 'undefined') {
335 | return window
336 | }
337 | if (typeof global !== 'undefined') {
338 | return global
339 | }
340 | if (typeof globalThis !== 'undefined') {
341 | return globalThis
342 | }
343 |
344 | throw new Error('unable to locate global object')
345 | })()
346 |
347 | globalNS['Promise'] = Promise
348 | // Expose the polyfill if Promise is undefined or set to a
349 | // non-function value. The latter can be due to a named HTMLElement
350 | // being exposed by browsers for legacy reasons.
351 | // https://github.com/taylorhakes/promise-polyfill/issues/114
352 | })
353 |
--------------------------------------------------------------------------------
/setting/index.js:
--------------------------------------------------------------------------------
1 | import { client_id, client_secret } from "../utils/config/client";
2 |
3 | const redirectUri =
4 | "http://zepp-os-staging.zepp.com/app-settings/v1.0.0/index.html?appId=1017560";
5 | const source = `https://accounts.spotify.com/en/authorize?client_id=${client_id}&response_type=code&redirect_uri=${redirectUri}&scope=ugc-image-upload user-read-playback-state user-modify-playback-state user-read-currently-playing app-remote-control streaming playlist-read-private playlist-read-collaborative playlist-modify-private playlist-modify-public user-follow-modify user-follow-read user-read-playback-position user-top-read user-read-recently-played user-library-modify user-library-read user-read-email user-read-private`;
6 | const manualRedirectUri = "https://juan518munoz.github.io/ZeppOS-Spotify-Web";
7 | const manualSource = `https://accounts.spotify.com/en/authorize?client_id=${client_id}&response_type=code&redirect_uri=https://juan518munoz.github.io/ZeppOS-Spotify-Web&scope=ugc-image-upload user-read-playback-state user-modify-playback-state user-read-currently-playing app-remote-control streaming playlist-read-private playlist-read-collaborative playlist-modify-private playlist-modify-public user-follow-modify user-follow-read user-read-playback-position user-top-read user-read-recently-played user-library-modify user-library-read user-read-email user-read-private`;
8 |
9 | let done = false;
10 |
11 | const BtnStyle = {
12 | textTransform: "",
13 | letterSpacing: "2px",
14 | fontWeight: "700",
15 | textAlign: "center",
16 | lineHeight: "1",
17 | borderRadius: "500px",
18 | background: "#1db954",
19 | color: "white",
20 | height: "40px",
21 | minWidth: "160px",
22 | borderWidth: "0",
23 | marginTop: "40px",
24 | padding: "11px 32px 9px",
25 | verticalAlign: "middle",
26 | };
27 |
28 | AppSettingsPage({
29 | logout(props) {
30 | props.settingsStorage.removeItem("authToken");
31 | props.settingsStorage.removeItem("refreshToken");
32 | props.settingsStorage.removeItem("user");
33 | props.settingsStorage.removeItem("mail");
34 | props.settingsStorage.removeItem("product");
35 | props.settingsStorage.removeItem("iosLogin");
36 | },
37 | login(props) {
38 | if (done) return;
39 | fetch("").then((res) => {
40 | const { url } = res;
41 |
42 | let start = url.indexOf("code=");
43 | let code = "";
44 | if (start != -1) {
45 | done = true;
46 | code = url.substring(start + 5);
47 | props.settingsStorage.setItem("authToken", code);
48 |
49 | this.getRefreshToken(props).then(this.getUserInfo(props));
50 | }
51 | });
52 | },
53 | getRefreshToken(props, token = "notoken", uri = redirectUri) {
54 | return new Promise((resolve) => {
55 | let details = {
56 | grant_type: "authorization_code",
57 | code: props.settingsStorage.getItem("authToken") || token,
58 | client_id: client_id,
59 | client_secret: client_secret,
60 | redirect_uri: uri,
61 | };
62 | let formBody = [];
63 | for (let property in details) {
64 | const encodedKey = encodeURIComponent(property);
65 | const encodedValue = encodeURIComponent(details[property]);
66 | formBody.push(encodedKey + "=" + encodedValue);
67 | }
68 | formBody = formBody.join("&");
69 |
70 | fetch("https://accounts.spotify.com/api/token", {
71 | method: "POST",
72 | headers: {
73 | "Content-Type": "application/x-www-form-urlencoded",
74 | },
75 | body: formBody,
76 | })
77 | .then((res) => {
78 | return res.json();
79 | })
80 | .then((data) => {
81 | const {
82 | refresh_token = "",
83 | error_description = "Unkown error",
84 | access_token = "",
85 | } = data;
86 |
87 | if (refresh_token != "") {
88 | props.settingsStorage.setItem("refreshToken", refresh_token);
89 | props.settingsStorage.setItem("accessToken", access_token);
90 | } else
91 | props.settingsStorage.setItem("refreshToken", error_description);
92 | });
93 | setTimeout(() => {
94 | console.log("func1 finished");
95 | resolve();
96 | }, 3000);
97 | });
98 | },
99 | getUserInfo(props) {
100 | const access_token = props.settingsStorage.getItem("accessToken");
101 | if (
102 | access_token === undefined ||
103 | access_token === null ||
104 | access_token.lenght === 0
105 | ) {
106 | setTimeout(() => {
107 | this.getUserInfo(props);
108 | }, "1000");
109 | }
110 | fetch("https://api.spotify.com/v1/me/", {
111 | method: "GET",
112 | headers: {
113 | Authorization: `Bearer ${access_token}`,
114 | },
115 | })
116 | .then((res) => {
117 | return res.json();
118 | })
119 | .then((res) => {
120 | console.log(res);
121 | const { display_name = "fail", email = "", product = "" } = res;
122 |
123 | props.settingsStorage.setItem("user", display_name);
124 | props.settingsStorage.setItem("mail", email);
125 | props.settingsStorage.setItem("product", product);
126 | })
127 | .catch((error) => {
128 | props.settingsStorage.setItem("user", error);
129 | });
130 | props.settingsStorage.removeItem("accessToken");
131 | },
132 | build(props) {
133 | this.login(props);
134 | const logo = View(
135 | {
136 | style: {
137 | display: "flex",
138 | justifyContent: "center",
139 | alignItems: "center",
140 | paddingBottom: "50px",
141 | },
142 | },
143 | [
144 | Image({
145 | src: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/Spotify_logo_with_text.svg/559px-Spotify_logo_with_text.svg.png",
146 | alt: "spotifyLogo",
147 | width: "50%",
148 | }),
149 | ]
150 | );
151 | const userInfo = View(
152 | {
153 | style: {
154 | display: "flex",
155 | flexDirection: "column",
156 | width: "100%",
157 | },
158 | },
159 | [
160 | View(
161 | {
162 | style: {
163 | marginBottom: "12px",
164 | },
165 | },
166 | [
167 | Text({ bold: true }, "User: "),
168 | Text(
169 | {},
170 | `${
171 | props.settingsStorage.getItem("user")
172 | ? props.settingsStorage.getItem("user")
173 | : ""
174 | }`
175 | ),
176 | ]
177 | ),
178 | View(
179 | {
180 | style: {
181 | marginBottom: "12px",
182 | },
183 | },
184 | [
185 | Text({ bold: true }, "Mail: "),
186 | Text(
187 | {},
188 | `${
189 | props.settingsStorage.getItem("mail")
190 | ? props.settingsStorage.getItem("mail")
191 | : ""
192 | }`
193 | ),
194 | ]
195 | ),
196 | View(
197 | {
198 | style: {
199 | marginBottom: "12px",
200 | },
201 | },
202 | [
203 | Text({ bold: true }, "Product: "),
204 | Text(
205 | {},
206 | `${
207 | props.settingsStorage.getItem("product")
208 | ? props.settingsStorage.getItem("product")
209 | : ""
210 | }`
211 | ),
212 | ]
213 | ),
214 | ]
215 | );
216 | const loginBtn = Link(
217 | {
218 | source: source,
219 | },
220 | [
221 | Button({
222 | label: "Log in",
223 | style: {
224 | ...BtnStyle,
225 | },
226 | }),
227 | ]
228 | );
229 | const logoutBtn = Button({
230 | label: "Log out",
231 | style: {
232 | ...BtnStyle,
233 | },
234 | onClick: () => {
235 | this.logout(props);
236 | },
237 | });
238 | const credits = Link(
239 | {
240 | source: "https://github.com/juan518munoz/ZeppOS-Spotify",
241 | },
242 | [
243 | Text(
244 | {
245 | style: {
246 | display: "flex",
247 | color: "black",
248 | justifyContent: "center",
249 | alignItems: "center",
250 | verticalAlign: "bottom",
251 | position: "fixed",
252 | bottom: "20px",
253 | left: "50%",
254 | transform: "translate(-50%, -50%)",
255 | margin: "0 auto",
256 | },
257 | },
258 | [
259 | Image({
260 | src: "https://github.com/favicon.ico",
261 | style: {
262 | marginRight: "8px",
263 | },
264 | }),
265 | "juan518munoz",
266 | ]
267 | ),
268 | ]
269 | );
270 | const iosBtn = Button({
271 | label: "MANUAL LOG IN",
272 | style: {
273 | ...BtnStyle,
274 | },
275 | onClick: () => {
276 | props.settingsStorage.getItem("iosLogin") == "visible"
277 | ? props.settingsStorage.setItem("iosLogin", "hidden")
278 | : props.settingsStorage.setItem("iosLogin", "visible");
279 | },
280 | });
281 | const manualLogInModal = View(
282 | {
283 | style: {
284 | visibility: props.settingsStorage.getItem("iosLogin") || "hidden",
285 | marginTop: "40px",
286 | },
287 | },
288 | [
289 | Link({ source: manualSource }, "Click here to log in"),
290 | Text(
291 | {},
292 | ", then on the resulting URL then paste the obtained token bellow:"
293 | ),
294 | TextInput({
295 | settingsKey: "authToken",
296 | subStyle: {
297 | marginTop: "4px",
298 | color: "#000000",
299 | fontSize: "15px",
300 | borderStyle: "solid",
301 | borderColor: "#1db954",
302 | borderRadius: "12px",
303 | height: "40px",
304 | overflow: "hidden",
305 | },
306 | onChange: (value) => {
307 | this.getRefreshToken(props, value, manualRedirectUri);
308 | this.getUserInfo(props);
309 | },
310 | }),
311 | ]
312 | );
313 |
314 | return View(
315 | {
316 | style: {
317 | position: "relative",
318 | display: "flex",
319 | flexDirection: "column",
320 | alignItems: "center",
321 | height: "100vh",
322 | padding: "40px",
323 | textTransform: "",
324 | fontFamily: "Circular,Helvetica,Arial,sans-serif",
325 | fontSize: "16px",
326 | fontWeight: "400",
327 | },
328 | },
329 | [
330 | logo,
331 | props.settingsStorage.getItem("refreshToken") == null ||
332 | props.settingsStorage.getItem("refreshToken") == ""
333 | ? [loginBtn, iosBtn, manualLogInModal]
334 | : [userInfo, logoutBtn],
335 | credits,
336 | ]
337 | );
338 | },
339 | });
340 |
--------------------------------------------------------------------------------
/pages/home/index.style.js:
--------------------------------------------------------------------------------
1 | const deviceInfo = hmSetting.getDeviceInfo();
2 | const {
3 | width: DEVICE_WIDTH,
4 | height: DEVICE_HEIGHT,
5 | screenShape,
6 | deviceName,
7 | } = deviceInfo;
8 |
9 | const controlsStart = DEVICE_HEIGHT;
10 | const queueStart = DEVICE_HEIGHT * 2;
11 |
12 | const DEVICES_BUTTON = {
13 | band7: {
14 | x: 0,
15 | y: (DEVICE_HEIGHT * 1) / 4,
16 | w: DEVICE_WIDTH,
17 | h: DEVICE_HEIGHT / 2,
18 | radius: 12,
19 | normal_color: 0x1fdf64,
20 | press_color: 0x1fdf64,
21 | //w: -1,
22 | //h: -1,
23 | //normal_src: 'button_normal.png',
24 | //press_src: 'button_press.png',
25 | text: "devices",
26 | click_func: () => {
27 | hmApp.gotoPage({ url: "pages/devices/devices.page" });
28 | },
29 | },
30 | gts4mini: {
31 | x: 0,
32 | y: (DEVICE_HEIGHT * 1) / 4,
33 | w: DEVICE_WIDTH,
34 | h: DEVICE_HEIGHT / 2,
35 | radius: 12,
36 | normal_color: 0x1fdf64,
37 | press_color: 0x1fdf64,
38 | //w: -1,
39 | //h: -1,
40 | //normal_src: 'button_normal.png',
41 | //press_src: 'button_press.png',
42 | text: "devices",
43 | click_func: () => {
44 | hmApp.gotoPage({ url: "pages/devices/devices.page" });
45 | },
46 | },
47 | gts3: {
48 | x: 0,
49 | y: (DEVICE_HEIGHT * 1) / 4,
50 | w: DEVICE_WIDTH,
51 | h: DEVICE_HEIGHT / 2,
52 | radius: 12,
53 | normal_color: 0x1fdf64,
54 | press_color: 0x1fdf64,
55 | //w: -1,
56 | //h: -1,
57 | //normal_src: 'button_normal.png',
58 | //press_src: 'button_press.png',
59 | text: "devices",
60 | click_func: () => {
61 | hmApp.gotoPage({ url: "pages/devices/devices.page" });
62 | },
63 | },
64 | gtr3: {
65 | x: 0,
66 | y: (DEVICE_HEIGHT * 1) / 4,
67 | w: DEVICE_WIDTH,
68 | h: DEVICE_HEIGHT / 2,
69 | radius: 12,
70 | normal_color: 0x1fdf64,
71 | press_color: 0x1fdf64,
72 | //w: -1,
73 | //h: -1,
74 | //normal_src: 'button_normal.png',
75 | //press_src: 'button_press.png',
76 | text: "devices",
77 | click_func: () => {
78 | hmApp.gotoPage({ url: "pages/devices/devices.page" });
79 | },
80 | },
81 | gts4: {
82 | x: 0,
83 | y: (DEVICE_HEIGHT * 1) / 4,
84 | w: DEVICE_WIDTH,
85 | h: DEVICE_HEIGHT / 2,
86 | radius: 12,
87 | normal_color: 0x1fdf64,
88 | press_color: 0x1fdf64,
89 | //w: -1,
90 | //h: -1,
91 | //normal_src: 'button_normal.png',
92 | //press_src: 'button_press.png',
93 | text: "devices",
94 | click_func: () => {
95 | hmApp.gotoPage({ url: "pages/devices/devices.page" });
96 | },
97 | },
98 | };
99 |
100 | const SONG = {
101 | band7: {
102 | x: 0,
103 | y: px(controlsStart + DEVICE_HEIGHT * 0.12),
104 | w: px(DEVICE_WIDTH),
105 | h: px(50),
106 | color: 0xffffff,
107 | text_size: px(36),
108 | align_h: hmUI.align.CENTER_H,
109 | align_v: hmUI.align.CENTER_V,
110 | text_style: hmUI.text_style.NONE,
111 | text: "",
112 | },
113 | mi_band7: {
114 | x: 0,
115 | y: px(controlsStart + DEVICE_HEIGHT * 0.22),
116 | w: px(DEVICE_WIDTH),
117 | h: px(50),
118 | color: 0xffffff,
119 | text_size: px(36),
120 | align_h: hmUI.align.CENTER_H,
121 | align_v: hmUI.align.CENTER_V,
122 | text_style: hmUI.text_style.NONE,
123 | text: "",
124 | },
125 | gts4mini: {
126 | x: 0,
127 | y: px(controlsStart + DEVICE_HEIGHT * 0.12),
128 | w: px(DEVICE_WIDTH),
129 | h: px(50),
130 | color: 0xffffff,
131 | text_size: px(36),
132 | align_h: hmUI.align.CENTER_H,
133 | align_v: hmUI.align.CENTER_V,
134 | text_style: hmUI.text_style.NONE,
135 | text: "",
136 | },
137 | gts3: {
138 | x: 0,
139 | y: px(controlsStart + DEVICE_HEIGHT * 0.12),
140 | w: px(DEVICE_WIDTH),
141 | h: px(60),
142 | color: 0xffffff,
143 | text_size: px(48),
144 | align_h: hmUI.align.CENTER_H,
145 | align_v: hmUI.align.CENTER_V,
146 | text_style: hmUI.text_style.NONE,
147 | text: "",
148 | },
149 | gtr3: {
150 | x: px(24),
151 | y: px(controlsStart + DEVICE_HEIGHT * 0.12),
152 | w: px(DEVICE_WIDTH - 24),
153 | h: px(60),
154 | color: 0xffffff,
155 | text_size: px(48),
156 | align_h: hmUI.align.CENTER_H,
157 | align_v: hmUI.align.CENTER_V,
158 | text_style: hmUI.text_style.NONE,
159 | text: "",
160 | },
161 | gts4: {
162 | x: 0,
163 | y: px(controlsStart + DEVICE_HEIGHT * 0.12),
164 | w: px(DEVICE_WIDTH),
165 | h: px(60),
166 | color: 0xffffff,
167 | text_size: px(48),
168 | align_h: hmUI.align.CENTER_H,
169 | align_v: hmUI.align.CENTER_V,
170 | text_style: hmUI.text_style.NONE,
171 | text: "",
172 | },
173 | };
174 |
175 | const ARTIST = {
176 | band7: {
177 | x: 0,
178 | y: px(controlsStart + DEVICE_HEIGHT * 0.24),
179 | w: px(DEVICE_WIDTH),
180 | h: px(30),
181 | color: 0xb3b3b3,
182 | text_size: px(24),
183 | align_h: hmUI.align.CENTER_H,
184 | align_v: hmUI.align.CENTER_V,
185 | text_style: hmUI.text_style.NONE,
186 | text: "",
187 | },
188 | mi_band7: {
189 | x: 0,
190 | y: px(controlsStart + DEVICE_HEIGHT * 0.34),
191 | w: px(DEVICE_WIDTH),
192 | h: px(30),
193 | color: 0xb3b3b3,
194 | text_size: px(24),
195 | align_h: hmUI.align.CENTER_H,
196 | align_v: hmUI.align.CENTER_V,
197 | text_style: hmUI.text_style.NONE,
198 | text: "",
199 | },
200 | gts4mini: {
201 | x: 0,
202 | y: px(controlsStart + DEVICE_HEIGHT * 0.24),
203 | w: px(DEVICE_WIDTH),
204 | h: px(30),
205 | color: 0xb3b3b3,
206 | text_size: px(24),
207 | align_h: hmUI.align.CENTER_H,
208 | align_v: hmUI.align.CENTER_V,
209 | text_style: hmUI.text_style.NONE,
210 | text: "",
211 | },
212 | gts3: {
213 | x: 0,
214 | y: px(controlsStart + DEVICE_HEIGHT * 0.24),
215 | w: px(DEVICE_WIDTH),
216 | h: px(40),
217 | color: 0xb3b3b3,
218 | text_size: px(32),
219 | align_h: hmUI.align.CENTER_H,
220 | align_v: hmUI.align.CENTER_V,
221 | text_style: hmUI.text_style.NONE,
222 | text: "",
223 | },
224 | gtr3: {
225 | x: 0,
226 | y: px(controlsStart + DEVICE_HEIGHT * 0.24),
227 | w: px(DEVICE_WIDTH),
228 | h: px(40),
229 | color: 0xb3b3b3,
230 | text_size: px(32),
231 | align_h: hmUI.align.CENTER_H,
232 | align_v: hmUI.align.CENTER_V,
233 | text_style: hmUI.text_style.NONE,
234 | text: "",
235 | },
236 | gts4: {
237 | x: 0,
238 | y: px(controlsStart + DEVICE_HEIGHT * 0.24),
239 | w: px(DEVICE_WIDTH),
240 | h: px(40),
241 | color: 0xb3b3b3,
242 | text_size: px(32),
243 | align_h: hmUI.align.CENTER_H,
244 | align_v: hmUI.align.CENTER_V,
245 | text_style: hmUI.text_style.NONE,
246 | text: "",
247 | },
248 | };
249 |
250 | const PLAY_BUTTON = {
251 | band7: {
252 | x: px(DEVICE_WIDTH / 2) - px(24),
253 | y: px(controlsStart + DEVICE_HEIGHT * 0.55),
254 | },
255 | gts4mini: {
256 | x: px(DEVICE_WIDTH / 2 - 43),
257 | y: px(controlsStart + DEVICE_HEIGHT * 0.42),
258 | },
259 | gts3: {
260 | x: px(DEVICE_WIDTH / 2 - 47),
261 | y: px(controlsStart + DEVICE_HEIGHT * 0.45),
262 | },
263 | gtr3: {
264 | x: px(DEVICE_WIDTH / 2 - 47),
265 | y: px(controlsStart + DEVICE_HEIGHT * 0.5),
266 | },
267 | gts4: {
268 | x: px(DEVICE_WIDTH / 2 - 47),
269 | y: px(controlsStart + DEVICE_HEIGHT * 0.45),
270 | },
271 | };
272 |
273 | const NEXT_BUTTON = {
274 | band7: {
275 | x: px(DEVICE_WIDTH * 0.95 - 48),
276 | y: px(controlsStart + DEVICE_HEIGHT * 0.55),
277 | src: "next.png",
278 | },
279 | gts4mini: {
280 | x: px(DEVICE_WIDTH * 0.95 - 49),
281 | y: px(controlsStart + DEVICE_HEIGHT * 0.47),
282 | src: "next.png",
283 | },
284 | gts3: {
285 | x: px(DEVICE_WIDTH * 0.95 - 73),
286 | y: px(controlsStart + DEVICE_HEIGHT * 0.45 + 12),
287 | src: "next.png",
288 | },
289 | gtr3: {
290 | x: px(DEVICE_WIDTH * 0.95 - 73),
291 | y: px(controlsStart + DEVICE_HEIGHT * 0.5 + 12),
292 | src: "next.png",
293 | },
294 | gts4: {
295 | x: px(DEVICE_WIDTH * 0.95 - 73),
296 | y: px(controlsStart + DEVICE_HEIGHT * 0.45 + 12),
297 | src: "next.png",
298 | },
299 | };
300 |
301 | const PREV_BUTTON = {
302 | band7: {
303 | x: px(DEVICE_WIDTH * 0.05),
304 | y: px(controlsStart + DEVICE_HEIGHT * 0.55),
305 | src: "previous.png",
306 | },
307 | gts4mini: {
308 | x: px(DEVICE_WIDTH * 0.05),
309 | y: px(controlsStart + DEVICE_HEIGHT * 0.47),
310 | src: "previous.png",
311 | },
312 | gts3: {
313 | x: px(DEVICE_WIDTH * 0.05),
314 | y: px(controlsStart + DEVICE_HEIGHT * 0.45 + 12),
315 | src: "previous.png",
316 | },
317 | gtr3: {
318 | x: px(DEVICE_WIDTH * 0.05),
319 | y: px(controlsStart + DEVICE_HEIGHT * 0.5 + 12),
320 | src: "previous.png",
321 | },
322 | gts4: {
323 | x: px(DEVICE_WIDTH * 0.05),
324 | y: px(controlsStart + DEVICE_HEIGHT * 0.45 + 12),
325 | src: "previous.png",
326 | },
327 | };
328 |
329 | const PROGRESS_BAR_BKG = {
330 | band7: {
331 | x: px(8),
332 | y: px(controlsStart + DEVICE_HEIGHT * 0.4),
333 | w: px(DEVICE_WIDTH - 8),
334 | h: px(4),
335 | radius: px(2),
336 | color: 0x5e5e5e,
337 | },
338 | mi_band7: {
339 | x: px(8),
340 | y: px(controlsStart + DEVICE_HEIGHT * 0.45),
341 | w: px(DEVICE_WIDTH - 8),
342 | h: px(4),
343 | radius: px(2),
344 | color: 0x5e5e5e,
345 | },
346 | gts4mini: {
347 | x: px(8),
348 | y: px(controlsStart + DEVICE_HEIGHT * 0.35),
349 | w: px(DEVICE_WIDTH) - px(16),
350 | h: px(8),
351 | radius: px(4),
352 | color: 0x5e5e5e,
353 | },
354 | gts3: {
355 | x: px(8),
356 | y: px(controlsStart + DEVICE_HEIGHT * 0.38),
357 | w: px(DEVICE_WIDTH - 16),
358 | h: px(8),
359 | radius: px(4),
360 | color: 0x5e5e5e,
361 | },
362 | gtr3: {
363 | x: px(8),
364 | y: px(controlsStart + DEVICE_HEIGHT * 0.4),
365 | w: px(DEVICE_WIDTH - 16),
366 | h: px(8),
367 | radius: px(4),
368 | color: 0x5e5e5e,
369 | },
370 | gts4: {
371 | x: px(8),
372 | y: px(controlsStart + DEVICE_HEIGHT * 0.38),
373 | w: px(DEVICE_WIDTH - 16),
374 | h: px(8),
375 | radius: px(4),
376 | color: 0x5e5e5e,
377 | },
378 | };
379 |
380 | const PROGRESS_BAR = {
381 | band7: {
382 | x: px(8),
383 | y: px(controlsStart + DEVICE_HEIGHT * 0.4),
384 | h: px(4),
385 | radius: px(2),
386 | color: 0x1db954,
387 | },
388 | mi_band7: {
389 | x: px(8),
390 | y: px(controlsStart + DEVICE_HEIGHT * 0.45),
391 | h: px(4),
392 | radius: px(2),
393 | color: 0x1db954,
394 | },
395 | gts4mini: {
396 | x: px(8),
397 | y: px(controlsStart + DEVICE_HEIGHT * 0.35),
398 | w: px(0), // doesnt work
399 | h: px(8),
400 | radius: px(4),
401 | color: 0x1db954,
402 | },
403 | gts3: {
404 | x: px(8),
405 | y: px(controlsStart + DEVICE_HEIGHT * 0.38),
406 | h: px(8),
407 | radius: px(4),
408 | color: 0x1db954,
409 | },
410 | gtr3: {
411 | x: px(8),
412 | y: px(controlsStart + DEVICE_HEIGHT * 0.4),
413 | h: px(8),
414 | radius: px(4),
415 | color: 0x1db954,
416 | },
417 | gts4: {
418 | x: px(8),
419 | y: px(controlsStart + DEVICE_HEIGHT * 0.38),
420 | h: px(8),
421 | radius: px(4),
422 | color: 0x1db954,
423 | },
424 | };
425 |
426 | const LIKE_BUTTON = {
427 | band7: {
428 | x: DEVICE_WIDTH / 2 - px(68),
429 | y: controlsStart + DEVICE_HEIGHT,
430 | },
431 | mi_band7: {
432 | x: DEVICE_WIDTH / 2 - px(68),
433 | y: controlsStart + DEVICE_HEIGHT * 0.70,
434 | },
435 | gts4mini: {
436 | x: DEVICE_WIDTH / 2 - px(68) - px(30),
437 | y: controlsStart + DEVICE_HEIGHT - px(78) - px(28),
438 | },
439 | gts3: {
440 | x: DEVICE_WIDTH / 2 - px(68) - px(30),
441 | y: controlsStart + DEVICE_HEIGHT - px(78) - px(44),
442 | },
443 | gtr3: {
444 | x: DEVICE_WIDTH / 2 - px(68) - px(38),
445 | y: controlsStart + DEVICE_HEIGHT - px(78) - px(20),
446 | },
447 | gts4: {
448 | x: DEVICE_WIDTH / 2 - px(68) - px(40),
449 | y: controlsStart + DEVICE_HEIGHT - px(78) - px(40),
450 | },
451 | };
452 |
453 | const SHUFFLE_BUTTON = {
454 | band7: {
455 | x: DEVICE_WIDTH / 2 + px(20),
456 | y: controlsStart + DEVICE_HEIGHT + px(4),
457 | },
458 | mi_band7: {
459 | x: DEVICE_WIDTH / 2 + px(20),
460 | y: controlsStart + DEVICE_HEIGHT * 0.70 + px(4),
461 | },
462 | gts4mini: {
463 | x: DEVICE_WIDTH / 2 + px(20) + px(30),
464 | y: controlsStart + DEVICE_HEIGHT - px(74) - px(28),
465 | },
466 | gts3: {
467 | x: DEVICE_WIDTH / 2 + px(20) + px(30),
468 | y: controlsStart + DEVICE_HEIGHT - px(74) - px(44),
469 | },
470 | gtr3: {
471 | x: DEVICE_WIDTH / 2 + px(20) + px(38),
472 | y: controlsStart + DEVICE_HEIGHT - px(74) - px(20),
473 | },
474 | gts4: {
475 | x: DEVICE_WIDTH / 2 + px(20) + px(40),
476 | y: controlsStart + DEVICE_HEIGHT - px(74) - px(40),
477 | },
478 | };
479 |
480 | const SPOTIFY_ICON = {
481 | band7: {
482 | x: DEVICE_WIDTH / 2 - px(24),
483 | y: controlsStart + DEVICE_HEIGHT - px(78),
484 | src: "player_icon.png",
485 | },
486 | mi_band7: {
487 | x: DEVICE_WIDTH / 2 - px(24),
488 | y: controlsStart + DEVICE_HEIGHT - px(56),
489 | src: "player_icon.png",
490 | },
491 | gts4mini: {
492 | x: DEVICE_WIDTH / 2 - px(24),
493 | y: controlsStart + DEVICE_HEIGHT - px(24) - px(46),
494 | src: "player_icon.png",
495 | },
496 | gts3: {
497 | x: DEVICE_WIDTH / 2 - px(24),
498 | y: controlsStart + DEVICE_HEIGHT - px(24) - px(40),
499 | src: "player_icon.png",
500 | },
501 | gtr3: {
502 | x: DEVICE_WIDTH / 2 - px(24),
503 | y: controlsStart + DEVICE_HEIGHT - px(24) - px(28),
504 | src: "player_icon.png",
505 | },
506 | gts4: {
507 | x: DEVICE_WIDTH / 2 - px(24),
508 | y: controlsStart + DEVICE_HEIGHT - px(24) - px(42),
509 | src: "player_icon.png",
510 | },
511 | };
512 |
513 | const QUEUED_SONG = {
514 | band7: {
515 | x: 0,
516 | //y: px(DEVICE_HEIGHT + 70 * (i + 1)),
517 | w: px(DEVICE_WIDTH),
518 | h: px(30),
519 | color: 0xffffff,
520 | text_size: px(24),
521 | align_h: hmUI.align.LEFT,
522 | align_v: hmUI.align.CENTER_V,
523 | text_style: hmUI.text_style.ELLIPSIS,
524 | text: "",
525 | },
526 | gts4mini: {
527 | x: 0,
528 | //y: px(DEVICE_HEIGHT + 70 * (i + 1)),
529 | w: px(DEVICE_WIDTH),
530 | h: px(32),
531 | color: 0xffffff,
532 | text_size: px(30),
533 | align_h: hmUI.align.LEFT,
534 | align_v: hmUI.align.CENTER_V,
535 | text_style: hmUI.text_style.ELLIPSIS,
536 | text: "",
537 | },
538 | gts3: {
539 | x: 0,
540 | //y: px(DEVICE_HEIGHT + 70 * (i + 1)),
541 | w: px(DEVICE_WIDTH),
542 | h: px(48),
543 | color: 0xffffff,
544 | text_size: px(36),
545 | align_h: hmUI.align.LEFT,
546 | align_v: hmUI.align.CENTER_V,
547 | text_style: hmUI.text_style.ELLIPSIS,
548 | text: "",
549 | },
550 | gtr3: {
551 | x: 0,
552 | //y: px(DEVICE_HEIGHT + 70 * (i + 1)),
553 | w: px(DEVICE_WIDTH),
554 | h: px(48),
555 | color: 0xffffff,
556 | text_size: px(36),
557 | align_h: hmUI.align.LEFT,
558 | align_v: hmUI.align.CENTER_V,
559 | text_style: hmUI.text_style.ELLIPSIS,
560 | text: "",
561 | },
562 | gts4: {
563 | x: 0,
564 | //y: px(DEVICE_HEIGHT + 70 * (i + 1)),
565 | w: px(DEVICE_WIDTH),
566 | h: px(48),
567 | color: 0xffffff,
568 | text_size: px(36),
569 | align_h: hmUI.align.LEFT,
570 | align_v: hmUI.align.CENTER_V,
571 | text_style: hmUI.text_style.ELLIPSIS,
572 | text: "",
573 | },
574 | };
575 |
576 | export const getStyles = (deviceName) => {
577 | if (deviceName == "Amazfit Band 7")
578 | return {
579 | DEVICES_BUTTON: DEVICES_BUTTON.band7,
580 | SONG: SONG.band7,
581 | ARTIST: ARTIST.band7,
582 | PLAY_BUTTON: PLAY_BUTTON.band7,
583 | NEXT_BUTTON: NEXT_BUTTON.band7,
584 | PREV_BUTTON: PREV_BUTTON.band7,
585 | PROGRESS_BAR_BKG: PROGRESS_BAR_BKG.band7,
586 | PROGRESS_BAR: PROGRESS_BAR.band7,
587 | LIKE_BUTTON: LIKE_BUTTON.band7,
588 | SHUFFLE_BUTTON: SHUFFLE_BUTTON.band7,
589 | SPOTIFY_ICON: SPOTIFY_ICON.band7,
590 | QUEUED_SONG: QUEUED_SONG.band7,
591 | };
592 | else if (deviceName == "Xiaomi Smart Band 7")
593 | return {
594 | DEVICES_BUTTON: DEVICES_BUTTON.band7,
595 | SONG: SONG.mi_band7,
596 | ARTIST: ARTIST.mi_band7,
597 | PLAY_BUTTON: PLAY_BUTTON.band7,
598 | NEXT_BUTTON: NEXT_BUTTON.band7,
599 | PREV_BUTTON: PREV_BUTTON.band7,
600 | PROGRESS_BAR_BKG: PROGRESS_BAR_BKG.mi_band7,
601 | PROGRESS_BAR: PROGRESS_BAR.mi_band7,
602 | LIKE_BUTTON: LIKE_BUTTON.mi_band7,
603 | SHUFFLE_BUTTON: SHUFFLE_BUTTON.mi_band7,
604 | SPOTIFY_ICON: SPOTIFY_ICON.mi_band7,
605 | QUEUED_SONG: QUEUED_SONG.band7,
606 | };
607 | else if (deviceName == "GTS 4 mini")
608 | return {
609 | DEVICES_BUTTON: DEVICES_BUTTON.gts4mini,
610 | SONG: SONG.gts4mini,
611 | ARTIST: ARTIST.gts4mini,
612 | PLAY_BUTTON: PLAY_BUTTON.gts4mini,
613 | NEXT_BUTTON: NEXT_BUTTON.gts4mini,
614 | PREV_BUTTON: PREV_BUTTON.gts4mini,
615 | PROGRESS_BAR_BKG: PROGRESS_BAR_BKG.gts4mini,
616 | PROGRESS_BAR: PROGRESS_BAR.gts4mini,
617 | LIKE_BUTTON: LIKE_BUTTON.gts4mini,
618 | SHUFFLE_BUTTON: SHUFFLE_BUTTON.gts4mini,
619 | SPOTIFY_ICON: SPOTIFY_ICON.gts4mini,
620 | QUEUED_SONG: QUEUED_SONG.gts4mini,
621 | };
622 | else if (deviceName == "GTS 3")
623 | return {
624 | DEVICES_BUTTON: DEVICES_BUTTON.gts3,
625 | SONG: SONG.gts3,
626 | ARTIST: ARTIST.gts3,
627 | PLAY_BUTTON: PLAY_BUTTON.gts3,
628 | NEXT_BUTTON: NEXT_BUTTON.gts3,
629 | PREV_BUTTON: PREV_BUTTON.gts3,
630 | PROGRESS_BAR_BKG: PROGRESS_BAR_BKG.gts3,
631 | PROGRESS_BAR: PROGRESS_BAR.gts3,
632 | LIKE_BUTTON: LIKE_BUTTON.gts3,
633 | SHUFFLE_BUTTON: SHUFFLE_BUTTON.gts3,
634 | SPOTIFY_ICON: SPOTIFY_ICON.gts3,
635 | QUEUED_SONG: QUEUED_SONG.gts3,
636 | };
637 | else if (deviceName == "GTS 4")
638 | return {
639 | DEVICES_BUTTON: DEVICES_BUTTON.gts4,
640 | SONG: SONG.gts4,
641 | ARTIST: ARTIST.gts4,
642 | PLAY_BUTTON: PLAY_BUTTON.gts4,
643 | NEXT_BUTTON: NEXT_BUTTON.gts4,
644 | PREV_BUTTON: PREV_BUTTON.gts4,
645 | PROGRESS_BAR_BKG: PROGRESS_BAR_BKG.gts4,
646 | PROGRESS_BAR: PROGRESS_BAR.gts4,
647 | LIKE_BUTTON: LIKE_BUTTON.gts4,
648 | SHUFFLE_BUTTON: SHUFFLE_BUTTON.gts4,
649 | SPOTIFY_ICON: SPOTIFY_ICON.gts4,
650 | QUEUED_SONG: QUEUED_SONG.gts4,
651 | };
652 | return {
653 | // gtr 3
654 | DEVICES_BUTTON: DEVICES_BUTTON.gtr3,
655 | SONG: SONG.gtr3,
656 | ARTIST: ARTIST.gtr3,
657 | PLAY_BUTTON: PLAY_BUTTON.gtr3,
658 | NEXT_BUTTON: NEXT_BUTTON.gtr3,
659 | PREV_BUTTON: PREV_BUTTON.gtr3,
660 | PROGRESS_BAR_BKG: PROGRESS_BAR_BKG.gtr3,
661 | PROGRESS_BAR: PROGRESS_BAR.gtr3,
662 | LIKE_BUTTON: LIKE_BUTTON.gtr3,
663 | SHUFFLE_BUTTON: SHUFFLE_BUTTON.gtr3,
664 | SPOTIFY_ICON: SPOTIFY_ICON.gtr3,
665 | QUEUED_SONG: QUEUED_SONG.gtr3,
666 | };
667 | };
668 |
--------------------------------------------------------------------------------
/shared/message.js:
--------------------------------------------------------------------------------
1 | import './buffer'
2 | import './logger'
3 | import { EventBus } from './event'
4 | import { Deferred, timeout } from './defer'
5 | import { json2Buf, buf2Json, buf2hex, bin2hex, bin2json, str2buf } from './data'
6 | import { isHmBleDefined, isHmAppDefined } from './js-module'
7 |
8 | let logger
9 |
10 | if (isHmAppDefined()) {
11 | logger = Logger.getLogger('device-message')
12 | // logger.level = logger.levels.warn
13 | } else {
14 | logger = Logger.getLogger('side-message')
15 | }
16 |
17 | const DEBUG = false
18 |
19 | export const MessageFlag = {
20 | App: 0x1,
21 | }
22 |
23 | export const MessageType = {
24 | Shake: 0x1,
25 | Close: 0x2,
26 | Heart: 0x3,
27 | Data: 0x4,
28 | DataWithSystemTool: 0x5,
29 | Log: 0x6,
30 | }
31 |
32 | export const MessageVersion = {
33 | Version1: 0x1,
34 | }
35 |
36 | export const MessagePayloadType = {
37 | Request: 0x1,
38 | Response: 0x2,
39 | Notify: 0x3,
40 | }
41 |
42 | // 中续,结束
43 | export const MessagePayloadOpCode = {
44 | Continued: 0x0,
45 | Finished: 0x1,
46 | }
47 |
48 | let traceId = 10000
49 | export function genTraceId() {
50 | return traceId++
51 | }
52 |
53 | let spanId = 1000
54 | export function genSpanId() {
55 | return spanId++
56 | }
57 |
58 | export function getTimestamp(t = Date.now()) {
59 | return t % 10000000
60 | }
61 |
62 | class Session extends EventBus {
63 | constructor(id, type, ctx) {
64 | super()
65 | this.id = id
66 | this.type = type // payloadType
67 | this.ctx = ctx
68 | this.tempBuf = null
69 | this.chunks = []
70 | this.count = 0
71 | this.finishChunk = null
72 | }
73 |
74 | addChunk(payload) {
75 | if (payload.opCode === MessagePayloadOpCode.Finished) {
76 | this.count = payload.seqId
77 | this.finishChunk = payload
78 | }
79 |
80 | if (payload.payloadLength !== payload.payload.byteLength) {
81 | // logger.error('receive chunk data length error, expect %d but %d', payload.payloadLength, payload.payload.byteLength)
82 | this.emit('error', Error(`receive chunk data length error, expect ${payload.payloadLength} but ${payload.payload.byteLength}`))
83 | return
84 | }
85 |
86 | this.chunks.push(payload)
87 | this.checkIfReceiveAllChunks()
88 | }
89 |
90 | checkIfReceiveAllChunks() {
91 | if (this.count !== this.chunks.length) return
92 |
93 | for (let i = 1; i <= this.count; i++) {
94 | const chunk = this.chunks.find(c => c.seqId === i)
95 |
96 | if (!chunk) {
97 | this.releaseBuf()
98 | this.emit('error', Error('receive data error'))
99 | return
100 | }
101 |
102 | const buf = chunk.payload
103 | this.tempBuf = this.tempBuf ? Buffer.concat([this.tempBuf, buf]) : buf
104 | }
105 |
106 | if (!this.finishChunk) return
107 |
108 | this.finishChunk.payload = this.tempBuf
109 | this.finishChunk.payloadLength = this.finishChunk.payload.byteLength
110 |
111 | if (this.finishChunk.totalLength !== this.finishChunk.payloadLength) {
112 | // logger.error('receive full data length error, expect %d but %d', this.finishChunk.payloadLength, this.finishChunk.payload.byteLength)
113 | this.emit('error', Error(`receive full data length error, expect ${this.finishChunk.payloadLength} but ${this.finishChunk.payload.byteLength}`))
114 | return
115 | }
116 |
117 | this.emit('data', this.finishChunk)
118 | }
119 |
120 | getLength() {
121 | return this.tempBufLength
122 | }
123 | releaseBuf() {
124 | this.tempBuf = null
125 | this.chunks = []
126 | this.finishChunk = null
127 | this.count = 0
128 | }
129 | }
130 |
131 | class SessionMgr {
132 | constructor() {
133 | this.sessions = new Map()
134 | }
135 |
136 | key(session) {
137 | return `${session.id}:${session.type}`
138 | }
139 |
140 | newSession(id, type, ctx) {
141 | const newSession = new Session(id, type, ctx)
142 | this.sessions.set(this.key(newSession), newSession)
143 | return newSession
144 | }
145 |
146 | destroy(session) {
147 | session.releaseBuf()
148 | this.sessions.delete(this.key(session))
149 | }
150 |
151 | has(id, type) {
152 | return this.sessions.has(this.key({ id, type }))
153 | }
154 |
155 | getById(id, type) {
156 | return this.sessions.get(this.key({ id, type }))
157 | }
158 |
159 | clear() {
160 | this.sessions.clear()
161 | }
162 | }
163 |
164 | export class MessageBuilder extends EventBus {
165 | constructor({ appId = 0, appDevicePort = 20, appSidePort = 0 } = {
166 | appId: 0,
167 | appDevicePort: 20,
168 | appSidePort: 0,
169 | }, ) {
170 | super()
171 | this.isDevice = isHmBleDefined()
172 | this.isSide = !this.isDevice
173 |
174 | this.appId = appId
175 | this.appDevicePort = appDevicePort
176 | this.appSidePort = appSidePort
177 | this.sendMsg = this.getSafeSend()
178 | this.chunkSize = 2000
179 | this.tempBuf = null
180 | this.shakeTask = Deferred()
181 | this.waitingShakePromise = this.shakeTask.promise
182 | this.sessionMgr = new SessionMgr()
183 |
184 | // 打开
185 | // console.log('jsfwk.error', logger.eventBus.count())
186 | // logger.warn('init=>',logger.eventBus.count())
187 | if (isHmAppDefined() && DEBUG) {
188 | // logger.level = logger.levels.all
189 | logger.connect({
190 | log: (logEvent) => {
191 | this.log(JSON.stringify(logEvent))
192 | // console.log('jsfwk.error',JSON.stringify(logEvent))
193 | }
194 | })
195 | }
196 | }
197 |
198 | now(t = Date.now()) {
199 | return getTimestamp(t)
200 | }
201 |
202 | connect(cb) {
203 | this.on('message', (message) => {
204 | this.onMessage(message)
205 | })
206 |
207 | hmBle &&
208 | hmBle.createConnect((index, data, size) => {
209 | // logger.warn('[RAW] [R] receive index=>%d size=>%d bin=>%s', index, size, this.bin2hex(data))
210 | console.log('createConnect-------', size)
211 | this.onFragmentData(data)
212 | })
213 |
214 | this.sendShake()
215 | cb && cb(this)
216 | }
217 |
218 | disConnect(cb) {
219 | // logger.debug('app ble disconnect')
220 | this.sendClose()
221 | this.off('message')
222 | hmBle && hmBle.disConnect()
223 |
224 | cb && cb(this)
225 | }
226 |
227 | listen(cb) {
228 | messaging &&
229 | messaging.peerSocket.addListener('message', (message) => {
230 | // logger.warn('[RAW] [R] receive size=>%d bin=>%s', message.byteLength, this.bin2hex(message))
231 | this.onMessage(message)
232 | })
233 |
234 | this.waitingShakePromise = Promise.resolve()
235 | cb && cb(this)
236 | }
237 |
238 | buildBin(data) {
239 | const size = 16 + data.payload.byteLength
240 | let buf = Buffer.alloc(size)
241 | let offset = 0
242 |
243 | buf.writeUInt8(data.flag, offset)
244 | offset += 1
245 |
246 | buf.writeUInt8(data.version, offset)
247 | offset += 1
248 |
249 | buf.writeUInt16LE(data.type, offset)
250 | offset += 2
251 |
252 | buf.writeUInt16LE(data.port1, offset)
253 | offset += 2
254 |
255 | buf.writeUInt16LE(data.port2, offset)
256 | offset += 2
257 |
258 | buf.writeUInt32LE(data.appId, offset)
259 | offset += 4
260 |
261 | buf.writeUInt32LE(data.extra, offset)
262 | offset += 4
263 |
264 | buf.fill(data.payload, offset, data.payload.byteLength + offset)
265 |
266 | return buf
267 | }
268 |
269 | buildShake() {
270 | return this.buildBin({
271 | flag: MessageFlag.App,
272 | version: MessageVersion.Version1,
273 | type: MessageType.Shake,
274 | port1: this.appDevicePort,
275 | port2: this.appSidePort,
276 | appId: this.appId,
277 | extra: 0,
278 | payload: Buffer.from([this.appId]),
279 | })
280 | }
281 |
282 | sendShake() {
283 | if (this.appSidePort === 0) {
284 | const shake = this.buildShake()
285 | this.sendMsg(shake)
286 | }
287 | }
288 |
289 | buildClose() {
290 | return this.buildBin({
291 | flag: MessageFlag.App,
292 | version: MessageVersion.Version1,
293 | type: MessageType.Close,
294 | port1: this.appDevicePort,
295 | port2: this.appSidePort,
296 | appId: this.appId,
297 | extra: 0,
298 | payload: Buffer.from([this.appId]),
299 | })
300 | }
301 |
302 | sendClose() {
303 | if (this.appSidePort !== 0) {
304 | const close = this.buildClose()
305 |
306 | this.sendMsg(close)
307 | }
308 | }
309 |
310 | readBin(arrayBuf) {
311 | const buf = Buffer.from(arrayBuf)
312 | let offset = 0
313 |
314 | const flag = buf.readUInt8(offset)
315 | offset += 1
316 |
317 | const version = buf.readUInt8(offset)
318 | offset += 1
319 |
320 | const type = buf.readUInt16LE(offset)
321 | offset += 2
322 |
323 | const port1 = buf.readUInt16LE(offset)
324 | offset += 2
325 |
326 | const port2 = buf.readUInt16LE(offset)
327 | offset += 2
328 |
329 | const appId = buf.readUInt32LE(offset)
330 | offset += 4
331 |
332 | const extra = buf.readUInt32LE(offset)
333 | offset += 4
334 |
335 | const payload = buf.subarray(offset)
336 |
337 | return {
338 | flag,
339 | version,
340 | type,
341 | port1,
342 | port2,
343 | appId,
344 | extra,
345 | payload,
346 | }
347 | }
348 |
349 | // opts 覆盖头部选项
350 | buildData(payload, opts = {}) {
351 | return this.buildBin({
352 | flag: MessageFlag.App,
353 | version: MessageVersion.Version1,
354 | type: MessageType.Data,
355 | port1: this.appDevicePort,
356 | port2: this.appSidePort,
357 | appId: this.appId,
358 | extra: 0,
359 | ...opts,
360 | payload,
361 | })
362 | }
363 |
364 | json2Buf(obj) {
365 | return json2Buf(obj)
366 | }
367 |
368 | buf2Json(buf) {
369 | return buf2Json(buf)
370 | }
371 |
372 | buf2hex(buf) {
373 | return buf2hex(buf)
374 | }
375 |
376 | bin2hex(bin) {
377 | return bin2hex(bin)
378 | }
379 |
380 | bin2json(bin) {
381 | return bin2json(bin)
382 | }
383 |
384 | sendBin(buf) {
385 | // hmBle 发送消息
386 | // logger.warn('[RAW] [S] send size=%d bin=%s', buf.byteLength, this.bin2hex(buf.buffer))
387 | console.log('sendBin-------', buf.byteLength)
388 | hmBle.send(buf.buffer, buf.byteLength)
389 | }
390 |
391 | sendBinBySide(buf) {
392 | // side 发送消息
393 | // logger.warn('[RAW] [S] send size=%d bin=%s', buf.byteLength, this.bin2hex(buf.buffer))
394 | messaging.peerSocket.send(buf.buffer)
395 | }
396 |
397 | // 通用获取逻辑
398 | getSafeSend() {
399 | if (this.isDevice) {
400 | return this.sendBin.bind(this)
401 | } else {
402 | return this.sendBinBySide.bind(this)
403 | }
404 | }
405 |
406 | _logSend(buf) {
407 | // 日志的 send 里面不要打日志
408 | if (this.isDevice) {
409 | hmBle.send(buf.buffer, buf.byteLength)
410 | } else {
411 | messaging.peerSocket.send(buf.buffer)
412 | }
413 | }
414 |
415 | // 大数据的复杂头部分包协议
416 | sendHmProtocol({ requestId, dataBin, type }, { messageType = MessageType.Data } = {}) {
417 | const dataSize = this.chunkSize
418 | const headerSize = 0
419 | const userDataLength = dataBin.byteLength
420 |
421 | let offset = 0
422 | const _buf = Buffer.alloc(dataSize)
423 | const traceId = requestId ? requestId : genTraceId()
424 | const spanId = genSpanId()
425 | let seqId = 1
426 |
427 | const count = Math.ceil(userDataLength / dataSize)
428 |
429 | function genSeqId() {
430 | return seqId++
431 | }
432 |
433 | for (let i = 1; i <= count; i++) {
434 | if (i === count) {
435 | // last
436 | const tailSize = userDataLength - offset
437 | const tailBuf = Buffer.alloc(headerSize + tailSize)
438 |
439 | dataBin.copy(tailBuf, headerSize, offset, offset + tailSize)
440 | offset += tailSize
441 | this.sendDataWithSession({
442 | traceId,
443 | spanId: spanId,
444 | seqId: genSeqId(),
445 | payload: tailBuf,
446 | type,
447 | opCode: MessagePayloadOpCode.Finished,
448 | totalLength: userDataLength,
449 | }, { messageType })
450 |
451 | break
452 | }
453 |
454 | dataBin.copy(_buf, headerSize, offset, offset + dataSize)
455 | offset += dataSize
456 |
457 | this.sendDataWithSession({
458 | traceId,
459 | spanId: spanId,
460 | seqId: genSeqId(),
461 | payload: _buf,
462 | type,
463 | opCode: MessagePayloadOpCode.Continued,
464 | totalLength: userDataLength,
465 | }, { messageType })
466 | }
467 |
468 | if (offset === userDataLength) {
469 | // logger.debug('HmProtocol send ok msgSize=> %d dataSize=> %d', offset, userDataLength)
470 | } else {
471 | // logger.error('HmProtocol send error msgSize=> %d dataSize=> %d', offset, userDataLength)
472 | }
473 | }
474 |
475 | // 大数据的简单分包协议
476 | sendSimpleProtocol({ dataBin }, { messageType = MessageType.Data } = {}) {
477 | const dataSize = this.chunkSize
478 | const headerSize = 0
479 | const userDataLength = dataBin.byteLength
480 |
481 | let offset = 0
482 | const _buf = Buffer.alloc(dataSize)
483 |
484 | const count = Math.ceil(userDataLength / dataSize)
485 |
486 | for (let i = 1; i <= count; i++) {
487 | if (i === count) {
488 | // last
489 | const tailSize = userDataLength - offset
490 | const tailBuf = Buffer.alloc(headerSize + tailSize)
491 |
492 | dataBin.copy(tailBuf, headerSize, offset, offset + tailSize)
493 | offset += tailSize
494 | this.sendSimpleData({ payload: tailBuf }, { messageType })
495 |
496 | break
497 | }
498 |
499 | dataBin.copy(_buf, headerSize, offset, offset + dataSize)
500 | offset += dataSize
501 |
502 | this.sendSimpleData({ payload: _buf }, { messageType })
503 | }
504 |
505 | if (offset === userDataLength) {
506 | // logger.debug('SimpleProtocol send ok msgSize=> %d dataSize=> %d', offset, userDataLength)
507 | } else {
508 | // logger.error('SimpleProtocol send error msgSize=> %d dataSize=> %d', offset, userDataLength)
509 | }
510 | }
511 |
512 | sendJson({ requestId = 0, json, type = MessagePayloadType.Request }) {
513 | const packageBin = this.json2Buf(json)
514 | const traceId = requestId ? requestId : genTraceId()
515 |
516 | this.sendHmProtocol({ requestId: traceId, dataBin: packageBin, type })
517 | }
518 |
519 | sendLog(str) {
520 | const packageBuf = str2buf(str)
521 |
522 | this.sendSimpleProtocol({ dataBin: packageBuf }, { messageType: MessageType.Log })
523 | }
524 |
525 | sendDataWithSession({ traceId, spanId, seqId, payload, type, opCode, totalLength }, { messageType }, ) {
526 | const payloadBin = this.buildPayload({
527 | traceId,
528 | spanId,
529 | seqId,
530 | totalLength,
531 | type,
532 | opCode,
533 | payload,
534 | })
535 |
536 | let data = this.isDevice ? this.buildData(payloadBin, { type: messageType }) : payloadBin
537 |
538 | this.sendMsg(data)
539 | }
540 |
541 | sendSimpleData({ payload }, { messageType }) {
542 | let data = this.isDevice ? this.buildData(payload, { type: messageType }) : payload
543 |
544 | this._logSend(data)
545 | }
546 |
547 | buildPayload(data) {
548 | const size = 66 + data.payload.byteLength
549 | let buf = Buffer.alloc(size)
550 | let offset = 0
551 |
552 | // header
553 | // traceId
554 | buf.writeUInt32LE(data.traceId, offset)
555 | offset += 4
556 |
557 | // parentId
558 | buf.writeUInt32LE(0, offset)
559 | offset += 4
560 |
561 | // spanId
562 | buf.writeUInt32LE(data.spanId, offset)
563 | offset += 4
564 |
565 | // seqId // 顺序 id,消息部分顺序序列号
566 | buf.writeUInt32LE(data.seqId, offset)
567 | offset += 4
568 |
569 | // message total length
570 | buf.writeUInt32LE(data.totalLength, offset)
571 | offset += 4
572 |
573 | // payload length 当前
574 | buf.writeUInt32LE(data.payload.byteLength, offset)
575 | offset += 4
576 |
577 | // payload type
578 | buf.writeUInt8(data.type, offset)
579 | offset += 1
580 |
581 | // opCode
582 | buf.writeUInt8(data.opCode, offset)
583 | offset += 1
584 |
585 | // timestamp1
586 | buf.writeUInt32LE(this.now(), offset)
587 | offset += 4
588 |
589 | // timestamp2
590 | buf.writeUInt32LE(0, offset)
591 | offset += 4
592 |
593 | // timestamp3
594 | buf.writeUInt32LE(0, offset)
595 | offset += 4
596 |
597 | // timestamp4
598 | buf.writeUInt32LE(0, offset)
599 | offset += 4
600 |
601 | // timestamp5
602 | buf.writeUInt32LE(0, offset)
603 | offset += 4
604 |
605 | // timestamp6
606 | buf.writeUInt32LE(0, offset)
607 | offset += 4
608 |
609 | // timestamp7
610 | buf.writeUInt32LE(0, offset)
611 | offset += 4
612 |
613 | // timestamp8
614 | buf.writeUInt32LE(0, offset)
615 | offset += 4
616 |
617 | // extra1
618 | buf.writeUInt32LE(0, offset)
619 | offset += 4
620 |
621 | // extra2
622 | buf.writeUInt32LE(0, offset)
623 | offset += 4
624 |
625 | // payload
626 | buf.fill(data.payload, offset, data.payload.byteLength + offset)
627 |
628 | return buf
629 | }
630 |
631 | readPayload(arrayBuf) {
632 | const buf = Buffer.from(arrayBuf)
633 | let offset = 0
634 |
635 | const traceId = buf.readUInt32LE(offset)
636 | offset += 4
637 |
638 | const parentId = buf.readUInt32LE(offset)
639 | offset += 4
640 |
641 | const spanId = buf.readUInt32LE(offset)
642 | offset += 4
643 |
644 | const seqId = buf.readUInt32LE(offset)
645 | offset += 4
646 |
647 | const totalLength = buf.readUInt32LE(offset)
648 | offset += 4
649 |
650 | const payloadLength = buf.readUInt32LE(offset)
651 | offset += 4
652 |
653 | const payloadType = buf.readUInt8(offset)
654 | offset += 1
655 |
656 | const opCode = buf.readUInt8(offset)
657 | offset += 1
658 |
659 | const timestamp1 = buf.readUInt32LE(offset)
660 | offset += 4
661 |
662 | const timestamp2 = buf.readUInt32LE(offset)
663 | offset += 4
664 |
665 | const timestamp3 = buf.readUInt32LE(offset)
666 | offset += 4
667 |
668 | const timestamp4 = buf.readUInt32LE(offset)
669 | offset += 4
670 |
671 | const timestamp5 = buf.readUInt32LE(offset)
672 | offset += 4
673 |
674 | const timestamp6 = buf.readUInt32LE(offset)
675 | offset += 4
676 |
677 | const timestamp7 = buf.readUInt32LE(offset)
678 | offset += 4
679 |
680 | const timestamp8 = buf.readUInt32LE(offset)
681 | offset += 4
682 |
683 | const extra1 = buf.readUInt32LE(offset)
684 | offset += 4
685 |
686 | const extra2 = buf.readUInt32LE(offset)
687 | offset += 4
688 |
689 | const payload = buf.subarray(offset)
690 |
691 | return {
692 | traceId,
693 | parentId,
694 | spanId,
695 | seqId,
696 | totalLength,
697 | payloadLength,
698 | payloadType,
699 | opCode,
700 | timestamp1,
701 | timestamp2,
702 | timestamp3,
703 | timestamp4,
704 | timestamp5,
705 | timestamp6,
706 | timestamp7,
707 | timestamp8,
708 | extra1,
709 | extra2,
710 | payload,
711 | }
712 | }
713 |
714 | onFragmentData(bin) {
715 | const data = this.readBin(bin)
716 | this.emit('raw', bin)
717 |
718 | // logger.debug('receive data=>', JSON.stringify(data))
719 | if (data.flag === MessageFlag.App && data.type === MessageType.Shake) {
720 | this.appSidePort = data.port2
721 | // logger.debug('appSidePort=>', data.port2)
722 | this.shakeTask.resolve()
723 | } else if (
724 | data.flag === MessageFlag.App &&
725 | data.type === MessageType.Data &&
726 | data.port2 === this.appSidePort
727 | ) {
728 | this.emit('message', data.payload)
729 | this.emit('read', data)
730 | } else if (
731 | data.flag === MessageFlag.App &&
732 | data.type === MessageType.DataWithSystemTool &&
733 | data.port2 === this.appSidePort
734 | ) {
735 | this.emit('message', data.payload)
736 | this.emit('read', data)
737 | } else if (
738 | data.flag === MessageFlag.App &&
739 | data.type === MessageType.Log &&
740 | data.port2 === this.appSidePort
741 | ) {
742 | this.emit('log', data.payload)
743 | } else {
744 | // logger.error('error appSidePort=>%d data=>%j', this.appSidePort, data)
745 | }
746 | }
747 |
748 | onMessage(messagePayload) {
749 | const payload = this.readPayload(messagePayload)
750 | let session = this.sessionMgr.getById(payload.traceId, payload.payloadType)
751 |
752 | if (!session) {
753 | session = this.sessionMgr.newSession(payload.traceId, payload.payloadType, this)
754 |
755 | session.on('data', (fullPayload) => {
756 | if (fullPayload.opCode === MessagePayloadOpCode.Finished) {
757 | if (fullPayload.payloadType === MessagePayloadType.Request) {
758 | this.emit('request', {
759 | request: fullPayload,
760 | response: ({ data }) => {
761 | this.response({ requestId: fullPayload.traceId, data })
762 | },
763 | })
764 | } else if (fullPayload.payloadType === MessagePayloadType.Response) {
765 | this.emit('response', fullPayload)
766 | } else if (fullPayload.payloadType === MessagePayloadType.Notify) {
767 | this.emit('call', fullPayload)
768 | }
769 |
770 | this.emit('data', fullPayload)
771 | this.sessionMgr.destroy(session)
772 | }
773 | })
774 |
775 | session.on('error', (error) => {
776 | this.sessionMgr.destroy(session)
777 | this.emit('error', error)
778 | })
779 | }
780 |
781 | session.addChunk(payload)
782 | }
783 |
784 | request(data, opts) {
785 | const _request = () => {
786 | const defaultOpts = { timeout: 60000 }
787 | const requestId = genTraceId()
788 | const defer = Deferred()
789 | opts = Object.assign(defaultOpts, opts)
790 |
791 | const error = (error) => {
792 | this.off('error', error)
793 | defer.reject(error)
794 | }
795 |
796 | const transact = ({ traceId, payload }) => {
797 | // logger.debug('traceId=>%d payload=>%s', traceId, payload.toString('hex'))
798 | if (traceId === requestId) {
799 | const resultJson = this.buf2Json(payload)
800 | // logger.debug('request id=>%d payload=>%j', requestId, data)
801 | // logger.debug('response id=>%d payload=>%j', requestId, resultJson)
802 |
803 | this.off('response', transact)
804 | this.off('error', error)
805 | defer.resolve(resultJson)
806 | }
807 | }
808 |
809 | this.on('response', transact)
810 | this.on('error', error)
811 | this.sendJson({ requestId, json: data, type: MessagePayloadType.Request })
812 |
813 | let hasReturned = false
814 |
815 | return Promise.race([
816 | timeout(opts.timeout, (resolve, reject) => {
817 | if (hasReturned) {
818 | return resolve()
819 | }
820 |
821 | // logger.error(`request timeout in ${opts.timeout}ms error=> %d data=> %j`, requestId, data)
822 | this.off('response', transact)
823 |
824 | reject(Error(`Timed out in ${opts.timeout}ms.`))
825 | }),
826 | defer.promise.finally(() => {
827 | hasReturned = true
828 | }),
829 | ])
830 | }
831 |
832 | return this.waitingShakePromise.then(_request)
833 | }
834 |
835 | requestCb(data, opts, cb) {
836 | const _requestCb = () => {
837 | const defaultOpts = { timeout: 60000 }
838 |
839 | if (typeof opts === 'function') {
840 | cb = opts
841 | opts = defaultOpts
842 | } else {
843 | opts = Object.assign(defaultOpts, opts)
844 | }
845 |
846 | const requestId = genTraceId()
847 | let timer1 = null
848 | let hasReturned = false
849 |
850 | const transact = ({ traceId, payload }) => {
851 | // logger.debug('traceId=>%d payload=>%s', traceId, payload.toString('hex'))
852 | if (traceId === requestId) {
853 | const resultJson = this.buf2Json(payload)
854 | // logger.debug('request id=>%d payload=>%j', requestId, data)
855 | // logger.debug('response id=>%d payload=>%j', requestId, resultJson)
856 |
857 | this.off('response', transact)
858 | timer1 && clearTimeout(timer1)
859 | timer1 = null
860 | hasReturned = true
861 | cb(null, resultJson)
862 | }
863 | }
864 |
865 | this.on('response', transact)
866 | this.sendJson({ requestId, json: data, type: MessagePayloadType.Request })
867 |
868 | timer1 = setTimeout(() => {
869 | timer1 = null
870 | if (hasReturned) {
871 | return
872 | }
873 |
874 | // logger.error(`request time out in ${opts.timeout}ms error=>%d data=>%j`, requestId, data)
875 | this.off('response', transact)
876 | cb(Error(`Timed out in ${opts.timeout}ms.`))
877 | }, opts.timeout)
878 | }
879 |
880 | return this.waitingShakePromise.then(_requestCb)
881 | }
882 |
883 | response({ requestId, data }) {
884 | this.sendJson({ requestId, json: data, type: MessagePayloadType.Response })
885 | }
886 |
887 | call(data) {
888 | return this.waitingShakePromise.then(() => {
889 | return this.sendJson({ json: data, type: MessagePayloadType.Notify })
890 | })
891 | }
892 |
893 | log(str) {
894 | return this.waitingShakePromise.then(() => {
895 | return this.sendLog(str)
896 | })
897 | }
898 | }
899 |
--------------------------------------------------------------------------------
/LICENCE.md:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------