From eeb638956bab974704404f7f0e7dff2a43e44627 Mon Sep 17 00:00:00 2001 From: Sandakan Nipunajith <45460443+Sandakan@users.noreply.github.com> Date: Sun, 14 May 2023 20:43:38 +0530 Subject: [PATCH] v2.1.0-stable finalization update - Added a new context menu option for folders to show the relevant folder on the window's explorer. - Refactored code in preload script. - Changed the default sorting option in AlbumInfoPage to "Track Number (Ascending)". - Fixed a bug related to the offset tag in synced lyrics. - Fixed a bug where library updates don't reflect on the AllSearchResultsPage. - Updated Musixmatch Settings to show a message about the token updating process. --- .prettierrc | 3 +- README.md | 6 +- .../whats-new-v2.1.0-stable.webp | Bin 0 -> 58758 bytes changelog.md | 56 ++++- package-lock.json | 222 +++++++++--------- package.json | 24 +- release-notes.json | 146 +++++++++++- release/app/package-lock.json | 4 +- release/app/package.json | 2 +- release/package-lock.json | 2 +- release/package.json | 2 +- src/main/main.ts | 17 +- src/main/preload.ts | 193 ++++++++++----- src/renderer/App.tsx | 109 +++++---- .../AlbumInfoPage/AlbumInfoPage.tsx | 10 +- src/renderer/components/AlbumsPage/Album.tsx | 25 +- .../components/AlbumsPage/AlbumsPage.tsx | 2 +- .../ArtistInfoPage/ArtistInfoPage.tsx | 17 +- .../DuplicateArtistsSuggestion.tsx | 4 +- .../SeparateArtistsSuggestion.tsx | 11 +- src/renderer/components/ArtistPage/Artist.tsx | 54 +++-- .../components/ArtistPage/ArtistPage.tsx | 2 +- .../ContextMenu/ContextMenuDataItem.tsx | 4 +- .../CurrentQueuePage/CurrentQueuePage.tsx | 41 ++-- src/renderer/components/ErrorBoundary.tsx | 17 +- src/renderer/components/ErrorPrompt.tsx | 2 +- .../GenreInfoPage/GenreInfoPage.tsx | 4 +- src/renderer/components/GenresPage/Genre.tsx | 10 +- .../components/GenresPage/GenresPage.tsx | 2 +- src/renderer/components/HomePage/HomePage.tsx | 16 +- .../HomePage/ResetAppConfirmationPrompt.tsx | 2 +- src/renderer/components/Hyperlink.tsx | 2 +- src/renderer/components/Img.tsx | 54 +++-- .../components/LyricsPage/LyricsPage.tsx | 23 +- .../components/MiniPlayer/MiniPlayer.tsx | 14 +- .../MusicFolderInfoPage.tsx | 7 +- .../AddMusicFoldersPrompt.tsx | 4 +- .../BlacklistFolderConfirmPrompt.tsx | 40 ++-- .../components/MusicFoldersPage/Folder.tsx | 11 +- .../MusicFoldersPage/MusicFoldersPage.tsx | 2 +- .../RemoveFolderConfirmationPrompt.tsx | 2 +- .../MusicFoldersPage/SelectableFolder.tsx | 2 +- .../components/OpenLinkConfirmPrompt.tsx | 6 +- .../PlaylistsInfoPage/PlaylistsInfoPage.tsx | 8 +- .../PlaylistsPage/ConfirmDeletePlaylists.tsx | 4 +- .../PlaylistsPage/MultipleArtworksCover.tsx | 2 +- .../PlaylistsPage/NewPlaylistPrompt.tsx | 4 +- .../components/PlaylistsPage/Playlist.tsx | 19 +- .../PlaylistsPage/PlaylistsPage.tsx | 2 +- .../SearchPage/AllSearchResultsPage.tsx | 97 +++++++- .../SearchPage/RecentSearchResult.tsx | 3 +- .../AlbumSearchResultsContainer.tsx | 9 +- .../ArtistsSearchResultsContainer.tsx | 9 +- .../GenreSearchResultsContainer.tsx | 9 +- .../MostRelevantSearchResultsContainer.tsx | 12 +- .../PlaylistSearchResultsContainer.tsx | 9 +- .../SongSearchResultsContainer.tsx | 9 +- .../components/SearchPage/SearchPage.tsx | 7 +- .../SearchPage/SearchStartPlaceholder.tsx | 4 +- .../SettingsPage/BlacklistedSong.tsx | 28 ++- .../SettingsPage/MusixmatchSettingsPrompt.tsx | 44 +++- .../SettingsPage/Settings/AboutSettings.tsx | 18 +- .../SettingsPage/Settings/AppStats.tsx | 10 +- .../Settings/AppearanceSettings.tsx | 8 +- .../Settings/AudioPlaybackSettings.tsx | 2 +- .../SettingsPage/Settings/StartupSettings.tsx | 6 +- .../SettingsPage/Settings/StorageSettings.tsx | 2 +- .../ListeningActivityBarGraph.tsx | 14 +- .../components/SongInfoPage/SongInfoPage.tsx | 24 +- .../SongsWithFeaturingArtistSuggestion.tsx | 2 +- .../CustomizeSelectedMetadataPrompt.tsx | 16 +- .../SongTagsEditingPage/SongArtwork.tsx | 2 +- .../SongMetadataResult.tsx | 10 +- .../SongMetadataResultsSelectPrompt.tsx | 2 +- .../SongTagsEditingPage.tsx | 20 +- .../input_containers/SongLyricsEditor.tsx | 4 +- .../CurrentlyPlayingSongInfoContainer.tsx | 28 ++- .../SongsPage/AddSongsToPlaylists.tsx | 6 +- .../SongsPage/BlacklistSongConfirmPrompt.tsx | 2 +- .../DeleteSongsFromSystemConfrimPrompt.tsx | 4 +- src/renderer/components/SongsPage/Song.tsx | 11 +- .../components/SongsPage/SongCard.tsx | 22 +- .../components/SongsPage/SongsPage.tsx | 6 +- src/renderer/components/TitleBar/TitleBar.tsx | 2 +- .../TitleBar/WindowControlsContainer.tsx | 14 +- .../special_controls/ChangeThemeBtn.tsx | 2 +- src/renderer/index.tsx | 2 +- src/renderer/utils/localStorage.ts | 2 +- src/renderer/utils/log.ts | 7 +- 89 files changed, 1128 insertions(+), 575 deletions(-) create mode 100644 assets/other/release artworks/whats-new-v2.1.0-stable.webp diff --git a/.prettierrc b/.prettierrc index 6a71eb18..7a4c1398 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,7 +4,8 @@ { "files": [".prettierrc", ".eslintrc"], "options": { - "parser": "json" + "parser": "json", + "endOfLine": "crlf" } } ] diff --git a/README.md b/README.md index 0c3320cb..c2838b4a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
GitHub all releases - GitHub release (latest by date) + GitHub release (latest by date) GitHub package.json version GitHub license GitHub issues @@ -57,7 +57,7 @@ It packs a horizon of features including,
-![Latest Version Artwork](/assets/other/release%20artworks/whats-new-v2.0.0-stable.webp) +![Latest Version Artwork](/assets/other/release%20artworks/whats-new-v2.1.0-stable.webp) Visit the [release notes](/changelog.md) to see what's new on the latest release.

@@ -141,12 +141,10 @@ This project is built using [Electron React Boilerplate](https://github.com/elec
- ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=Sandakan/Nora&type=Date)](https://star-history.com/#Sandakan/Nora&Date) - ## Feedback If you have any feedback about bugs, feature requests, etc. about the app, please let me know through my [email](mailto:sandakannipunajith@gmail.com). diff --git a/assets/other/release artworks/whats-new-v2.1.0-stable.webp b/assets/other/release artworks/whats-new-v2.1.0-stable.webp new file mode 100644 index 0000000000000000000000000000000000000000..ce6d9f0ed1d0dcebef4b07ec6c1006a7c91b61a3 GIT binary patch literal 58758 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
    3o7bOtcW4t3SEt{fwkwAW6PSg)MLt5 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 z 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 ### The latest version, **( v2.0.0-stable )** contains a lot of new features and improvements. As always expect some bugs in the app. +> ### The latest version, **( v2.1.0-stable )** contains a lot of new features and improvements. As always expect some bugs in the app.
    -![Nora v2.0.0-stable version artwork](assets/other/release%20artworks/whats-new-v2.0.0-stable.webp) +![Nora v2.1.0-stable version artwork](assets/other/release%20artworks/whats-new-v2.1.0-stable.webp) + +
    + +- ### **v2.1.0-stable - ( 13th of May 2023 )** + + - ### 🎉 New Features and Features + + - Added a new design for the song cards on the Home page. Thanks to [**@Shapalapa** for the design inspiration](https://discord.com/channels/727373643935645727/1096107720358248571/1096107720358248571). + - Now songs show their album name next to their artist names. + - Added support for a new suggestion in the SongInfoPage that gets triggered when there are names of featured artists in the title of a song asking to add them to the song artists. + - Added the 'go to album' option to the context menu of songs. + - Added a feature to show the details of the song when right-clicking to get the context menu. + - Linked the new Nora Official Discord server with the app. + - Now, the SearchPage won't limit the no of results you can see to 5 on some components. + - Added experimental support for the offset tag in synced lyrics. + - Added a new hotkey to change the playback speed. Fixes #168. + - Added support for a range of playback speeds instead of a predefined list. + - Added experimental feature as the default sorting option for songs in an album according to their track number. Fixes #169. + - Added a new context menu option for folders to show the relevant folder on the Windows Explorer. + + - ### 🔨 Fixes and Improvements + + - Fixed some bugs related to draggable songs in the queue. Fixes #63. + - Fixed some bugs related to sorting content in the app. Fixes https://github.com/Sandakan/Nora/issues/156. + - Fixed a bug where clicking `Play next` would add the song next to the next song. + - Updated the context menu options by right-clicking the current song info container in the footer. Fixes #160 and #158. + - Fixed a bug where deleting the current playing song wouldn't remove it from the current queue. + - Fixed some bugs related to lyrics not being read from the audio source. + - Fixed a bug where app UI goes out of bounds. Fixes #157. + - Fixed a possible bug where media control buttons don't work as expected. Fixes [#166](https://github.com/Sandakan/Nora/issues/166). + - Removed predictive search when searching for artists, albums, and genres in the SongTagsEditingPage. + - Updated components to show information about the content when right-clicking a component. + - Fixed some image scaling issues in ArtistInfoPage. + - Fixed a bug where adding song metadata from the internet with new album data doesn't count the song artwork to the album artwork. + - Improved the app's responsiveness to various screen sizes. Fixes #128. + - Updated the file association icons to show the relevant file type. + - Fixed a bug related to synced lyrics saved in audio files. + - Fixed a bug where sometimes users can't see the artist name when in ArtistInfoPage due to contrast issues between light and dark modes. + - Improved the artist detection algorithm of the SeparateArtistsSuggestion. + - Improved app performance by loading only necessary components to display. + - Fixed a bug where the context menu overflows out of the visible part of the app's window. + - Fixed some bugs related to how SongCards display in the HomePage when different screen sizes. + - Added a new line with "•••" as the first line of synced lyrics. + - Fixed a bug where metrics in ListeningActivityBarGraph overflow out of its container. + - Fixed a bug where the `Download Synced Lyrics` button in the metadata editing page keeps spinning even though fetching lyrics failed. + - Improved the app version detection algorithm of the app. + - Updated Musixmatch Settings to show a message about the token updating process. + - Fixed a bug where library updates don't reflect on the AllSearchResultsPage. + + - ### 🐜 Known Issues and Bugs + - Sometimes updating song artwork may need an app restart to show on the app #162. + - The app may crash in mini-player mode when trying to use window snap feature #163.
    diff --git a/package-lock.json b/package-lock.json index 4d4f98b3..00514948 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nora", - "version": "2.1.0-stable.20230503", + "version": "2.1.0-stable", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nora", - "version": "2.1.0-stable.20230503", + "version": "2.1.0-stable", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -30,16 +30,16 @@ "@types/dotenv-webpack": "^7.0.3", "@types/jest": "^29.5.1", "@types/node": "18.16.3", - "@types/react": "^18.2.5", + "@types/react": "^18.2.6", "@types/react-beautiful-dnd": "^13.1.4", - "@types/react-dom": "^18.2.3", + "@types/react-dom": "^18.2.4", "@types/react-test-renderer": "^18.0.0", "@types/react-window": "^1.8.5", "@types/sharp": "^0.32.0", "@types/terser-webpack-plugin": "^5.2.0", "@types/webpack-bundle-analyzer": "^4.6.0", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", + "@typescript-eslint/eslint-plugin": "^5.59.5", + "@typescript-eslint/parser": "^5.59.5", "autoprefixer": "^10.4.14", "browserslist-config-erb": "^0.0.3", "chalk": "^4.1.2", @@ -50,12 +50,12 @@ "detect-port": "^1.5.1", "dotenv": "^16.0.3", "dotenv-webpack": "^8.0.1", - "electron": "^24.2.0", + "electron": "^24.3.0", "electron-builder": "^23.6.0", "electron-debug": "^3.2.0", "electron-devtools-installer": "^3.2.0", "electronmon": "^2.0.2", - "eslint": "^8.39.0", + "eslint": "^8.40.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-erb": "^4.0.6", "eslint-import-resolver-typescript": "^3.5.5", @@ -68,7 +68,7 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "file-loader": "^6.2.0", - "file-type": "^18.3.0", + "file-type": "^18.4.0", "html-webpack-plugin": "^5.5.1", "husky": "^8.0.3", "identity-obj-proxy": "^3.0.0", @@ -83,16 +83,16 @@ "rimraf": "^3.0.2", "style-loader": "^3.3.2", "tailwindcss": "^3.3.2", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.8", "ts-jest": "^29.1.0", "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "typescript": "^5.0.4", "url-loader": "^4.1.1", - "webpack": "^5.82.0", + "webpack": "^5.82.1", "webpack-bundle-analyzer": "^4.8.0", - "webpack-cli": "^5.0.2", - "webpack-dev-server": "^4.13.3", + "webpack-cli": "^5.1.1", + "webpack-dev-server": "^4.15.0", "webpack-merge": "^5.8.0" } }, @@ -962,14 +962,14 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.5.2", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -985,9 +985,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", + "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2354,9 +2354,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.5.tgz", - "integrity": "sha512-RuoMedzJ5AOh23Dvws13LU9jpZHIc/k90AgmK7CecAYeWmSr3553L4u5rk4sWAPBuQosfT7HmTfG4Rg5o4nGEA==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.6.tgz", + "integrity": "sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2373,9 +2373,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.3.tgz", - "integrity": "sha512-hxXEXWxFJXbY0LMj/T69mznqOZJXNtQMqVxIiirVAZnnpeYiD4zt+lPsgcr/cfWg2VLsxZ1y26vigG03prYB+Q==", + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz", + "integrity": "sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==", "dev": true, "dependencies": { "@types/react": "*" @@ -2549,15 +2549,15 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz", - "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz", + "integrity": "sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/type-utils": "5.59.2", - "@typescript-eslint/utils": "5.59.2", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/type-utils": "5.59.5", + "@typescript-eslint/utils": "5.59.5", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -2583,14 +2583,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", - "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.5.tgz", + "integrity": "sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/typescript-estree": "5.59.5", "debug": "^4.3.4" }, "engines": { @@ -2610,13 +2610,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", - "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz", + "integrity": "sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2" + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/visitor-keys": "5.59.5" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2627,13 +2627,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", - "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz", + "integrity": "sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.2", - "@typescript-eslint/utils": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.5", + "@typescript-eslint/utils": "5.59.5", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -2654,9 +2654,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", - "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz", + "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2667,13 +2667,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", - "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz", + "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/visitor-keys": "5.59.5", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2694,17 +2694,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", - "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.5.tgz", + "integrity": "sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/typescript-estree": "5.59.5", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -2720,12 +2720,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", - "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz", + "integrity": "sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/types": "5.59.5", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2980,9 +2980,9 @@ } }, "node_modules/@webpack-cli/configtest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.1.tgz", - "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.0.tgz", + "integrity": "sha512-K/vuv72vpfSEZoo5KIU0a2FsEoYdW0DUMtMpB5X3LlUwshetMZRZRxB7sCsVji/lFaSxtQQ3aM9O4eMolXkU9w==", "dev": true, "engines": { "node": ">=14.15.0" @@ -3006,9 +3006,9 @@ } }, "node_modules/@webpack-cli/serve": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.2.tgz", - "integrity": "sha512-S9h3GmOmzUseyeFW3tYNnWS7gNUuwxZ3mmMq0JyW78Vx1SGKPSkt5bT4pB0rUnVfHjP0EL9gW2bOzmtiTfQt0A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.4.tgz", + "integrity": "sha512-0xRgjgDLdz6G7+vvDLlaRpFatJaJ69uTalZLRSMX5B3VUrDmXcrVA3+6fXXQgmYz7bY9AAgs348XQdmtLsK41A==", "dev": true, "engines": { "node": ">=14.15.0" @@ -5907,9 +5907,9 @@ } }, "node_modules/electron": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-24.2.0.tgz", - "integrity": "sha512-fEYAftYqFhveniWJbEHXjNMWjooFFIuqNj/eEFJkGzycInfBJq/c4E/dew++s6s0YLubxFnjoF2qZiqapLj0gA==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-24.3.0.tgz", + "integrity": "sha512-M7PpfpOzGdLeZPr2xhxXuvJeoXPEHMH40Rtv8BCGleRPolwna9BepAGc0H0F+Uz5kGKOv3xcm99fTurvXUH0nw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -6197,9 +6197,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", - "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz", + "integrity": "sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -6497,15 +6497,15 @@ } }, "node_modules/eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", + "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.40.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -6516,8 +6516,8 @@ "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -7119,9 +7119,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -7147,14 +7147,14 @@ } }, "node_modules/espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", "dev": true, "dependencies": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -7549,9 +7549,9 @@ } }, "node_modules/file-type": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.3.0.tgz", - "integrity": "sha512-pkPZ5OGIq0TYb37b8bHDLNeQSe1H2KlaQ2ySGpJkkr2KZdaWsO4QhPzHA0mQcsUW2cSqJk+4gM/UyLz/UFbXdQ==", + "version": "18.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.4.0.tgz", + "integrity": "sha512-o6MQrZKTAK6WpvmQk3jqTVUmqxYBxW5bloUfrdH1ZnRFDvvAPNr+l+rgOxM3nkqWT+3khaj3FRMDydWe0xhu+w==", "dev": true, "dependencies": { "readable-web-to-node-stream": "^3.0.2", @@ -14863,16 +14863,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", - "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz", + "integrity": "sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.5" + "terser": "^5.16.8" }, "engines": { "node": ">= 10.13.0" @@ -15778,9 +15778,9 @@ } }, "node_modules/webpack": { - "version": "5.82.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.0.tgz", - "integrity": "sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg==", + "version": "5.82.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.1.tgz", + "integrity": "sha512-C6uiGQJ+Gt4RyHXXYt+v9f+SN1v83x68URwgxNQ98cvH8kxiuywWGP4XeNZ1paOzZ63aY3cTciCEQJNFUljlLw==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -15792,7 +15792,7 @@ "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.13.0", + "enhanced-resolve": "^5.14.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -15879,15 +15879,15 @@ } }, "node_modules/webpack-cli": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.2.tgz", - "integrity": "sha512-4y3W5Dawri5+8dXm3+diW6Mn1Ya+Dei6eEVAdIduAmYNLzv1koKVAqsfgrrc9P2mhrYHQphx5htnGkcNwtubyQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.1.tgz", + "integrity": "sha512-OLJwVMoXnXYH2ncNGU8gxVpUtm3ybvdioiTvHgUyBuyMLKiVvWy+QObzBsMtp5pH7qQoEuWgeEUQ/sU3ZJFzAw==", "dev": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.0.1", + "@webpack-cli/configtest": "^2.1.0", "@webpack-cli/info": "^2.0.1", - "@webpack-cli/serve": "^2.0.2", + "@webpack-cli/serve": "^2.0.4", "colorette": "^2.0.14", "commander": "^10.0.1", "cross-spawn": "^7.0.3", @@ -16018,9 +16018,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.13.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.3.tgz", - "integrity": "sha512-KqqzrzMRSRy5ePz10VhjyL27K2dxqwXQLP5rAKwRJBPUahe7Z2bBWzHw37jeb8GCPKxZRO79ZdQUAPesMh/Nug==", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.0.tgz", + "integrity": "sha512-HmNB5QeSl1KpulTBQ8UT4FPrByYyaLxpJoQ0+s7EvUrMc16m0ZS1sgb1XGqzmgCPk0c9y+aaXxn11tbLzuM7NQ==", "dev": true, "dependencies": { "@types/bonjour": "^3.5.9", diff --git a/package.json b/package.json index ca7f791b..6793b713 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nora", "productName": "Nora", "description": "An elegant music player built using Electron and React. Inspired by Oto Music for Android by Piyush Mamidwar.", - "version": "2.1.0-stable.20230512", + "version": "2.1.0-stable", "license": "MIT", "appPreferences": { "removeReactStrictMode": false, @@ -136,16 +136,16 @@ "@types/dotenv-webpack": "^7.0.3", "@types/jest": "^29.5.1", "@types/node": "18.16.3", - "@types/react": "^18.2.5", + "@types/react": "^18.2.6", "@types/react-beautiful-dnd": "^13.1.4", - "@types/react-dom": "^18.2.3", + "@types/react-dom": "^18.2.4", "@types/react-test-renderer": "^18.0.0", "@types/react-window": "^1.8.5", "@types/sharp": "^0.32.0", "@types/terser-webpack-plugin": "^5.2.0", "@types/webpack-bundle-analyzer": "^4.6.0", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", + "@typescript-eslint/eslint-plugin": "^5.59.5", + "@typescript-eslint/parser": "^5.59.5", "autoprefixer": "^10.4.14", "browserslist-config-erb": "^0.0.3", "chalk": "^4.1.2", @@ -156,12 +156,12 @@ "detect-port": "^1.5.1", "dotenv": "^16.0.3", "dotenv-webpack": "^8.0.1", - "electron": "^24.2.0", + "electron": "^24.3.0", "electron-builder": "^23.6.0", "electron-debug": "^3.2.0", "electron-devtools-installer": "^3.2.0", "electronmon": "^2.0.2", - "eslint": "^8.39.0", + "eslint": "^8.40.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-erb": "^4.0.6", "eslint-import-resolver-typescript": "^3.5.5", @@ -174,7 +174,7 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "file-loader": "^6.2.0", - "file-type": "^18.3.0", + "file-type": "^18.4.0", "html-webpack-plugin": "^5.5.1", "husky": "^8.0.3", "identity-obj-proxy": "^3.0.0", @@ -189,16 +189,16 @@ "rimraf": "^3.0.2", "style-loader": "^3.3.2", "tailwindcss": "^3.3.2", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.8", "ts-jest": "^29.1.0", "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "typescript": "^5.0.4", "url-loader": "^4.1.1", - "webpack": "^5.82.0", + "webpack": "^5.82.1", "webpack-bundle-analyzer": "^4.8.0", - "webpack-cli": "^5.0.2", - "webpack-dev-server": "^4.13.3", + "webpack-cli": "^5.1.1", + "webpack-dev-server": "^4.15.0", "webpack-merge": "^5.8.0" }, "overrides": { diff --git a/release-notes.json b/release-notes.json index 6134e413..56df8596 100644 --- a/release-notes.json +++ b/release-notes.json @@ -1,14 +1,152 @@ { "latestVersion": { - "version": "2.0.0-stable", + "version": "2.1.0-stable", "phase": "stable", - "releaseDate": "2023 April 23", - "artwork": "/assets/other/release artworks/whats-new-v2.0.0-stable.webp", + "releaseDate": "2023 May 14", + "artwork": "/assets/other/release artworks/whats-new-v2.1.0-stable.webp", "importantNotes": [ - "Installing this update would RESET the app to provide support for new features." + "This update includes fixes for app responsiveness no different screen sizes.", + "Welcome to a new design for Song Cards in Home page." ] }, "versions": [ + { + "version": "2.1.0-stable", + "artwork": "/assets/other/release artworks/whats-new-v2.1.0-stable.webp", + "releaseDate": "2023 May 14", + "importantNotes": [ + "This update includes fixes for app responsiveness no different screen sizes.", + "Welcome to a new design for Song Cards in Home page." + ], + "notes": { + "new": [ + { + "note": "Added a new design for the song cards on the Home page. Thanks to @Shapalapa for the design inspiration." + }, + { + "note": "Now songs show their album name next to their artist names." + }, + { + "note": "Added support for a new suggestion in the SongInfoPage that gets triggered when there are names of featured artists in the title of a song asking to add them to the song artists." + }, + { + "note": "Added the 'go to album' option to the context menu of songs." + }, + { + "note": "Added a feature to show the details of the song when right-clicking to get the context menu." + }, + { + "note": "Linked the new Nora Official Discord server with the app." + }, + { + "note": "Now, the SearchPage won't limit the no of results you can see to 5 on some components." + }, + { + "note": "Added experimental support for the offset tag in synced lyrics." + }, + { + "note": "Added a new hotkey to change the playback speed." + }, + { + "note": "Added support for a range of playback speeds instead of a predefined list." + }, + { + "note": "Added experimental feature as the default sorting option for songs in an album according to their track number." + }, + { + "note": "Added a new context menu option for folders to show the relevant folder on the window's explorer." + } + ], + "fixed": [ + { + "note": "Fixed some bugs related to draggable songs in the queue." + }, + { + "note": "Fixed some bugs related to sorting content in the app." + }, + { + "note": "Fixed a bug where clicking \"Play Next\" would add the song next to the next song." + }, + { + "note": "Updated the context menu options by right-clicking the current song info container in the footer." + }, + { + "note": "Fixed a bug where deleting the current playing song wouldn't remove it from the current queue." + }, + { + "note": "Fixed some bugs related to lyrics not being read from the audio source." + }, + { + "note": "Fixed a bug where app UI goes out of bounds." + }, + { + "note": "Fixed a possible bug where media control buttons don't work as expected." + }, + { + "note": "Removed predictive search when searching for artists, albums, and genres in the SongTagsEditingPage." + }, + { + "note": "Updated components to show information about the content when right-clicking a component." + }, + { + "note": "Fixed some image scaling issues in ArtistInfoPage." + }, + { + "note": "Fixed a bug where adding song metadata from the internet with new album data doesn't count the song artwork to the album artwork." + }, + { + "note": "Improved the app's responsiveness to various screen sizes." + }, + { + "note": "Updated the file association icons to show the relevant file type." + }, + { + "note": "Fixed a bug related to synced lyrics saved in audio files." + }, + { + "note": "Fixed a bug where sometimes users can't see the artist name when in ArtistInfoPage due to contrast issues between light and dark modes." + }, + { + "note": "Improved the artist detection algorithm of the SeparateArtistsSuggestion." + }, + { + "note": "Improved app performance by loading only necessary components to display." + }, + { + "note": "Fixed a bug where the context menu overflows out of the visible part of the app's window." + }, + { + "note": "Fixed some bugs related to how SongCards display in the HomePage when different screen sizes." + }, + { + "note": "Added a new line with \"•••\" as the first line of synced lyrics." + }, + { + "note": "Fixed a bug where metrics in ListeningActivityBarGraph overflow out of its container." + }, + { + "note": "Fixed a bug where the \"Download Synced Lyrics\" button in the metadata editing page keeps spinning even though fetching lyrics failed." + }, + { + "note": "Improved the app version detection algorithm of the app." + }, + { + "note": "Fixed a bug where library updates don't reflect on the AllSearchResultsPage." + }, + { + "note": "Updated Musixmatch Settings to show a message about the token updating process." + } + ], + "knownIssues": [ + { + "note": "Sometimes updating song artwork may need an app restart to show on the app." + }, + { + "note": "The app may crash in mini-player mode when trying to use window snap feature." + } + ] + } + }, { "version": "2.0.0-stable", "artwork": "/assets/other/release artworks/whats-new-v2.0.0-stable.webp", diff --git a/release/app/package-lock.json b/release/app/package-lock.json index 26e84ec3..25fe7068 100644 --- a/release/app/package-lock.json +++ b/release/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "nora", - "version": "2.1.0-stable.20230512", + "version": "2.1.0-stable", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nora", - "version": "2.1.0-stable.20230512", + "version": "2.1.0-stable", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/release/app/package.json b/release/app/package.json index 47f90bf3..0bcbfe81 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,7 +1,7 @@ { "name": "nora", "productName": "Nora", - "version": "2.1.0-stable.20230512", + "version": "2.1.0-stable", "description": "An elegant music player built using Electron and React. Inspired by Oto Music for Android by Piyush Mamidwar.", "main": "./dist/main/main.js", "author": { diff --git a/release/package-lock.json b/release/package-lock.json index 2ee371e8..286a57d4 100644 --- a/release/package-lock.json +++ b/release/package-lock.json @@ -1,6 +1,6 @@ { "name": "nora", - "version": "2.1.0-stable.20230512", + "version": "2.1.0-stable", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/release/package.json b/release/package.json index 415b2c7f..984995ea 100644 --- a/release/package.json +++ b/release/package.json @@ -1,7 +1,7 @@ { "name": "nora", "productName": "Nora", - "version": "2.1.0-stable.20230512", + "version": "2.1.0-stable", "description": "An elegant music player built using Electron and React. Inspired by Oto Music for Android by Piyush Mamidwar.", "main": "./dist/main/main.js", "author": { diff --git a/src/main/main.ts b/src/main/main.ts index ce72e55f..d7d7f018 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -344,8 +344,8 @@ app console.log('Left full screen'); mainWindow.webContents.send('app/leftFullscreen'); }); - powerMonitor.addListener('on-ac', toggleonBatteryPower); - powerMonitor.addListener('on-battery', toggleonBatteryPower); + powerMonitor.addListener('on-ac', toggleOnBatteryPower); + powerMonitor.addListener('on-battery', toggleOnBatteryPower); ipcMain.on('app/getSongPosition', (_, position: number) => saveUserData('currentSong.stoppedPosition', position) @@ -457,13 +457,6 @@ app ipcMain.handle('app/generatePalettes', generatePalettes); - ipcMain.on( - 'app/savePageSortState', - (_, pageType: PageSortTypes, state: unknown) => { - saveUserData(pageType, state); - } - ); - ipcMain.handle('app/getArtistArtworks', (_, artistId: string) => getArtistInfoFromNet(artistId) ); @@ -643,6 +636,10 @@ app revealSongInFileExplorer(songId) ); + ipcMain.on('revealFolderInFileExplorer', (_, folderPath: string) => + shell.showItemInFolder(folderPath) + ); + ipcMain.on('app/openInBrowser', (_, url: string) => shell.openExternal(url) ); @@ -788,7 +785,7 @@ function handleBeforeQuit() { ); } -function toggleonBatteryPower() { +function toggleOnBatteryPower() { isOnBatteryPower = powerMonitor.isOnBatteryPower(); mainWindow.webContents.send('app/isOnBatteryPower', isOnBatteryPower); } diff --git a/src/main/preload.ts b/src/main/preload.ts index 96d1296e..242c32c7 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -1,30 +1,36 @@ import { contextBridge, ipcRenderer } from 'electron'; import { LastFMTrackInfoApi } from '../@types/last_fm_api'; -export const api = { - // $ APP PROPERTIES +const properties = { isInDevelopment: process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true', commandLineArgs: process.argv, +}; - // $ APP WINDOW CONTROLS +const windowControls = { minimizeApp: (): void => ipcRenderer.send('app/minimize'), toggleMaximizeApp: (): void => ipcRenderer.send('app/toggleMaximize'), closeApp: (): void => ipcRenderer.send('app/close'), hideApp: (): void => ipcRenderer.send('app/hide'), showApp: (): void => ipcRenderer.send('app/show'), + onWindowFocus: (callback: (e: any) => void) => + ipcRenderer.on('app/focused', callback), + onWindowBlur: (callback: (e: any) => void) => + ipcRenderer.on('app/blurred', callback), +}; - // $ APP THEME +const theme = { listenForSystemThemeChanges: ( callback: (e: any, isDarkMode: boolean, usingSystemTheme: boolean) => void ) => ipcRenderer.on('app/systemThemeChange', callback), - changeAppTheme: (theme?: AppTheme): void => - ipcRenderer.send('app/changeAppTheme', theme), + changeAppTheme: (appTheme?: AppTheme): void => + ipcRenderer.send('app/changeAppTheme', appTheme), stoplisteningForSystemThemeChanges: ( callback: (e: any, isDarkMode: boolean, usingSystemTheme: boolean) => void ) => ipcRenderer.removeListener('app/systemThemeChange', callback), +}; - // $ APP PLAYER CONTROLS +const playerControls = { songPlaybackStateChange: (isPlaying: boolean): void => ipcRenderer.send('app/player/songPlaybackStateChange', isPlaying), toggleSongPlayback: (callback: (e: any) => void) => @@ -47,8 +53,9 @@ export const api = { ipcRenderer.removeListener('app/player/skipBackward', callback), removeSkipForwardToNextSongEvent: (callback: (e: any) => void) => ipcRenderer.removeListener('app/player/skipForward', callback), +}; - // $ AUDIO LIBRARY CONTROLS +const audioLibraryControls = { checkForStartUpSongs: (): Promise => ipcRenderer.invoke('app/checkForStartUpSongs'), addSongsFromFolderStructures: ( @@ -101,8 +108,6 @@ export const api = { resyncSongsLibrary: (): Promise => ipcRenderer.invoke('app/resyncSongsLibrary'), - removeAMusicFolder: (absolutePath: string): Promise => - ipcRenderer.invoke('app/removeAMusicFolder', absolutePath), getBlacklistData: (): Promise => ipcRenderer.invoke('app/getBlacklistData'), blacklistSongs: (songIds: string[]): Promise => @@ -120,8 +125,11 @@ export const api = { ), generatePalettes: (): Promise => ipcRenderer.invoke('app/generatePalettes'), + clearSongHistory: (): PromiseFunctionReturn => + ipcRenderer.invoke('app/clearSongHistory'), +}; - // $ SUGGESTIONS RELATED APIS +const suggestions = { getArtistDuplicates: (artistName: string): Promise => ipcRenderer.invoke('app/getArtistDuplicates', artistName), @@ -156,41 +164,45 @@ export const api = { featArtistNames, removeFeatInfoInTitle ), +}; - // $ APP PLAYER UNKNOWN SONGS FETCHING APIS +// $ APP PLAYER UNKNOWN SONGS FETCHING APIS +const unknownSource = { playSongFromUnknownSource: ( callback: (_: unknown, audioPlayerData: AudioPlayerData) => void ) => ipcRenderer.on('app/playSongFromUnknownSource', callback), getSongFromUnknownSource: (songPath: string): Promise => ipcRenderer.invoke('app/getSongFromUnknownSource', songPath), +}; - // $ QUIT EVENT HANDLING +// $ QUIT EVENT HANDLING +const quitEvent = { beforeQuitEvent: (callback: (e: any) => void) => ipcRenderer.on('app/beforeQuitEvent', callback), removeBeforeQuitEventListener: (callback: (...args: any[]) => void) => ipcRenderer.removeListener('app/beforeQuitEvent', callback), +}; - // $ APP WINDOW BLUR AND FOCUS EVENTS - onWindowFocus: (callback: (e: any) => void) => - ipcRenderer.on('app/focused', callback), - onWindowBlur: (callback: (e: any) => void) => - ipcRenderer.on('app/blurred', callback), - - // $ APP FULL-SCREEN EVENTS +// $ SYSTEM BATTERY RELATED EVENTS +const battery = { listenForBatteryPowerStateChanges: ( callback: (_: any, isOnBatteryPower: boolean) => void ) => ipcRenderer.on('app/isOnBatteryPower', callback), stopListeningForBatteryPowerStateChanges: ( callback: (_: any, isOnBatteryPower: boolean) => void ) => ipcRenderer.removeListener('app/isOnBatteryPower', callback), +}; - // $ APP FULL-SCREEN EVENTS +// $ APP FULL-SCREEN EVENTS +const fullscreen = { onEnterFullscreen: (callback: (e: any) => void) => ipcRenderer.on('app/enteredFullscreen', callback), onLeaveFullscreen: (callback: (e: any) => void) => ipcRenderer.on('app/leftFullscreen', callback), +}; - // $ APP SEARCH +// $ APP SEARCH +const search = { search: ( filter: SearchFilters, value: string, @@ -206,8 +218,10 @@ export const api = { ), clearSearchHistory: (searchText?: string[]): Promise => ipcRenderer.invoke('app/clearSearchHistory', searchText), +}; - // $ SONG LYRICS +// $ SONG LYRICS +const lyrics = { getSongLyrics: ( songInfo: LyricsRequestTrackInfo, lyricsType?: LyricsTypes, @@ -220,10 +234,12 @@ export const api = { lyricsRequestType ), - saveLyricsToSong: (songPath: string, lyrics: SongLyrics) => - ipcRenderer.invoke('app/saveLyricsToSong', songPath, lyrics), + saveLyricsToSong: (songPath: string, text: SongLyrics) => + ipcRenderer.invoke('app/saveLyricsToSong', songPath, text), +}; - // $ APP MESSAGES +// $ APP MESSAGES +const messages = { getMessageFromMain: ( callback: ( event: unknown, @@ -234,21 +250,19 @@ export const api = { ) => ipcRenderer.on('app/sendMessageToRendererEvent', callback), removeMessageToRendererEventListener: (callback: (...args: any[]) => void) => ipcRenderer.removeListener('app/sendMessageToRendererEvent', callback), +}; - // $ APP DATA UPDATE EVENTS +// $ APP DATA UPDATE EVENTS +const dataUpdates = { dataUpdateEvent: ( callback: (e: unknown, dataEvents: DataUpdateEvent[]) => void ) => ipcRenderer.on('app/dataUpdateEvent', callback), removeDataUpdateEventListeners: () => ipcRenderer.removeAllListeners('app/dataUpdateEvent'), +}; - // $ APP GLOBAL EVENT LISTENER CONTROLS - removeIpcEventListener: ( - channel: IpcChannels, - callback: (...args: any[]) => void - ) => ipcRenderer.removeListener(channel, callback), - - // $ UPDATE SONG DATA +// $ UPDATE SONG DATA +const songUpdates = { updateSongId3Tags: ( songIdOrPath: string, tags: SongTags, @@ -271,8 +285,10 @@ export const api = { ipcRenderer.invoke('app/getImgFileLocation'), revealSongInFileExplorer: (songId: string): void => ipcRenderer.send('revealSongInFileExplorer', songId), +}; - // $ FETCH SONG DATA FROM INTERNET +// $ FETCH SONG DATA FROM INTERNET +const songDataFromInternet = { searchSongMetadataResultsInInternet: ( songTitle: string, songArtists: string[] @@ -296,17 +312,23 @@ export const api = { songArtists: string[] ): Promise => ipcRenderer.invoke('app/fetchSongInfoFromNet', songTitle, songArtists), +}; - // $ APP USER DATA +// $ APP USER DATA +const userData = { getUserData: (): Promise => ipcRenderer.invoke('app/getUserData'), saveUserData: (dataType: UserDataTypes, data: unknown) => ipcRenderer.invoke('app/saveUserData', dataType, data), +}; - // $ STORAGE DATA +// $ STORAGE DATA +const storageData = { getStorageUsage: (forceRefresh?: boolean): Promise => ipcRenderer.invoke('app/getStorageUsage', forceRefresh), +}; - // $ FOLDER DATA +// $ FOLDER DATA +const folderData = { getFolderData: ( folderPaths: string[], sortType?: FolderSortTypes @@ -325,8 +347,16 @@ export const api = { folderPaths, isBlacklistFolder ), + revealFolderInFileExplorer: (folderPath: string): void => + ipcRenderer.send('revealFolderInFileExplorer', folderPath), + getFolderStructures: (): Promise => + ipcRenderer.invoke('app/getFolderStructures'), + removeAMusicFolder: (absolutePath: string): Promise => + ipcRenderer.invoke('app/removeAMusicFolder', absolutePath), +}; - // $ ARTISTS DATA +// $ ARTISTS DATA +const artistsData = { getArtistData: ( artistIdsOrNames?: string[], sortType?: ArtistSortTypes, @@ -342,22 +372,28 @@ export const api = { artistId: string ): Promise => ipcRenderer.invoke('app/getArtistArtworks', artistId), +}; - // $ GENRES DATA +// $ GENRES DATA +const genresData = { getGenresData: ( genreNamesOrIds?: string[], sortType?: GenreSortTypes ): Promise => ipcRenderer.invoke('app/getGenresData', genreNamesOrIds, sortType), +}; - // $ ALBUMS DATA +// $ ALBUMS DATA +const albumsData = { getAlbumData: ( albumTitlesOrIds?: string[], sortType?: AlbumSortTypes ): Promise => ipcRenderer.invoke('app/getAlbumData', albumTitlesOrIds, sortType), +}; - // $ PLAYLIST DATA AND CONTROLS +// $ PLAYLIST DATA AND CONTROLS +const playlistsData = { getPlaylistData: ( playlistIds?: string[], sortType?: PlaylistSortTypes, @@ -390,39 +426,38 @@ export const api = { artworkPath: string ): Promise => ipcRenderer.invoke('app/addArtworkToAPlaylist', playlistId, artworkPath), - - // $ APP PLAYLISTS DATA UPDATE removeSongFromPlaylist: ( playlistId: string, songId: string ): PromiseFunctionReturn => ipcRenderer.invoke('app/removeSongFromPlaylist', playlistId, songId), - clearSongHistory: (): PromiseFunctionReturn => - ipcRenderer.invoke('app/clearSongHistory'), removePlaylists: (playlistIds: string[]) => ipcRenderer.invoke('app/removePlaylists', playlistIds), + getArtworksForMultipleArtworksCover: (songIds: string[]): Promise => + ipcRenderer.invoke('app/getArtworksForMultipleArtworksCover', songIds), +}; - // $ APP PAGES STATE - savePageSortingState: (pageType: PageSortTypes, state: unknown): void => - ipcRenderer.send('app/savePageSortState', pageType, state), - - // $ APP LOGS +// $ APP LOGS +const log = { sendLogs: ( - log: string, + logStr: string, logToConsoleType: 'log' | 'warn' | 'error' = 'log', forceWindowRestart = false, forceMainRestart = false ): Promise => { return ipcRenderer.invoke( 'app/getRendererLogs', - log, + logStr, logToConsoleType, forceWindowRestart, forceMainRestart ); }, + openLogFile: (): void => ipcRenderer.send('app/openLogFile'), +}; - // $ APP MINI PLAYER CONTROLS +// $ APP MINI PLAYER CONTROLS +const miniPlayer = { toggleMiniPlayer: (isMiniPlayerActive: boolean): Promise => ipcRenderer.invoke('app/toggleMiniPlayer', isMiniPlayerActive), toggleMiniPlayerAlwaysOnTop: ( @@ -432,18 +467,21 @@ export const api = { 'app/toggleMiniPlayerAlwaysOnTop', isMiniPlayerAlwaysOnTop ), +}; - // $ APP SETTINGS HELPER FUNCTIONS +// $ APP SETTINGS HELPER FUNCTIONS +const settingsHelpers = { openInBrowser: (url: string): void => ipcRenderer.send('app/openInBrowser', url), toggleAutoLaunch: (autoLaunchState: boolean): Promise => ipcRenderer.invoke('app/toggleAutoLaunch', autoLaunchState), - openLogFile: (): void => ipcRenderer.send('app/openLogFile'), openDevtools: () => ipcRenderer.send('app/openDevTools'), networkStatusChange: (isConnected: boolean): void => ipcRenderer.send('app/networkStatusChange', isConnected), +}; - // $ APP RESTART OR RESET +// $ APP RESTART OR RESET +const appControls = { restartRenderer: (reason: string): void => ipcRenderer.send('app/restartRenderer', reason), restartApp: (reason: string): void => @@ -452,17 +490,13 @@ export const api = { ipcRenderer.removeAllListeners('app/beforeQuitEvent'); ipcRenderer.send('app/resetApp'); }, +}; - // $ PATH FOR RENDERER +// $ OTHER +const utils = { path: { join: (...args: string[]) => args.join('/'), }, - - // $ OTHER - getArtworksForMultipleArtworksCover: (songIds: string[]): Promise => - ipcRenderer.invoke('app/getArtworksForMultipleArtworksCover', songIds), - getFolderStructures: (): Promise => - ipcRenderer.invoke('app/getFolderStructures'), getExtension: (dir: string) => { const ext = dir.split('.').at(-1) || ''; return ext; @@ -483,4 +517,35 @@ export const api = { }, }; +export const api = { + properties, + windowControls, + theme, + playerControls, + audioLibraryControls, + suggestions, + unknownSource, + quitEvent, + battery, + fullscreen, + search, + lyrics, + messages, + dataUpdates, + songUpdates, + songDataFromInternet, + userData, + storageData, + folderData, + artistsData, + genresData, + albumsData, + playlistsData, + log, + miniPlayer, + settingsHelpers, + appControls, + utils, +}; + contextBridge.exposeInMainWorld('api', api); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index deb1faca..55e4a214 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -253,7 +253,7 @@ const reducer = ( }, }; case 'UPDATE_MINI_PLAYER_STATE': - window.api.toggleMiniPlayer( + window.api.miniPlayer.toggleMiniPlayer( typeof action.data === 'boolean' ? action.data : state.player.isMiniPlayer @@ -472,7 +472,7 @@ player.addEventListener('player/trackchange', (e) => { // / / / / / / / / const updateNetworkStatus = () => - window.api.networkStatusChange(navigator.onLine); + window.api.settingsHelpers.networkStatusChange(navigator.onLine); updateNetworkStatus(); window.addEventListener('online', updateNetworkStatus); @@ -538,7 +538,7 @@ const reducerData: AppReducer = { isOnBatteryPower: false, }; -console.log('Command line args', window.api.commandLineArgs); +console.log('Command line args', window.api.properties.commandLineArgs); export default function App() { const [content, dispatch] = React.useReducer(reducer, reducerData); @@ -781,11 +781,15 @@ export default function App() { dispatch({ type: 'UPDATE_BATTERY_POWER_STATE', data: isOnBatteryPower }); }; - window.api.listenForSystemThemeChanges(watchForSystemThemeChanges); - window.api.listenForBatteryPowerStateChanges(watchPowerChanges); + window.api.theme.listenForSystemThemeChanges(watchForSystemThemeChanges); + window.api.battery.listenForBatteryPowerStateChanges(watchPowerChanges); return () => { - window.api.stoplisteningForSystemThemeChanges(watchForSystemThemeChanges); - window.api.stopListeningForBatteryPowerStateChanges(watchPowerChanges); + window.api.theme.stoplisteningForSystemThemeChanges( + watchForSystemThemeChanges + ); + window.api.battery.stopListeningForBatteryPowerStateChanges( + watchPowerChanges + ); }; }, []); @@ -839,25 +843,33 @@ export default function App() { type: 'CURRENT_SONG_PLAYBACK_STATE', data: true, }); - window.api.songPlaybackStateChange(true); + window.api.playerControls.songPlaybackStateChange(true); }); player.addEventListener('pause', () => { dispatch({ type: 'CURRENT_SONG_PLAYBACK_STATE', data: false, }); - window.api.songPlaybackStateChange(false); + window.api.playerControls.songPlaybackStateChange(false); }); - window.api.beforeQuitEvent(handleBeforeQuitEvent); + window.api.quitEvent.beforeQuitEvent(handleBeforeQuitEvent); - window.api.onWindowBlur(() => manageWindowBlurOrFocus('blur')); - window.api.onWindowFocus(() => manageWindowBlurOrFocus('focus')); + window.api.windowControls.onWindowBlur(() => + manageWindowBlurOrFocus('blur') + ); + window.api.windowControls.onWindowFocus(() => + manageWindowBlurOrFocus('focus') + ); - window.api.onEnterFullscreen(() => manageWindowFullscreen('fullscreen')); - window.api.onLeaveFullscreen(() => manageWindowFullscreen('windowed')); + window.api.fullscreen.onEnterFullscreen(() => + manageWindowFullscreen('fullscreen') + ); + window.api.fullscreen.onLeaveFullscreen(() => + manageWindowFullscreen('windowed') + ); return () => { - window.api.removeBeforeQuitEventListener(handleBeforeQuitEvent); + window.api.quitEvent.removeBeforeQuitEventListener(handleBeforeQuitEvent); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -983,7 +995,7 @@ export default function App() { toggleShuffling(playback?.isShuffling); toggleRepeat(playback?.isRepeating); - window.api + window.api.audioLibraryControls .checkForStartUpSongs() .then((startUpSongData) => { if (startUpSongData) playSongFromUnknownSource(startUpSongData, true); @@ -1012,7 +1024,7 @@ export default function App() { queueId: queue.queueId, }; else { - window.api + window.api.audioLibraryControls .getAllSongs() .then((audioData) => { if (!audioData) return undefined; @@ -1032,7 +1044,7 @@ export default function App() { }, []); React.useEffect(() => { - window.api + window.api.userData .getUserData() .then((res) => { if (!res) return undefined; @@ -1044,20 +1056,28 @@ export default function App() { }) .catch((err) => console.error(err)); - window.api.toggleSongPlayback(() => { + window.api.playerControls.toggleSongPlayback(() => { console.log('Main requested song playback'); toggleSongPlayback(); }); - window.api.playSongFromUnknownSource((_, data) => { + window.api.unknownSource.playSongFromUnknownSource((_, data) => { playSongFromUnknownSource(data, true); }); - window.api.skipBackwardToPreviousSong(handleSkipBackwardClick); - window.api.skipForwardToNextSong(handleSkipForwardClick); + window.api.playerControls.skipBackwardToPreviousSong( + handleSkipBackwardClick + ); + window.api.playerControls.skipForwardToNextSong(handleSkipForwardClick); return () => { - window.api.removeTogglePlaybackStateEvent(toggleSongPlayback); - window.api.removeSkipBackwardToPreviousSongEvent(handleSkipBackwardClick); - window.api.removeSkipForwardToNextSongEvent(handleSkipForwardClick); - window.api.removeDataUpdateEventListeners(); + window.api.playerControls.removeTogglePlaybackStateEvent( + toggleSongPlayback + ); + window.api.playerControls.removeSkipBackwardToPreviousSongEvent( + handleSkipBackwardClick + ); + window.api.playerControls.removeSkipForwardToNextSongEvent( + handleSkipForwardClick + ); + window.api.dataUpdates.removeDataUpdateEventListeners(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -1071,10 +1091,10 @@ export default function App() { document.dispatchEvent(event); }; - window.api.dataUpdateEvent(noticeDataUpdateEvents); + window.api.dataUpdates.dataUpdateEvent(noticeDataUpdateEvents); return () => { - window.api.removeDataUpdateEventListeners(); + window.api.dataUpdates.removeDataUpdateEventListeners(); }; }, []); @@ -1207,7 +1227,8 @@ export default function App() { label: 'Resync Songs', iconClassName: 'sync', className: defaultButtonStyles, - clickHandler: () => window.api.resyncSongsLibrary(), + clickHandler: () => + window.api.audioLibraryControls.resyncSongsLibrary(), }); } if ( @@ -1291,9 +1312,11 @@ export default function App() { ); React.useEffect(() => { - window.api.getMessageFromMain(displayMessageFromMain); + window.api.messages.getMessageFromMain(displayMessageFromMain); return () => { - window.api.removeMessageToRendererEventListener(displayMessageFromMain); + window.api.messages.removeMessageToRendererEventListener( + displayMessageFromMain + ); }; }, [displayMessageFromMain]); @@ -1390,7 +1413,7 @@ export default function App() { if (!passedFullListenRange && seconds > (duration * 90) / 100) { passedFullListenRange = true; console.warn(`user listened to 90% of ${songId}`); - window.api.updateSongListeningData( + window.api.audioLibraryControls.updateSongListeningData( songId, 'fullListens', 'increment' @@ -1409,7 +1432,11 @@ export default function App() { console.warn(`user skipped ${songId} before 90% completion.`); if (!passedSkipRange) { console.warn(`user skipped ${songId}. before 10% completion.`); - window.api.updateSongListeningData(songId, 'skips', 'increment'); + window.api.audioLibraryControls.updateSongListeningData( + songId, + 'skips', + 'increment' + ); } abortController.abort(); clearInterval(intervalId); @@ -1433,7 +1460,7 @@ export default function App() { return toggleSongPlayback(); console.time('timeForSongFetch'); - return window.api + return window.api.audioLibraryControls .getSong(songId) .then((songData) => { console.timeEnd('timeForSongFetch'); @@ -1602,7 +1629,7 @@ export default function App() { const fetchSongFromUnknownSource = React.useCallback( (songPath: string) => { - window.api + window.api.unknownSource .getSongFromUnknownSource(songPath) .then((res) => playSongFromUnknownSource(res, true)) .catch((err) => { @@ -1672,7 +1699,7 @@ export default function App() { ) { player.currentTime = 0; toggleSongPlayback(true); - window.api.updateSongListeningData( + window.api.audioLibraryControls.updateSongListeningData( contentRef.current.currentSongData.songId, 'listens', 'increment' @@ -2090,7 +2117,7 @@ export default function App() { contentRef.current.currentSongData.isAFavorite !== newFavorite && !onlyChangeCurrentSongData ) { - window.api + window.api.playerControls .toggleLikeSongs( [contentRef.current.currentSongData.songId], newFavorite @@ -2194,7 +2221,7 @@ export default function App() { else if (e.ctrlKey && e.key === 's') toggleShuffling(); else if (e.ctrlKey && e.key === 't') toggleRepeat(); else if (e.ctrlKey && e.key === 'h') toggleIsFavorite(); - else if (e.ctrlKey && e.key === 'y') window.api.changeAppTheme(); + else if (e.ctrlKey && e.key === 'y') window.api.theme.changeAppTheme(); else if (e.ctrlKey && e.key === 'l') { const currentlyActivePage = content.navigationHistory.history[ @@ -2271,9 +2298,9 @@ export default function App() { // function key combinations else if (e.key === 'F5') { e.preventDefault(); - window.api.restartRenderer(`User request through F5.`); - } else if (e.key === 'F12' && !window.api.isInDevelopment) - window.api.openDevtools(); + window.api.appControls.restartRenderer(`User request through F5.`); + } else if (e.key === 'F12' && !window.api.properties.isInDevelopment) + window.api.settingsHelpers.openDevtools(); }, [ updateVolume, diff --git a/src/renderer/components/AlbumInfoPage/AlbumInfoPage.tsx b/src/renderer/components/AlbumInfoPage/AlbumInfoPage.tsx index c8797557..99836d53 100644 --- a/src/renderer/components/AlbumInfoPage/AlbumInfoPage.tsx +++ b/src/renderer/components/AlbumInfoPage/AlbumInfoPage.tsx @@ -88,7 +88,7 @@ const dropdownOptions: { label: string; value: SongSortTypes }[] = [ }, ]; -export default () => { +const AlbumInfoPage = () => { const { currentlyActivePage, queue, localStorageData } = useContext(AppContext); const { @@ -101,12 +101,12 @@ export default () => { const [albumContent, dispatch] = React.useReducer(reducer, { albumData: {} as Album, songsData: [] as SongData[], - sortingOrder: 'aToZ', + sortingOrder: 'trackNoAscending' as SongSortTypes, }); const fetchAlbumData = React.useCallback(() => { if (currentlyActivePage.data.albumId) { - window.api + window.api.albumsData .getAlbumData([currentlyActivePage.data.albumId as string]) .then((res) => { if (res && res.length > 0 && res[0]) { @@ -123,7 +123,7 @@ export default () => { albumContent.albumData.songs && albumContent.albumData.songs.length > 0 ) { - window.api + window.api.audioLibraryControls .getSongInfo( albumContent.albumData.songs.map((song) => song.songId), albumContent.sortingOrder @@ -410,3 +410,5 @@ export default () => { ); }; + +export default AlbumInfoPage; diff --git a/src/renderer/components/AlbumsPage/Album.tsx b/src/renderer/components/AlbumsPage/Album.tsx index c4d31ee9..f2fcd2b9 100644 --- a/src/renderer/components/AlbumsPage/Album.tsx +++ b/src/renderer/components/AlbumsPage/Album.tsx @@ -38,7 +38,7 @@ export const Album = (props: AlbumProp) => { const playAlbumSongs = React.useCallback( (isShuffle = false) => { - return window.api + return window.api.audioLibraryControls .getSongInfo( props.songs.map((song) => song.songId), undefined, @@ -66,7 +66,7 @@ export const Album = (props: AlbumProp) => { (isShuffle = false) => { const { multipleSelections: albumIds } = multipleSelectionsData; - window.api + window.api.albumsData .getAlbumData(albumIds) .then((albums) => { if (Array.isArray(albums) && albums.length > 0) { @@ -74,7 +74,7 @@ export const Album = (props: AlbumProp) => { .map((album) => album.songs.map((song) => song.songId)) .flat(); - return window.api.getSongInfo( + return window.api.audioLibraryControls.getSongInfo( albumSongIds, undefined, undefined, @@ -103,7 +103,7 @@ export const Album = (props: AlbumProp) => { const addToQueueForMultipleSelections = React.useCallback(() => { const { multipleSelections: albumIds } = multipleSelectionsData; - window.api + window.api.genresData .getGenresData(albumIds) .then((albums) => { if (Array.isArray(albums) && albums.length > 0) { @@ -111,7 +111,7 @@ export const Album = (props: AlbumProp) => { .map((album) => album.songs.map((song) => song.songId)) .flat(); - return window.api.getSongInfo( + return window.api.audioLibraryControls.getSongInfo( albumSongIds, undefined, undefined, @@ -187,7 +187,14 @@ export const Album = (props: AlbumProp) => { ]; if ((artists?.length ?? 1) - 1 !== i) - arr.push(,); + arr.push( + + , + + ); return arr; }) @@ -300,7 +307,7 @@ export const Album = (props: AlbumProp) => { ]); const contextMenuItemData = React.useMemo( - () => + (): ContextMenuAdditionalData => isMultipleSelectionEnabled && multipleSelectionsData.selectionType === 'album' && isAMultipleSelection @@ -312,12 +319,16 @@ export const Album = (props: AlbumProp) => { title: props.title, artworkPath: props?.artworkPaths?.optimizedArtworkPath, subTitle: `${props.songs.length} songs`, + subTitle2: + props.artists?.map((artist) => artist.name).join(', ') || + 'Unknown artist', }, [ isAMultipleSelection, isMultipleSelectionEnabled, multipleSelectionsData.multipleSelections.length, multipleSelectionsData.selectionType, + props.artists, props?.artworkPaths?.optimizedArtworkPath, props.songs.length, props.title, diff --git a/src/renderer/components/AlbumsPage/AlbumsPage.tsx b/src/renderer/components/AlbumsPage/AlbumsPage.tsx index 764df3dc..1b08fc05 100644 --- a/src/renderer/components/AlbumsPage/AlbumsPage.tsx +++ b/src/renderer/components/AlbumsPage/AlbumsPage.tsx @@ -45,7 +45,7 @@ const AlbumsPage = () => { const fetchAlbumData = React.useCallback( () => - window.api.getAlbumData([], sortingOrder).then((res) => { + window.api.albumsData.getAlbumData([], sortingOrder).then((res) => { if (res && Array.isArray(res)) { if (res.length > 0) setAlbumsData(res); else setAlbumsData([]); diff --git a/src/renderer/components/ArtistInfoPage/ArtistInfoPage.tsx b/src/renderer/components/ArtistInfoPage/ArtistInfoPage.tsx index 87e7a67e..408d352f 100644 --- a/src/renderer/components/ArtistInfoPage/ArtistInfoPage.tsx +++ b/src/renderer/components/ArtistInfoPage/ArtistInfoPage.tsx @@ -78,7 +78,7 @@ const ArtistInfoPage = () => { const fetchArtistsData = React.useCallback(() => { if (currentlyActivePage?.data?.artistId) { - window.api + window.api.artistsData .getArtistData([currentlyActivePage.data.artistId]) .then((res) => { if (res && res.length > 0) { @@ -97,7 +97,7 @@ const ArtistInfoPage = () => { const fetchArtistArtworks = React.useCallback(() => { if (artistData?.artistId) { - window.api + window.api.artistsData .getArtistArtworks(artistData.artistId) .then((x) => { if (x) @@ -121,19 +121,22 @@ const ArtistInfoPage = () => { const fetchSongsData = React.useCallback(() => { if (artistData?.songs && artistData.songs.length > 0) { - window.api - .getSongInfo(artistData.songs.map((song) => song.songId)) + window.api.audioLibraryControls + .getSongInfo( + artistData.songs.map((song) => song.songId), + sortingOrder + ) .then((songsData) => { if (songsData && songsData.length > 0) setSongs(songsData); return undefined; }) .catch((err) => console.error(err)); } - }, [artistData?.songs]); + }, [artistData?.songs, sortingOrder]); const fetchAlbumsData = React.useCallback(() => { if (artistData?.albums && artistData.albums.length > 0) { - window.api + window.api.albumsData .getAlbumData(artistData.albums.map((album) => album.albumId)) .then((res) => { if (res && res.length > 0) setAlbums(res); @@ -381,7 +384,7 @@ const ArtistInfoPage = () => { }`} clickHandler={() => { if (artistData) - window.api + window.api.artistsData .toggleLikeArtists( [artistData.artistId], !artistData.isAFavorite diff --git a/src/renderer/components/ArtistInfoPage/DuplicateArtistsSuggestion.tsx b/src/renderer/components/ArtistInfoPage/DuplicateArtistsSuggestion.tsx index 7eb47f90..e1b83f83 100644 --- a/src/renderer/components/ArtistInfoPage/DuplicateArtistsSuggestion.tsx +++ b/src/renderer/components/ArtistInfoPage/DuplicateArtistsSuggestion.tsx @@ -39,7 +39,7 @@ const DuplicateArtistsSuggestion = (props: Props) => { if (isIgnored) setIsVisible(false); if (name?.trim() && !isIgnored) { - window.api + window.api.suggestions .getArtistDuplicates(name) .then((res) => setDuplicateArtists(res)) .catch((err) => console.error(err)); @@ -91,7 +91,7 @@ const DuplicateArtistsSuggestion = (props: Props) => { .map((x) => x.artistId) .filter((id) => id !== selectedId); - window.api + window.api.suggestions .resolveArtistDuplicates(selectedId, duplicateIds) .then((res) => { if ( diff --git a/src/renderer/components/ArtistInfoPage/SeparateArtistsSuggestion.tsx b/src/renderer/components/ArtistInfoPage/SeparateArtistsSuggestion.tsx index 4a146f46..cb361856 100644 --- a/src/renderer/components/ArtistInfoPage/SeparateArtistsSuggestion.tsx +++ b/src/renderer/components/ArtistInfoPage/SeparateArtistsSuggestion.tsx @@ -50,11 +50,16 @@ const SeparateArtistsSuggestion = (props: Props) => { const artists = separatedArtistsNames.map((artist, i, arr) => { return ( <> - + {artist} {i !== arr.length - 1 && ( - {i === arr.length - 2 ? ' and ' : ', '} + ${arr[i + 1]}`}> + {i === arr.length - 2 ? ' and ' : ', '} + )} ); @@ -73,7 +78,7 @@ const SeparateArtistsSuggestion = (props: Props) => { setIsDisabled(true); setIsPending(true); - window.api + window.api.suggestions .resolveSeparateArtists(artistId, separatedArtistsNames) .then((res) => { if ( diff --git a/src/renderer/components/ArtistPage/Artist.tsx b/src/renderer/components/ArtistPage/Artist.tsx index d935dce3..0a3243b3 100644 --- a/src/renderer/components/ArtistPage/Artist.tsx +++ b/src/renderer/components/ArtistPage/Artist.tsx @@ -50,7 +50,7 @@ export const Artist = (props: ArtistProp) => { const playArtistSongs = React.useCallback( (isShuffle = false) => - window.api + window.api.audioLibraryControls .getSongInfo(props.songIds, undefined, undefined, true) .then((songs) => { if (Array.isArray(songs)) @@ -72,14 +72,14 @@ export const Artist = (props: ArtistProp) => { (isShuffling = false) => { const { multipleSelections: artistIds } = multipleSelectionsData; - return window.api + return window.api.artistsData .getArtistData(artistIds) .then((res) => { if (Array.isArray(res) && res.length > 0) { const songIds = res .map((artist) => artist.songs.map((song) => song.songId)) .flat(); - return window.api.getSongInfo(songIds); + return window.api.audioLibraryControls.getSongInfo(songIds); } return undefined; }) @@ -149,28 +149,30 @@ export const Artist = (props: ArtistProp) => { handlerFunction: () => { if (isMultipleSelectionsEnabled) { const { multipleSelections: artistIds } = multipleSelectionsData; - return window.api.getArtistData(artistIds).then((artists) => { - const songIds = artists - .map((artist) => artist.songs.map((song) => song.songId)) - .flat(); - const uniqueSongIds = [...new Set(songIds)]; - updateQueueData( - undefined, - [...queue.queue, ...uniqueSongIds], - false - ); - return addNewNotifications([ - { - id: `${uniqueSongIds.length}AddedToQueueFromMultiSelection`, - delay: 5000, - content: ( - - Added {uniqueSongIds.length} songs to the queue. - - ), - }, - ]); - }); + return window.api.artistsData + .getArtistData(artistIds) + .then((artists) => { + const songIds = artists + .map((artist) => artist.songs.map((song) => song.songId)) + .flat(); + const uniqueSongIds = [...new Set(songIds)]; + updateQueueData( + undefined, + [...queue.queue, ...uniqueSongIds], + false + ); + return addNewNotifications([ + { + id: `${uniqueSongIds.length}AddedToQueueFromMultiSelection`, + delay: 5000, + content: ( + + Added {uniqueSongIds.length} songs to the queue. + + ), + }, + ]); + }); } updateQueueData( undefined, @@ -213,7 +215,7 @@ export const Artist = (props: ArtistProp) => { handlerFunction: () => { const { multipleSelections: artistIds } = multipleSelectionsData; - return window.api + return window.api.artistsData .toggleLikeArtists( isMultipleSelectionsEnabled ? artistIds : [props.artistId] ) diff --git a/src/renderer/components/ArtistPage/ArtistPage.tsx b/src/renderer/components/ArtistPage/ArtistPage.tsx index 9ce5be75..79ac839e 100644 --- a/src/renderer/components/ArtistPage/ArtistPage.tsx +++ b/src/renderer/components/ArtistPage/ArtistPage.tsx @@ -44,7 +44,7 @@ const ArtistPage = () => { const fetchArtistsData = React.useCallback( () => - window.api.getArtistData([], sortingOrder).then((res) => { + window.api.artistsData.getArtistData([], sortingOrder).then((res) => { if (res && Array.isArray(res)) { if (res.length > 0) return setArtistsData(res); return setArtistsData([]); diff --git a/src/renderer/components/ContextMenu/ContextMenuDataItem.tsx b/src/renderer/components/ContextMenu/ContextMenuDataItem.tsx index 151a73ae..4de36f65 100644 --- a/src/renderer/components/ContextMenu/ContextMenuDataItem.tsx +++ b/src/renderer/components/ContextMenu/ContextMenuDataItem.tsx @@ -20,7 +20,9 @@ const ContextMenuDataItem = (props: { data: ContextMenuAdditionalData }) => { {subTitle} - {subTitle && subTitle2 && } + {subTitle && subTitle2 && ( + + )} {subTitle2} diff --git a/src/renderer/components/CurrentQueuePage/CurrentQueuePage.tsx b/src/renderer/components/CurrentQueuePage/CurrentQueuePage.tsx index 3eef3463..c18db0ad 100644 --- a/src/renderer/components/CurrentQueuePage/CurrentQueuePage.tsx +++ b/src/renderer/components/CurrentQueuePage/CurrentQueuePage.tsx @@ -83,7 +83,7 @@ const CurrentQueuePage = () => { return arr; }); } else { - window.api.getAllSongs().then((res) => { + window.api.audioLibraryControls.getAllSongs().then((res) => { if (res) { const x = queue.queue .map((songId) => { @@ -98,7 +98,8 @@ const CurrentQueuePage = () => { } }); } - }, [queue.queue, queuedSongs]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [queue.queue]); React.useEffect(() => { fetchAllSongsData(); @@ -143,7 +144,7 @@ const CurrentQueuePage = () => { } if (queue.queueId) { if (queue.queueType === 'artist') { - window.api.getArtistData([queue.queueId]).then((res) => { + window.api.artistsData.getArtistData([queue.queueId]).then((res) => { if (res && Array.isArray(res) && res[0]) { setQueueInfo((prevData) => { return { @@ -159,7 +160,7 @@ const CurrentQueuePage = () => { }); } if (queue.queueType === 'album') { - window.api.getAlbumData([queue.queueId]).then((res) => { + window.api.albumsData.getAlbumData([queue.queueId]).then((res) => { if (res && res.length > 0 && res[0]) { setQueueInfo((prevData) => { return { @@ -172,22 +173,24 @@ const CurrentQueuePage = () => { }); } if (queue.queueType === 'playlist') { - window.api.getPlaylistData([queue.queueId]).then((res) => { - if (res && res.length > 0 && res[0]) { - setQueueInfo((prevData) => { - return { - ...prevData, - artworkPath: res[0].artworkPaths - ? res[0].artworkPaths.artworkPath - : DefaultPlaylistCover, - title: res[0].name, - }; - }); - } - }); + window.api.playlistsData + .getPlaylistData([queue.queueId]) + .then((res) => { + if (res && res.length > 0 && res[0]) { + setQueueInfo((prevData) => { + return { + ...prevData, + artworkPath: res[0].artworkPaths + ? res[0].artworkPaths.artworkPath + : DefaultPlaylistCover, + title: res[0].name, + }; + }); + } + }); } if (queue.queueType === 'genre') { - window.api.getGenresData([queue.queueId]).then((res) => { + window.api.genresData.getGenresData([queue.queueId]).then((res) => { if (res && res.length > 0 && res[0]) { setQueueInfo((prevData) => { return { @@ -200,7 +203,7 @@ const CurrentQueuePage = () => { }); } if (queue.queueType === 'folder') { - window.api.getFolderData([queue.queueId]).then((res) => { + window.api.folderData.getFolderData([queue.queueId]).then((res) => { if (res && res.length > 0 && res[0]) { const folderName = res[0].path.split('\\').pop(); setQueueInfo((prevData) => { diff --git a/src/renderer/components/ErrorBoundary.tsx b/src/renderer/components/ErrorBoundary.tsx index a64e9335..21e75b23 100644 --- a/src/renderer/components/ErrorBoundary.tsx +++ b/src/renderer/components/ErrorBoundary.tsx @@ -4,7 +4,7 @@ import log from 'renderer/utils/log'; import BugImg from '../../../assets/images/svg/Bug Fixed_Monochromatic.svg'; import Button from './Button'; -const { isInDevelopment } = window.api; +const { isInDevelopment } = window.api.properties; interface ErrorBoundaryProps { children: React.ReactNode; @@ -46,11 +46,14 @@ class ErrorBoundary extends React.Component< {isInDevelopment && (
    - {this.state.error && this.state.error.toString()} -
    - {this.state.errorInfo.componentStack} + Details +

    + {this.state.error && this.state.error.toString()} +
    + {this.state.errorInfo.componentStack} +

    )}
    @@ -58,7 +61,9 @@ class ErrorBoundary extends React.Component< className="!mr-0 mt-4 !bg-background-color-3 text-sm text-font-color-black hover:border-background-color-3 dark:!bg-dark-background-color-3 dark:!text-font-color-black dark:hover:border-background-color-3" label="Restart App" iconName="restart_alt" - clickHandler={() => window.api.restartRenderer('error')} + clickHandler={() => + window.api.appControls.restartRenderer('error') + } />
diff --git a/src/renderer/components/ErrorPrompt.tsx b/src/renderer/components/ErrorPrompt.tsx index faed336c..b4ecdda3 100644 --- a/src/renderer/components/ErrorPrompt.tsx +++ b/src/renderer/components/ErrorPrompt.tsx @@ -49,7 +49,7 @@ export default (props: ErrorPromptProps) => { label="Restart App" iconName="sync" className="mt-6 w-fit !bg-background-color-3 !text-font-color-black hover:border-background-color-3 dark:!bg-dark-background-color-3 dark:text-font-color-black dark:hover:border-background-color-3" - clickHandler={() => window.api.restartRenderer(reason)} + clickHandler={() => window.api.appControls.restartRenderer(reason)} /> diff --git a/src/renderer/components/GenreInfoPage/GenreInfoPage.tsx b/src/renderer/components/GenreInfoPage/GenreInfoPage.tsx index b2d2ffd1..0b967562 100644 --- a/src/renderer/components/GenreInfoPage/GenreInfoPage.tsx +++ b/src/renderer/components/GenreInfoPage/GenreInfoPage.tsx @@ -66,7 +66,7 @@ const GenreInfoPage = () => { const fetchGenresData = React.useCallback(() => { if (currentlyActivePage.data) { - window.api + window.api.genresData .getGenresData([currentlyActivePage.data.genreId]) .then((res) => { if (res && res.length > 0 && res[0]) setGenreData(res[0]); @@ -78,7 +78,7 @@ const GenreInfoPage = () => { const fetchSongsData = React.useCallback(() => { if (genreData && genreData.songs && genreData.songs.length > 0) { - window.api + window.api.audioLibraryControls .getSongInfo( genreData.songs.map((song) => song.songId), sortingOrder diff --git a/src/renderer/components/GenresPage/Genre.tsx b/src/renderer/components/GenresPage/Genre.tsx index fe9498a7..d6a9c974 100644 --- a/src/renderer/components/GenresPage/Genre.tsx +++ b/src/renderer/components/GenresPage/Genre.tsx @@ -52,7 +52,7 @@ const Genre = (props: GenreProp) => { const playGenreSongs = React.useCallback( (isShuffle = false) => { - return window.api + return window.api.audioLibraryControls .getSongInfo(songIds, undefined, undefined, true) .then((songs) => { if (Array.isArray(songs)) @@ -74,7 +74,7 @@ const Genre = (props: GenreProp) => { const playGenreSongsForMultipleSelections = React.useCallback( (isShuffle = false) => { const { multipleSelections: genreIds } = multipleSelectionsData; - window.api + window.api.genresData .getGenresData(genreIds) .then((genres) => { if (Array.isArray(genres) && genres.length > 0) { @@ -82,7 +82,7 @@ const Genre = (props: GenreProp) => { .map((genre) => genre.songs.map((song) => song.songId)) .flat(); - return window.api.getSongInfo( + return window.api.audioLibraryControls.getSongInfo( genreSongIds, undefined, undefined, @@ -111,7 +111,7 @@ const Genre = (props: GenreProp) => { const addToQueueForMultipleSelections = React.useCallback(() => { const { multipleSelections: genreIds } = multipleSelectionsData; - window.api + window.api.genresData .getGenresData(genreIds) .then((genres) => { if (Array.isArray(genres) && genres.length > 0) { @@ -119,7 +119,7 @@ const Genre = (props: GenreProp) => { .map((genre) => genre.songs.map((song) => song.songId)) .flat(); - return window.api.getSongInfo( + return window.api.audioLibraryControls.getSongInfo( genreSongIds, undefined, undefined, diff --git a/src/renderer/components/GenresPage/GenresPage.tsx b/src/renderer/components/GenresPage/GenresPage.tsx index 446d7717..68219caa 100644 --- a/src/renderer/components/GenresPage/GenresPage.tsx +++ b/src/renderer/components/GenresPage/GenresPage.tsx @@ -48,7 +48,7 @@ const GenresPage = () => { MIN_ITEM_WIDTH + ((width % MIN_ITEM_WIDTH) - 10) / noOfColumns; const fetchGenresData = React.useCallback(() => { - window.api + window.api.genresData .getGenresData([], sortingOrder) .then((genres) => { if (genres && genres.length > 0) return setGenresData(genres); diff --git a/src/renderer/components/HomePage/HomePage.tsx b/src/renderer/components/HomePage/HomePage.tsx index 13bae118..c8b9d7da 100644 --- a/src/renderer/components/HomePage/HomePage.tsx +++ b/src/renderer/components/HomePage/HomePage.tsx @@ -101,7 +101,7 @@ const HomePage = () => { }, [recentlyAddedSongsContainerDiamensions]); const fetchLatestSongs = React.useCallback(() => { - window.api + window.api.audioLibraryControls .getAllSongs('dateAddedAscending', 1, noOfRecentlyAddedSongCards) .then((audioData) => { if (!audioData || audioData.data.length === 0) @@ -117,7 +117,7 @@ const HomePage = () => { }, [noOfRecentlyAddedSongCards]); const fetchRecentlyPlayedSongs = React.useCallback(async () => { - const recentSongs = await window.api + const recentSongs = await window.api.playlistsData .getPlaylistData(['History']) .catch((err) => console.error(err)); if ( @@ -126,7 +126,7 @@ const HomePage = () => { Array.isArray(recentSongs[0].songs) && recentSongs[0].songs.length > 0 ) - window.api + window.api.audioLibraryControls .getSongInfo( recentSongs[0].songs, undefined, @@ -157,7 +157,7 @@ const HomePage = () => { ]; if (artistIds.length > 0) - window.api + window.api.artistsData .getArtistData(artistIds, undefined, noOfRecentandLovedArtists) .then( (res) => @@ -173,11 +173,11 @@ const HomePage = () => { // ? Most loved songs are fetched after the user have made at least one favorite song from the library. const fetchMostLovedSongs = React.useCallback(() => { - window.api + window.api.playlistsData .getPlaylistData(['Favorites']) .then((res) => { if (Array.isArray(res) && res.length > 0) { - return window.api.getSongInfo( + return window.api.audioLibraryControls.getSongInfo( res[0].songs, 'allTimeMostListened', noOfRecentandLovedSongCards + 5, @@ -206,7 +206,7 @@ const HomePage = () => { .flat() ), ]; - window.api + window.api.artistsData .getArtistData(artistIds, undefined, noOfRecentandLovedArtists) .then( (res) => @@ -308,7 +308,7 @@ const HomePage = () => { const homePageContextMenus: ContextMenuItem[] = React.useMemo( () => - window.api.isInDevelopment + window.api.properties.isInDevelopment ? [ { label: 'Alert Error', diff --git a/src/renderer/components/HomePage/ResetAppConfirmationPrompt.tsx b/src/renderer/components/HomePage/ResetAppConfirmationPrompt.tsx index 9e61e582..4d148755 100644 --- a/src/renderer/components/HomePage/ResetAppConfirmationPrompt.tsx +++ b/src/renderer/components/HomePage/ResetAppConfirmationPrompt.tsx @@ -20,7 +20,7 @@ export default () => {