mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 02:24:16 +00:00
Merge pull request #146 from ragestudio/dev
This commit is contained in:
commit
add53cbe15
2
comty.js
2
comty.js
@ -1 +1 @@
|
||||
Subproject commit 7c11af2643de00423a6ac680bba235c3352620a0
|
||||
Subproject commit cbb45df2ef42205022e38e4c7d33a001e162c383
|
@ -1,9 +0,0 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Authenticating
|
||||
|
||||
## Server Keys
|
||||
|
||||
## User Token
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"label": "Definitions",
|
||||
"position": 6,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
# Like Status Object
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| post_id | string | |
|
||||
| liked | Boolean | |
|
||||
| count | Number | Current like count |
|
@ -1,8 +0,0 @@
|
||||
# Post Object
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| _id | string | |
|
||||
| user_id | string | |
|
||||
| message | string | |
|
||||
| created_at | string | |
|
||||
| updated_at | string | |
|
@ -1,6 +0,0 @@
|
||||
# Save Status Object
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| post_id | string | |
|
||||
| saved | Boolean | |
|
||||
| count | Number | Current global save count |
|
@ -1,37 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Getting Started with Comty.JS
|
||||
[https://github.com/ragestudio/comty.js](https://github.com/ragestudio/comty.js)
|
||||
|
||||
Welcome to Comty.JS, the official JavaScript library for interacting with the Comty API! This library is designed to simplify communication with Comty services, whether you're building a server-side application or a client-side interface.
|
||||
|
||||
## What is Comty.JS?
|
||||
|
||||
Comty.JS provides a convenient wrapper around the Comty API, handling authentication, request management, real-time communication via WebSockets, and more. It aims to make your development process smoother and more efficient by abstracting away the complexities of direct API interaction.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- **Simplified API Access:** Easy-to-use models for various Comty services like Authentication, Posts, User Management, Music, Chats, and more.
|
||||
- **Authentication Handling:** Built-in support for server keys and user token-based authentication, including automatic token refresh.
|
||||
- **WebSocket Integration:** Seamlessly connect to Comty's real-time services.
|
||||
- **Addon System:** Extend the library's functionality with custom addons.
|
||||
- **Environment Aware:** Works in both Node.js (server-side) and browser (client-side) environments.
|
||||
- **Request Management:** Uses `axios` for HTTP requests with interceptors for automatic token attachment and error handling.
|
||||
|
||||
## Who is this for?
|
||||
|
||||
This library is for developers who want to:
|
||||
|
||||
- Integrate their JavaScript or TypeScript applications with Comty.
|
||||
- Build features that utilize Comty's social, music, or other platform functionalities.
|
||||
- Quickly set up communication with the Comty API without dealing with raw HTTP requests and WebSocket management.
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **[Installing Comty.JS](./installing.md)**: Learn how to add the library to your project.
|
||||
2. **[Client Initialization](./client-initialization.md)**: Understand how to set up and configure the Comty.JS client.
|
||||
3. **[Authentication](./authentication.md)**: Dive into how authentication works with Comty.JS.
|
||||
|
||||
We're excited to see what you build with Comty.JS!
|
44
docs/comty-js/index.mdx
Normal file
44
docs/comty-js/index.mdx
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
id: index
|
||||
title: Comty.js Library
|
||||
sidebar_label: Introduction
|
||||
---
|
||||
|
||||
Welcome to the documentation for the `comty.js` library. This library provides a set of modules and classes for interacting with the Comty platform.
|
||||
|
||||
## Core Models
|
||||
|
||||
The following are the core models available in the `comty.js` library:
|
||||
|
||||
* [AddonsManager](models/addons): Manages addons within the library.
|
||||
* [AuthModel](models/auth): Handles user authentication and session management.
|
||||
* [MusicModel](models/music): Handles Music Data.
|
||||
* [Post](models/post): Handles Post Data.
|
||||
* [Search](models/search): Handles Search requests.
|
||||
* [SessionModel](models/session): Manages user sessions and tokens.
|
||||
* [UserModel](models/user): Manages User Data.
|
||||
|
||||
## Helpers
|
||||
|
||||
* [Remotes](remotes): Describes all the remotes.
|
||||
* [Settings](settings): Describes all the settings.
|
||||
* [Storage](storage): Describes all the storage.
|
||||
* [WebsocketManager](ws): Handles websocket connections.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started with the `comty.js` library, you can install it using npm:
|
||||
|
||||
```bash
|
||||
npm install comty.js
|
||||
```
|
||||
|
||||
Then, you can import the modules you need into your project:
|
||||
|
||||
```javascript
|
||||
import { AuthModel, SessionModel } from 'comty.js';
|
||||
|
||||
// Use the modules
|
||||
AuthModel.login({ username: 'myuser', password: 'mypassword' })
|
||||
.then(data => console.log(data));
|
||||
```
|
@ -1,103 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Installing Comty.JS
|
||||
|
||||
To get started with Comty.JS, you need to add it as a dependency to your project. The library is available on npm.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Node.js (version 12 or higher recommended, as per `jsonwebtoken` and `sucrase` dependencies)
|
||||
* A package manager like npm or Yarn
|
||||
|
||||
## Installation
|
||||
|
||||
You can install Comty.JS using either npm or Yarn:
|
||||
|
||||
### Using npm
|
||||
|
||||
```bash
|
||||
npm install comty.js
|
||||
```
|
||||
|
||||
### Using Yarn
|
||||
|
||||
```bash
|
||||
yarn add comty.js
|
||||
```
|
||||
|
||||
This will download Comty.JS and add it to your project's `node_modules` directory. The following dependencies will also be installed:
|
||||
|
||||
* `@foxify/events`: ^2.1.0
|
||||
* `axios`: ^1.8.4
|
||||
* `js-cookie`: ^3.0.5
|
||||
* `jsonwebtoken`: ^9.0.0
|
||||
* `jwt-decode`: ^4.0.0
|
||||
* `linebridge-client`: ^1.1.1
|
||||
* `luxon`: ^3.6.0
|
||||
* `socket.io-client`: ^4.8.1
|
||||
|
||||
For development, if you plan to contribute or build the library locally, you'll also need:
|
||||
|
||||
* `@ragestudio/hermes`: ^1.0.1 (used for building the project)
|
||||
|
||||
## Importing the library
|
||||
|
||||
Once installed, you can import Comty.JS into your project:
|
||||
|
||||
### ES Modules (JavaScript or TypeScript)
|
||||
|
||||
```javascript
|
||||
import createClient from 'comty.js';
|
||||
// or for specific models if needed (though typically client is the main entry)
|
||||
// import { AuthModel, PostModel } from 'comty.js/models'; // Adjust path based on actual export structure if modular imports are supported
|
||||
```
|
||||
|
||||
### CommonJS (Node.js)
|
||||
|
||||
```javascript
|
||||
const createClient = require('comty.js');
|
||||
// or for specific models
|
||||
// const { AuthModel, PostModel } = require('comty.js/models'); // Adjust path
|
||||
```
|
||||
|
||||
If you look at the `package.json`, the main entry point is `"./dist/index.js"`.
|
||||
|
||||
```json comty-project/public-repo/comty.js/package.json#L3
|
||||
{
|
||||
"name": "comty.js",
|
||||
"version": "0.65.5",
|
||||
"main": "./dist/index.js",
|
||||
"description": "Official Comty API for JavaScript",
|
||||
"homepage": "https://github.com/ragestudio/comty.js",
|
||||
"author": "RageStudio <support@ragestudio.net>",
|
||||
"scripts": {
|
||||
"build": "hermes build"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@foxify/events": "^2.1.0",
|
||||
"axios": "^1.8.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"linebridge-client": "^1.1.1",
|
||||
"luxon": "^3.6.0",
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ragestudio/hermes": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Now you're ready to initialize the client and start interacting with the Comty API.
|
||||
|
||||
## Next Steps
|
||||
|
||||
* **[Client Initialization](./client-initialization.md)**: Learn how to set up and configure the Comty.JS client.
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"label": "Models",
|
||||
"position": 4,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
46
docs/comty-js/models/addons.mdx
Normal file
46
docs/comty-js/models/addons.mdx
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
id: addons
|
||||
title: AddonsManager
|
||||
sidebar_label: AddonsManager
|
||||
---
|
||||
|
||||
## AddonsManager
|
||||
|
||||
The `AddonsManager` class provides a way to register, retrieve, and manage addons within the comty.js library.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `AddonsManager` class allows you to register addons, retrieve them by name, and find addons that implement specific operations. It uses a `Map` internally to store the registered addons.
|
||||
|
||||
### Properties
|
||||
|
||||
* `addons`: A `Map` that stores the registered addons. The keys are the addon names, and the values are the addon instances.
|
||||
|
||||
### Methods
|
||||
|
||||
* `register(name, addon)`
|
||||
* Registers a new addon with the specified name.
|
||||
* Parameters:
|
||||
* `name`: *string* The name of the addon.
|
||||
* `addon`: *object* The addon instance.
|
||||
* Returns: void
|
||||
|
||||
* `get(name)`
|
||||
* Retrieves an addon by its name.
|
||||
* Parameters:
|
||||
* `name`: *string* The name of the addon to retrieve.
|
||||
* Returns: *object | undefined* The addon instance if found, otherwise `undefined`.
|
||||
|
||||
* `getByOperation(operation)`
|
||||
* Searches all registered addons and returns an array of addons that have a function for the specified operation.
|
||||
* Parameters:
|
||||
* `operation`: *string* The name of the operation to search for.
|
||||
* Returns: *Array[object]* An array of objects, where each object contains the addon's ID and the corresponding function for the specified operation. Each object has the following structure:
|
||||
* `id`: *string* The ID of the addon (addon.constructor.id).
|
||||
* `fn`: *function* The addon's function for the specified operation (addon[operation]).
|
||||
|
||||
### API Reference
|
||||
|
||||
* `register(name: string, addon: object)`: void - Registers a new addon.
|
||||
* `get(name: string)`: object | undefined - Retrieves an addon by name.
|
||||
* `getByOperation(operation: string)`: Array[object] - Gets addons by operation.
|
101
docs/comty-js/models/auth.mdx
Normal file
101
docs/comty-js/models/auth.mdx
Normal file
@ -0,0 +1,101 @@
|
||||
---
|
||||
id: auth
|
||||
title: AuthModel
|
||||
sidebar_label: AuthModel
|
||||
---
|
||||
|
||||
## AuthModel
|
||||
|
||||
The `AuthModel` class provides static methods for handling user authentication, registration, and session management.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `AuthModel` class provides methods for user login, logout, registration, token verification, username validation, password management, and account activation/disabling.
|
||||
|
||||
### Methods
|
||||
|
||||
* `login(payload, callback)`
|
||||
* Asynchronously handles the login process.
|
||||
* Parameters:
|
||||
* `payload`: *object* An object containing the username, password, and MFA code if required.
|
||||
* `callback`: *function, optional* A callback function to handle further actions after login.
|
||||
* Returns: *Promise[object | boolean]* A Promise that resolves with the response data if login is successful. Returns `false` if MFA is required.
|
||||
|
||||
* `logout()`
|
||||
* Asynchronously logs out the user by destroying the current session and emitting an event for successful logout.
|
||||
* Returns: *Promise[void]* A Promise that resolves after the logout process is completed.
|
||||
|
||||
* `register(payload)`
|
||||
* Registers a new user with the provided payload.
|
||||
* Parameters:
|
||||
* `payload`: *object* An object containing the user's information username, password, email, tos.
|
||||
* Returns: *Promise[object]* A Promise that resolves with the response data if registration is successful.
|
||||
* Throws: Error if the registration fails.
|
||||
|
||||
* `authToken(token)`
|
||||
* Verifies the given token and returns the user data associated with it.
|
||||
* Parameters:
|
||||
* `token`: *string, optional* The token to verify. If not provided, the stored token is used.
|
||||
* Returns: *Promise[object]* A Promise that resolves with the user data if the token is valid.
|
||||
* Throws: Error if there was an issue with the request.
|
||||
|
||||
* `usernameValidation(username)`
|
||||
* Validates the existence of a username.
|
||||
* Parameters:
|
||||
* `username`: *string* The username to validate.
|
||||
* Returns: *Promise[boolean | object]* A Promise that resolves with the response data if the validation is successful, or `false` if there was an error.
|
||||
* Throws: Error if the validation fails.
|
||||
|
||||
* `availability(payload)`
|
||||
* Retrieves the availability of a username and email.
|
||||
* Parameters:
|
||||
* `payload`: *object* An object containing the username and email to check availability for.
|
||||
* Returns: *Promise[object | boolean]* A Promise that resolves with the availability data if successful, or `false` if an error occurred.
|
||||
|
||||
* `changePassword(payload)`
|
||||
* Changes the user's password.
|
||||
* Parameters:
|
||||
* `payload`: *object* An object containing the currentPassword, newPassword, and code optional.
|
||||
* Returns: *Promise[object]* The data response after changing the password.
|
||||
|
||||
* `activateAccount(user_id, code)`
|
||||
* Activates a user account using the provided activation code.
|
||||
* Parameters:
|
||||
* `user_id`: *string* The ID of the user to activate.
|
||||
* `code`: *string* The activation code sent to the user's email.
|
||||
* Returns: *Promise[object]* A Promise that resolves with the response data after activation.
|
||||
* Throws: Error if the activation process fails.
|
||||
|
||||
* `resendActivationCode(user_id)`
|
||||
* Resends the activation code to the user.
|
||||
* Parameters:
|
||||
* `user_id`: *string* The ID of the user to resend the activation code to.
|
||||
* Returns: *Promise[object]* A Promise that resolves with the response data after sending the activation code.
|
||||
* Throws: Error if the resend activation code process fails.
|
||||
|
||||
* `disableAccount(options)`
|
||||
* Disables the user's account.
|
||||
* Parameters:
|
||||
* `options`: *object, optional* An object containing options for disabling the account.
|
||||
* `confirm`: *boolean* Confirmation to disable the account.
|
||||
* Returns: *Promise[object]* A Promise that resolves with the response data after disabling the account.
|
||||
|
||||
* `recoverPassword(usernameOrEmail)`
|
||||
* Recovers the password for a user account.
|
||||
* Parameters:
|
||||
* `usernameOrEmail`: *string* The username or email associated with the account to recover.
|
||||
* Returns: *Promise[object]* A Promise that resolves with the response data after initiating the password recovery process.
|
||||
|
||||
### API Reference
|
||||
|
||||
* `login(payload: object, callback: function | undefined)`: *Promise[object | boolean]* - Asynchronously handles the login process.
|
||||
* `logout()`: *Promise[void]* - Asynchronously logs out the user.
|
||||
* `register(payload: object)`: *Promise[object]* - Registers a new user.
|
||||
* `authToken(token: string | undefined)`: *Promise[object]* - Verifies the given token.
|
||||
* `usernameValidation(username: string)`: *Promise[boolean | object]* - Validates the existence of a username.
|
||||
* `availability(payload: object)`: *Promise[object | boolean]* - Retrieves the availability of a username and email.
|
||||
* `changePassword(payload: object)`: *Promise[object]* - Changes the user's password.
|
||||
* `activateAccount(user_id: string, code: string)`: *Promise[object]* - Activates a user account.
|
||||
* `resendActivationCode(user_id: string)`: *Promise[object]* - Resends the activation code.
|
||||
* `disableAccount(options: object | undefined)`: *Promise[object]* - Disables the user's account.
|
||||
* `recoverPassword(usernameOrEmail: string)`: *Promise[object]* - Recovers the password for a user account.
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"label": "Auth",
|
||||
"position": 1,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Check if username or email is available
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Update password
|
@ -1,79 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Login (Credentials)
|
||||
This method allows you to create a auth session with a username and password.
|
||||
|
||||
:::info
|
||||
Use of [**server keys**](/docs/comty-js/authentication#server-keys) is recommended instead using credentials.
|
||||
:::
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Parameters
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [payload](#object-payload) | Object | true | | |
|
||||
| [callback](#function-callback) | Function | false | | |
|
||||
|
||||
#### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| username | String | true | | |
|
||||
| password | String | true | | |
|
||||
| mfa_code | String | false | | Required if MFA is enabled for this user |
|
||||
|
||||
#### [Function] Callback
|
||||
Executed on successful login
|
||||
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| data | Object | [Successful Auth](#successful-auth) |
|
||||
|
||||
#### [Object] Successful Auth
|
||||
Contains the token and refresh token
|
||||
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| token | String | |
|
||||
| refreshToken | String | |
|
||||
| expires_in | String | |
|
||||
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Examples
|
||||
Basic usage
|
||||
|
||||
```js
|
||||
const auth = await AuthModel.login({
|
||||
username: "testuser",
|
||||
password: "testpassword",
|
||||
})
|
||||
|
||||
console.log(auth)
|
||||
|
||||
// returns
|
||||
// {
|
||||
// token: "xxxx",
|
||||
// refreshToken: "xxxx",
|
||||
// }
|
||||
```
|
||||
|
||||
Using Callback
|
||||
|
||||
```js
|
||||
AuthModel.login({
|
||||
username: "testuser",
|
||||
password: "testpassword",
|
||||
}, (data) => {
|
||||
console.log(data)
|
||||
|
||||
// returns
|
||||
// {
|
||||
// token: "xxxx",
|
||||
// refreshToken: "xxxx",
|
||||
// }
|
||||
})
|
||||
```
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Logout
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Create a new Account
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Check if exist a username
|
@ -1 +0,0 @@
|
||||
# Chats
|
29
docs/comty-js/models/chats.mdx
Normal file
29
docs/comty-js/models/chats.mdx
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
id: chats
|
||||
title: ChatsService
|
||||
sidebar_label: ChatsService
|
||||
---
|
||||
|
||||
## ChatsService
|
||||
|
||||
The `ChatsService` class provides static methods for interacting with chat data.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `ChatsService` class offers methods for retrieving chat history and recent chats for a user.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `getChatHistory(chat_id)`
|
||||
|
||||
Retrieves the chat history for a given chat ID.
|
||||
|
||||
* `chat_id`: *string* The ID of the chat.
|
||||
* Returns: A Promise that resolves with the chat history data.
|
||||
* Throws: Error if the chat_id is not provided.
|
||||
|
||||
* `getRecentChats()`
|
||||
|
||||
Retrieves the recent chats for the current user.
|
||||
|
||||
* Returns: A Promise that resolves with the chat history data.
|
31
docs/comty-js/models/e2e.mdx
Normal file
31
docs/comty-js/models/e2e.mdx
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
id: e2e
|
||||
title: E2EModel
|
||||
sidebar_label: E2EModel
|
||||
---
|
||||
|
||||
## E2EModel
|
||||
|
||||
The `E2EModel` class provides static methods for managing end-to-end encryption keys.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `E2EModel` class offers methods for retrieving and updating key pairs.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `getKeyPair()`
|
||||
|
||||
Retrieves the key pair for the current user.
|
||||
|
||||
* Returns: A Promise that resolves with the key pair data.
|
||||
|
||||
* `updateKeyPair(str, { imSure = false } = {})`
|
||||
|
||||
Updates the key pair for the current user.
|
||||
|
||||
* `str`: *string* The new key pair, encoded as a string.
|
||||
* `{ imSure = false }`: *object, optional* Options for the update.
|
||||
* `imSure`: *boolean, optional* Confirmation to update the keypair. Must be set to `true` to proceed.
|
||||
* Returns: A Promise that resolves with the updated key pair data.
|
||||
* Throws: Error if confirmation is missing.
|
28
docs/comty-js/models/events.mdx
Normal file
28
docs/comty-js/models/events.mdx
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
id: events
|
||||
title: EventsModel
|
||||
sidebar_label: EventsModel
|
||||
---
|
||||
|
||||
## EventsModel
|
||||
|
||||
The `EventsModel` class provides static methods for retrieving event data.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `EventsModel` class offers methods for retrieving featured events and data for a specific event.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `getFeatured()`
|
||||
|
||||
Retrieves featured events.
|
||||
|
||||
* Returns: A Promise that resolves with the featured events data.
|
||||
|
||||
* `data(id)`
|
||||
|
||||
Retrieves data for a specific event.
|
||||
|
||||
* `id`: *string* The ID of the event.
|
||||
* Returns: A Promise that resolves with the event data.
|
@ -1 +0,0 @@
|
||||
# Feed
|
51
docs/comty-js/models/feed.mdx
Normal file
51
docs/comty-js/models/feed.mdx
Normal file
@ -0,0 +1,51 @@
|
||||
---
|
||||
id: feed
|
||||
title: FeedModel
|
||||
sidebar_label: FeedModel
|
||||
---
|
||||
|
||||
## FeedModel
|
||||
|
||||
The `FeedModel` class provides static methods for retrieving feed data.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `FeedModel` class offers methods for retrieving music, global music, timeline, and global timeline feeds.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `getMusicFeed({ page, limit } = {})`
|
||||
|
||||
Retrieves music feed.
|
||||
|
||||
* `{ page, limit }`: *object, optional* An object containing the page and limit.
|
||||
* `page`: *number, optional* The number of items to page from the feed.
|
||||
* `limit`: *number, optional* The maximum number of items to fetch from the feed.
|
||||
* Returns: A Promise that resolves with the music feed data.
|
||||
|
||||
* `getGlobalMusicFeed({ page, limit } = {})`
|
||||
|
||||
Retrieves global music feed.
|
||||
|
||||
* `{ page, limit }`: *object, optional* An object containing the page and limit.
|
||||
* `page`: *number, optional* The number of items to page from the feed.
|
||||
* `limit`: *number, optional* The maximum number of items to fetch from the feed.
|
||||
* Returns: A Promise that resolves with the global music feed data.
|
||||
|
||||
* `getTimelineFeed({ page, limit } = {})`
|
||||
|
||||
Retrieves timeline feed.
|
||||
|
||||
* `{ page, limit }`: *object, optional* An object containing the page and limit.
|
||||
* `page`: *number, optional* The number of feed items to page.
|
||||
* `limit`: *number, optional* The maximum number of feed items to retrieve.
|
||||
* Returns: A Promise that resolves with the timeline feed data.
|
||||
|
||||
* `getGlobalTimelineFeed({ page, limit } = {})`
|
||||
|
||||
Retrieves global timeline feed.
|
||||
|
||||
* `{ page, limit }`: *object, optional* An object containing the page and limit.
|
||||
* `page`: *number, optional* The number of items to page from the feed.
|
||||
* `limit`: *number, optional* The maximum number of posts to fetch from the feed.
|
||||
* Returns: A Promise that resolves with the posts feed data.
|
38
docs/comty-js/models/follows.mdx
Normal file
38
docs/comty-js/models/follows.mdx
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
id: follows
|
||||
title: FollowsModel
|
||||
sidebar_label: FollowsModel
|
||||
---
|
||||
|
||||
## FollowsModel
|
||||
|
||||
The `FollowsModel` class provides static methods for interacting with user follow relationships.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `FollowsModel` class offers methods for checking if a user is following another user, retrieving followers, and toggling the follow status.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `imFollowing(user_id)`
|
||||
|
||||
Checks if the current user is following the specified user.
|
||||
|
||||
* `user_id`: *string* The ID of the user to check if the current user is following.
|
||||
* Returns: A Promise that resolves with the response data indicating if the current user is following the specified user.
|
||||
* Throws: Error if the user_id parameter is not provided.
|
||||
|
||||
* `getFollowers(user_id, fetchData)`
|
||||
|
||||
Retrieves the list of followers for a given user.
|
||||
|
||||
* `user_id`: *string, optional* The ID of the user. If not provided, the current user ID will be used.
|
||||
* `fetchData`: *boolean* Whether to fetch additional data for each follower. Defaults to false.
|
||||
* Returns: A promise that resolves with the list of followers and their data.
|
||||
|
||||
* `toggleFollow({ user_id })`
|
||||
|
||||
Toggles the follow status for a user.
|
||||
|
||||
* `user_id`: *string* The ID of the user to toggle follow status.
|
||||
* Returns: A promise that resolves with the response data after toggling follow status.
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"label": "Follows",
|
||||
"position": 3,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Get followers
|
||||
Retrieves the list of followers for a given user.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function FollowsModel.getFollowers(user_id, fetchData)
|
||||
```
|
||||
|
||||
### Arguments
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| user_id | String | false | | |
|
||||
| fetchData | Boolean | true | false | If true, the response will contain an array of users data|
|
||||
| limit | Number | true | 10 | Only if fetchData is true. Limit the number of followers to fetch |
|
||||
| offset | Number | true | 0 | Only if fetchData is true. Offset the list of followers to fetch |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const followers = await FollowsModel.getFollowers("0000")
|
||||
|
||||
console.log(followers)
|
||||
|
||||
// result: {
|
||||
// count: 10
|
||||
// }
|
||||
```
|
||||
|
||||
### Retrieve user data
|
||||
```js
|
||||
const followers = await FollowsModel.getFollowers("0000", true, 50, 0)
|
||||
|
||||
console.log(followers)
|
||||
|
||||
// result: {
|
||||
// count: 10
|
||||
// list: [
|
||||
// {
|
||||
// _id_: "0000",
|
||||
// username: "comty",
|
||||
// },
|
||||
// {
|
||||
// _id_: "0001",
|
||||
// username: "john",
|
||||
// },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
```
|
@ -1 +0,0 @@
|
||||
# Music
|
159
docs/comty-js/models/music.mdx
Normal file
159
docs/comty-js/models/music.mdx
Normal file
@ -0,0 +1,159 @@
|
||||
---
|
||||
id: music
|
||||
title: MusicModel
|
||||
sidebar_label: MusicModel
|
||||
---
|
||||
|
||||
## MusicModel
|
||||
|
||||
The `MusicModel` class provides static methods for interacting with music-related data. It encapsulates various getters and setters for tracks, releases, library management, and search functionalities.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `MusicModel` class acts as a facade, providing a simplified interface to access and manipulate music data through its nested `Getters` and `Setters` classes.
|
||||
|
||||
### Static Properties
|
||||
|
||||
* `Getters`: An object containing static getter methods for retrieving music data.
|
||||
* `Setters`: An object containing static setter methods for modifying music data.
|
||||
|
||||
### Track Related Methods
|
||||
|
||||
* `getAllTracks( { user_id, limit, page } )`
|
||||
* Retrieves tracks for a given user.
|
||||
* Parameters:
|
||||
* `user_id`: *String* The ID of the user.
|
||||
* `limit`: *Number* The number of tracks to retrieve per page.
|
||||
* `page`: *Number* The page number to retrieve.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
* `getTrackData(id: String, options: Object)`
|
||||
* Retrieves data for a specific track.
|
||||
* Parameters:
|
||||
* `id`: *String* The ID of the track.
|
||||
* `options`: *Object* Additional options for the request.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
* `putTrack(track)`
|
||||
* Creates/Updates a track.
|
||||
* Parameters:
|
||||
* `track`: *Object* The track object to create/update.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
### Lyrics Related Methods
|
||||
|
||||
* `getTrackLyrics(id: String, options = { preferTranslation: false })`
|
||||
* Retrieves lyrics for a specific track.
|
||||
* Parameters:
|
||||
* `id`: *String* The ID of the track.
|
||||
* `options`: *Object*
|
||||
* `preferTranslation`: *Boolean* If true, attempts to retrieve lyrics in the user's preferred language.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
* `putTrackLyrics(track_id, data)`
|
||||
* Updates lyrics for a specific track.
|
||||
* Parameters:
|
||||
* `track_id`: *String* The ID of the track.
|
||||
* `data`: *Object* The lyrics data to update.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
### Release Related Methods
|
||||
|
||||
* `getMyReleases({ limit, offset, keywords })`
|
||||
* Retrieves the user's releases.
|
||||
* Parameters:
|
||||
* `limit`: *Number* The number of releases to retrieve.
|
||||
* `offset`: *Number* The offset to start retrieving from.
|
||||
* `keywords`: *String* Keywords to search for.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
* `getAllReleases({ user_id, limit, page })`
|
||||
* Retrieves releases for a given user.
|
||||
* Parameters:
|
||||
* `user_id`: *String* The ID of the user.
|
||||
* `limit`: *Number* The number of releases to retrieve per page.
|
||||
* `page`: *Number* The page number to retrieve.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
* `getReleaseData(id: String)`
|
||||
* Retrieves data for a specific release.
|
||||
* Parameters:
|
||||
* `id`: *String* The ID of the release.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
* `putRelease(release)`
|
||||
* Creates/Updates a release.
|
||||
* Parameters:
|
||||
* `release`: *Object* The release object to create/update.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
* `deleteRelease(release_id)`
|
||||
* Deletes a release.
|
||||
* Parameters:
|
||||
* `release_id`: *String* The ID of the release to delete.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
### Library Related Methods
|
||||
|
||||
* `getMyLibrary({ limit = 100, offset = 0, order = "desc", kind })`
|
||||
* Retrieves the user's music library.
|
||||
* Parameters:
|
||||
* `limit`: *Number* The number of items to retrieve.
|
||||
* `offset`: *Number* The offset to start retrieving from.
|
||||
* `order`: *String* The order to sort the items in ("asc" or "desc").
|
||||
* `kind`: *String* Filter the library by kind (e.g., "track", "release").
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
* `toggleItemFavorite(type, item_id, to)`
|
||||
* Toggles the favorite status of an item in the library.
|
||||
* Parameters:
|
||||
* `type`: *String* The type of item ("track", "release", etc.).
|
||||
* `item_id`: *String* The ID of the item.
|
||||
* `to`: *Boolean* Whether to add to favorites (true) or remove (false).
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
* `isItemFavorited(type, item_id)`
|
||||
* Checks if an item is favorited in the library.
|
||||
* Parameters:
|
||||
* `type`: *String* The type of item ("track", "release", etc.).
|
||||
* `item_id`: *String* The ID of the item.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
### Other Methods
|
||||
|
||||
* `getRecentyPlayed(params)`
|
||||
* Retrieves recently played tracks.
|
||||
* Parameters:
|
||||
* `params`: *Object* Additional parameters for the request.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
* `search({ keywords, limit, offset })`
|
||||
* Searches for music items.
|
||||
* Parameters:
|
||||
* `keywords`: *String* The search keywords.
|
||||
* `limit`: *Number* The number of results to return.
|
||||
* `offset`: *Number* The offset to start the search from.
|
||||
* Returns: *Promise[object]*
|
||||
|
||||
### Aliases
|
||||
|
||||
* `toggleItemFavourite`: Alias for `toggleItemFavorite`.
|
||||
* `isItemFavourited`: Alias for `isItemFavorited`.
|
||||
|
||||
### API Reference
|
||||
|
||||
* `getAllTracks( { user_id: String, limit: Number, page: Number } )`: *Promise[object]*
|
||||
* `getTrackData(id: String, options: Object)`: *Promise[object]*
|
||||
* `putTrack(track: Object)`: *Promise[object]*
|
||||
* `getTrackLyrics(id: String, options: Object)`: *Promise[object]*
|
||||
* `putTrackLyrics(track_id: String, data: Object)`: *Promise[object]*
|
||||
* `getMyReleases({ limit: Number, offset: Number, keywords: String })`: *Promise[object]*
|
||||
* `getAllReleases({ user_id: String, limit: Number, page: Number })`: *Promise[object]*
|
||||
* `getReleaseData(id: String)`: *Promise[object]*
|
||||
* `putRelease(release: Object)`: *Promise[object]*
|
||||
* `deleteRelease(release_id: String)`: *Promise[object]*
|
||||
* `getMyLibrary({ limit: Number, offset: Number, order: String, kind: String })`: *Promise[object]*
|
||||
* `toggleItemFavorite(type: String, item_id: String, to: Boolean)`: *Promise[object]*
|
||||
* `isItemFavorited(type: String, item_id: String)`: *Promise[object]*
|
||||
* `getRecentyPlayed(params: Object)`: *Promise[object]*
|
||||
* `search({ keywords: String, limit: Number, offset: Number })`: *Promise[object]*
|
@ -1 +0,0 @@
|
||||
# NFC
|
54
docs/comty-js/models/nfc.mdx
Normal file
54
docs/comty-js/models/nfc.mdx
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
id: nfc
|
||||
title: NFCModel
|
||||
sidebar_label: NFCModel
|
||||
---
|
||||
|
||||
## NFCModel
|
||||
|
||||
The `NFCModel` class provides static methods for interacting with NFC tags.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `NFCModel` class offers methods for retrieving, registering, and deleting NFC tags.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `getOwnTags()`
|
||||
|
||||
Retrieves the list of tags owned by the current user.
|
||||
|
||||
* Returns: A Promise that resolves with the data of the tags.
|
||||
|
||||
* `getTagById(id)`
|
||||
|
||||
Retrieves a tag by its ID.
|
||||
|
||||
* `id`: *string* The ID of the tag to retrieve.
|
||||
* Returns: The data of the retrieved tag.
|
||||
* Throws: Error if the ID is not provided.
|
||||
|
||||
* `getTagBySerial(serial)`
|
||||
|
||||
Retrieves a tag by its serial number.
|
||||
|
||||
* `serial`: *string* The serial number of the tag to retrieve.
|
||||
* Returns: A Promise that resolves with the data of the tag.
|
||||
* Throws: Error if the serial number is not provided.
|
||||
|
||||
* `registerTag(serial, payload)`
|
||||
|
||||
Registers a tag with the given serial number and payload.
|
||||
|
||||
* `serial`: *string* The serial number of the tag.
|
||||
* `payload`: *object* The payload data for the tag.
|
||||
* Returns: The data of the registered tag.
|
||||
* Throws: Error if the serial or payload is not provided.
|
||||
|
||||
* `deleteTag(id)`
|
||||
|
||||
Deletes a tag.
|
||||
|
||||
* `id`: *string* The ID of the tag to delete.
|
||||
* Returns: A Promise that resolves with the data of the deleted tag.
|
||||
* Throws: Error if the ID is not provided.
|
21
docs/comty-js/models/payments.mdx
Normal file
21
docs/comty-js/models/payments.mdx
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
id: payments
|
||||
title: PaymentsModel
|
||||
sidebar_label: PaymentsModel
|
||||
---
|
||||
|
||||
## PaymentsModel
|
||||
|
||||
The `PaymentsModel` class provides static methods for interacting with payment information.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `PaymentsModel` class offers methods for retrieving payment data.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `fetchBalance()`
|
||||
|
||||
Fetches the current balance.
|
||||
|
||||
* Returns: A promise that resolves with the balance data received from the server.
|
114
docs/comty-js/models/post.mdx
Normal file
114
docs/comty-js/models/post.mdx
Normal file
@ -0,0 +1,114 @@
|
||||
---
|
||||
id: post
|
||||
title: Post
|
||||
sidebar_label: Post
|
||||
---
|
||||
|
||||
## Post
|
||||
|
||||
The `Post` class provides static methods for interacting with posts and related data on the Comty platform.
|
||||
|
||||
### API Reference
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `Post` class offers a comprehensive set of methods for retrieving, creating, updating, deleting, liking, saving, and voting on posts. It also provides functionalities for retrieving trending hashtags and posts.
|
||||
|
||||
### Static Properties
|
||||
|
||||
* `maxPostTextLength`: *number* The maximum length allowed for the post text (3200).
|
||||
* `maxCommentLength`: *number* The maximum length allowed for a comment (1200).
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `getPostingPolicy()`
|
||||
* Retrieves the posting policy from the server.
|
||||
* Returns: *Promise[object]* The posting policy data.
|
||||
|
||||
* `post(options)`
|
||||
* Retrieves the data of a post by its ID.
|
||||
* Parameters:
|
||||
* `options`: *object* An object containing the post_id.
|
||||
* `post_id`: *string* The ID of the post to retrieve.
|
||||
* Returns: *Promise[object]* The data of the post.
|
||||
* Throws: Error if the post_id is not provided.
|
||||
|
||||
* `replies(options)`
|
||||
* Retrieves the replies of a post by its ID.
|
||||
* Parameters:
|
||||
* `options`: *object* An object containing the post_id, page, and limit.
|
||||
* `post_id`: *string* The ID of the post to retrieve replies for.
|
||||
* `page`: *number, optional* The number of characters to page the reply content (default: 0).
|
||||
* `limit`: *number, optional* The maximum number of replies to fetch (default: Settings.get("feed_max_fetch")).
|
||||
* Returns: *Promise[object]* The data of the replies.
|
||||
* Throws: Error if the post_id is not provided.
|
||||
|
||||
* `getSavedPosts(options)`
|
||||
* Retrieves the saved posts with optional trimming and limiting.
|
||||
* Parameters:
|
||||
* `options`: *object* An object containing the page and limit.
|
||||
* `page`: *number, optional* The number of posts to page from the result (default: 0).
|
||||
* `limit`: *number, optional* The maximum number of posts to fetch (default: Settings.get("feed_max_fetch")).
|
||||
* Returns: *Promise[object]* The data of the liked posts.
|
||||
|
||||
* `getUserPosts(options)`
|
||||
* Retrieves the liked posts with optional trimming and limiting.
|
||||
* Parameters:
|
||||
* `options`: *object* An object containing the page and limit.
|
||||
* `page`: *number, optional* The number of posts to page from the result (default: 0).
|
||||
* `limit`: *number, optional* The maximum number of posts to fetch (default: Settings.get("feed_max_fetch")).
|
||||
* Returns: *Promise[object]* The data of the liked posts.
|
||||
|
||||
* `getUserPosts(options)`
|
||||
* Retrieves the posts of a user with optional trimming and limiting.
|
||||
* Parameters:
|
||||
* `options`: *object* An object containing the user_id, page, and limit.
|
||||
* `user_id`: *string, optional* The ID of the user whose posts to retrieve. If not provided, the current user's ID will be used.
|
||||
* `page`: *number, optional* The number of characters to page the post content (default: 0).
|
||||
* `limit`: *number, optional* The maximum number of posts to fetch (default: Settings.get("feed_max_fetch")).
|
||||
* Returns: *Promise[object]* The data of the user's posts.
|
||||
|
||||
* `toggleLike(options)`
|
||||
* Toggles the like status of a post.
|
||||
* Parameters:
|
||||
* `options`: *object* An object containing the post_id.
|
||||
* `post_id`: *string* The ID of the post to toggle the like status.
|
||||
* Returns: *Promise[object]* The response data after toggling the like status.
|
||||
* Throws: Error if the post_id is not provided.
|
||||
|
||||
* `toggleSave(options)`
|
||||
* Toggles the save status of a post.
|
||||
* Parameters:
|
||||
* `options`: *object* An object containing the post_id.
|
||||
* `post_id`: *string* The ID of the post to toggle the save status.
|
||||
* Returns: *Promise[object]* The response data after toggling the save status.
|
||||
* Throws: Error if the post_id is not provided.
|
||||
|
||||
* `create(payload)`
|
||||
* Creates a new post with the given payload.
|
||||
* Parameters:
|
||||
* `payload`: *object* The data to create the post with.
|
||||
* Returns: *Promise[object]* The response data after creating the post.
|
||||
|
||||
* `update(post_id, update)`
|
||||
* Updates a post with the given post ID and update payload.
|
||||
* Parameters:
|
||||
* `post_id`: *string* The ID of the post to update.
|
||||
* `update`: *object* The data to update the post with.
|
||||
* Returns: *Promise[object]* The response data after updating the post.
|
||||
* Throws: Error if the post_id is not provided.
|
||||
|
||||
* `delete(options)`
|
||||
* Deletes a post with the given post ID.
|
||||
* Parameters:
|
||||
* `options`: *object* An object containing the post_id.
|
||||
* `post_id`: *string* The ID of the post to delete.
|
||||
* Returns: *Promise[object]* The response data after deleting the post.
|
||||
* Throws: Error if the post_id is not provided.
|
||||
|
||||
* `votePoll(options)`
|
||||
* Votes for a poll with the given post ID and option ID.
|
||||
* Parameters:
|
||||
* `options`: *object* An object containing the post_id and option_id.
|
||||
* `post_id`: *string* The ID of the post to vote for.
|
||||
* `
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"label": "Post",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
---
|
||||
|
||||
# Create a post
|
||||
Creates a new post with the given payload.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function PostModel.create(payload)
|
||||
```
|
||||
|
||||
### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| message | String | false | undefined | The message of the post |
|
||||
| attachments | Array | true | [] | A list of attachments |
|
||||
| timestamp | String | true | DateTime.local().toISO() | |
|
||||
| reply_to | String | true | null | |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| data | Object | [post-object](/docs/comty-js/definitions/post-object) |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const post = await PostModel.create({
|
||||
message: "Testing Comty.JS",
|
||||
timestamp: new Date(),
|
||||
})
|
||||
|
||||
console.log(post)
|
||||
|
||||
// result: {
|
||||
// _id_: "0000",
|
||||
// message: "Testing Comty.JS",
|
||||
// timestamp: "2024-01-01T17:00:00.000Z",
|
||||
// }
|
||||
|
||||
```
|
||||
|
||||
### Add attachments
|
||||
```js
|
||||
const post = await PostModel.create({
|
||||
message: "Look at this fox",
|
||||
attachments: [
|
||||
{
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/3/30/Vulpes_vulpes_ssp_fulvus.jpg",
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
console.log(post)
|
||||
|
||||
// result: {
|
||||
// _id_: "0001",
|
||||
// message: "Look at this fox",
|
||||
// timestamp: "2024-01-01T17:00:00.000Z",
|
||||
// attachments: [
|
||||
// {
|
||||
// url: "https://upload.wikimedia.org/wikipedia/commons/3/30/Vulpes_vulpes_ssp_fulvus.jpg",
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
```
|
||||
|
||||
### Reply to a post
|
||||
```js
|
||||
const post = await PostModel.create({
|
||||
reply_to: "0001",
|
||||
message: "* pet pet *",
|
||||
})
|
||||
|
||||
console.log(post)
|
||||
|
||||
// result: {
|
||||
// _id_: "0002",
|
||||
// reply_to: "0001",
|
||||
// message: "* pat pat*",
|
||||
// timestamp: "2024-01-01T17:30:00.000Z",
|
||||
// }
|
||||
|
||||
```
|
@ -1,45 +0,0 @@
|
||||
---
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
# Delete a post
|
||||
Delete a post with the given post ID.
|
||||
|
||||
Only can delete your own posts.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function PostModel.delete(payload)
|
||||
```
|
||||
|
||||
### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| post_id | String | false | undefined | |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| post_id | String | |
|
||||
| deleted | Boolean | |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const post = await PostModel.delete({
|
||||
post_id: "0000",
|
||||
})
|
||||
|
||||
console.log(post)
|
||||
|
||||
// result: {
|
||||
// post_id: "0000",
|
||||
// deleted: true,
|
||||
// }
|
||||
|
||||
```
|
@ -1,45 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Get my liked posts
|
||||
Retrieves the liked posts of current authed user.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function PostModel.getLikedPosts(payload)
|
||||
```
|
||||
|
||||
### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| trim | number | true | 0 | Trim the post index content |
|
||||
| limit | number | true | 10 | Limit the number of posts to fetch |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| data | Array | [[post_obj](/docs/comty-js/definitions/post-object), ...] |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const posts = await PostModel.getLikedPosts({
|
||||
trim: 0,
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
console.log(posts)
|
||||
|
||||
// result: [
|
||||
// { _id: "0000", user_id: "0000", message: "example text", ... },
|
||||
// { _id: "0001", user_id: "0000", message: "example text", ... },
|
||||
// ...
|
||||
// ]
|
||||
|
||||
```
|
@ -1,44 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Get post data
|
||||
Retrieves the data of a post.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function PostModel.post(payload)
|
||||
```
|
||||
|
||||
### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| post_id | string | false | | Defines the ID of the post to retrieve.|
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| data | object | [post_obj](/docs/comty-js/definitions/post-object) |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const post = await PostModel.post({
|
||||
post_id: "0000",
|
||||
})
|
||||
|
||||
console.log(post)
|
||||
|
||||
// result: {
|
||||
// _id: "0000",
|
||||
// user_id: "0000",
|
||||
// message: "example text",
|
||||
// ...
|
||||
// }
|
||||
|
||||
```
|
@ -1,47 +0,0 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Get post replies
|
||||
Retrieves replies of a post.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function PostModel.replies(payload)
|
||||
```
|
||||
|
||||
### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| post_id | string | false | | |
|
||||
| trim | number | true | 0 | Trim the post index content |
|
||||
| limit | number | true | 10 | Limit the number of posts to fetch |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| data | Array | [[post_obj](/docs/comty-js/definitions/post-object), ...] |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const posts = await PostModel.replies({
|
||||
post_id: "0000",
|
||||
trim: 0,
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
console.log(posts)
|
||||
|
||||
// result: [
|
||||
// { _id: "0000", user_id: "0000", message: "example text", ... },
|
||||
// { _id: "0001", user_id: "0000", message: "example text", ... },
|
||||
// ...
|
||||
// ]
|
||||
|
||||
```
|
@ -1,45 +0,0 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Get my saved posts
|
||||
Retrieves the saved posts of current authed user.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function PostModel.getSavedPosts(payload)
|
||||
```
|
||||
|
||||
### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| trim | number | true | 0 | Trim the post index content |
|
||||
| limit | number | true | 10 | Limit the number of posts to fetch |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| data | Array | [[post_obj](/docs/comty-js/definitions/post-object), ...] |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const posts = await PostModel.getSavedPosts({
|
||||
trim: 0,
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
console.log(posts)
|
||||
|
||||
// result: [
|
||||
// { _id: "0000", user_id: "0000", message: "example text", ... },
|
||||
// { _id: "0001", user_id: "0000", message: "example text", ... },
|
||||
// ...
|
||||
// ]
|
||||
|
||||
```
|
@ -1,47 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Get user posts
|
||||
Retrieves the public posts of a user.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function PostModel.getUserPosts(payload)
|
||||
```
|
||||
|
||||
### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| user_id | string | false | | |
|
||||
| trim | number | true | 0 | Trim the post index content |
|
||||
| limit | number | true | 10 | Limit the number of posts to fetch |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| data | Array | [[post_obj](/docs/comty-js/definitions/post-object), ...] |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const posts = await PostModel.getUserPosts({
|
||||
user_id: "0000",
|
||||
trim: 0,
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
console.log(posts)
|
||||
|
||||
// result: [
|
||||
// { _id: "0000", user_id: "0000", message: "example text", ... },
|
||||
// { _id: "0001", user_id: "0000", message: "example text", ... },
|
||||
// ...
|
||||
// ]
|
||||
|
||||
```
|
@ -1,60 +0,0 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Toggle post like
|
||||
Toggles the like status of a post.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function PostModel.toggleLike(payload)
|
||||
```
|
||||
|
||||
### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| post_id | string | false | | |
|
||||
| to | Boolean | true | | Set like to true or false |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| data | Object | [like-status-object](/docs/comty-js/definitions/like-status-object) |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const like = await PostModel.toggleLike({
|
||||
post_id: "0000",
|
||||
})
|
||||
|
||||
console.log(like)
|
||||
|
||||
// result: {
|
||||
// post_id: "0000",
|
||||
// liked: true,
|
||||
// count: 1,
|
||||
// }
|
||||
|
||||
```
|
||||
### Specify status
|
||||
```js
|
||||
const like = await PostModel.toggleLike({
|
||||
post_id: "0000",
|
||||
to: false
|
||||
})
|
||||
|
||||
console.log(like)
|
||||
|
||||
// result: {
|
||||
// post_id: "0000",
|
||||
// liked: false,
|
||||
// count: 0,
|
||||
// }
|
||||
|
||||
```
|
@ -1,60 +0,0 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
# Toggle post save
|
||||
Toggles the save status of a post.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function PostModel.toggleSave(payload)
|
||||
```
|
||||
|
||||
### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| post_id | string | false | | |
|
||||
| to | Boolean | true | | Set save to true or false |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| data | Object | [save-status-object](/docs/comty-js/definitions/save-status-object) |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const save = await PostModel.toggleSave({
|
||||
post_id: "0000",
|
||||
})
|
||||
|
||||
console.log(save)
|
||||
|
||||
// result: {
|
||||
// post_id: "0000",
|
||||
// saved: true,
|
||||
// count: 1,
|
||||
// }
|
||||
|
||||
```
|
||||
### Specify status
|
||||
```js
|
||||
const save = await PostModel.toggleSave({
|
||||
post_id: "0000",
|
||||
to: false
|
||||
})
|
||||
|
||||
console.log(save)
|
||||
|
||||
// result: {
|
||||
// post_id: "0000",
|
||||
// saved: false,
|
||||
// count: 0,
|
||||
// }
|
||||
|
||||
```
|
@ -1,76 +0,0 @@
|
||||
---
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
# Update a post
|
||||
Updates a post with the given post ID and update payload.
|
||||
|
||||
Only can update your own posts.
|
||||
|
||||
<div class="divider"/>
|
||||
<br />
|
||||
```js
|
||||
async function PostModel.update(post_id, payload)
|
||||
```
|
||||
|
||||
### [String] post_id
|
||||
Defines the ID of the post to update.
|
||||
|
||||
### [Object] Payload
|
||||
| Parameter | Type | Optional | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| message | String | false | undefined | The message of the post |
|
||||
| attachments | Array | true | [] | A list of attachments |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
### Success Response
|
||||
| Parameter | Type | Content |
|
||||
| --- | --- | --- |
|
||||
| data | Object | [post-object](/docs/comty-js/definitions/post-object) |
|
||||
|
||||
<div class="divider"/>
|
||||
|
||||
## Examples
|
||||
### Basic usage
|
||||
```js
|
||||
const post = await PostModel.update({
|
||||
post_id: "0000",
|
||||
message: "Updated message",
|
||||
})
|
||||
|
||||
console.log(post)
|
||||
|
||||
// result: {
|
||||
// _id_: "0000",
|
||||
// message: "Updated message",
|
||||
// timestamp: "2024-01-01T17:00:00.000Z",
|
||||
// }
|
||||
|
||||
```
|
||||
|
||||
### Modify or remove attachments
|
||||
```js
|
||||
const post = await PostModel.update({
|
||||
post_id: "0000",
|
||||
attachments: [
|
||||
{
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/3/30/Vulpes_vulpes_ssp_fulvus.jpg",
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
console.log(post)
|
||||
|
||||
// result: {
|
||||
// _id_: "0000",
|
||||
// message: "Updated message",
|
||||
// timestamp: "2024-01-01T17:00:00.000Z",
|
||||
// attachments: [
|
||||
// {
|
||||
// url: "https://upload.wikimedia.org/wikipedia/commons/3/30/Vulpes_vulpes_ssp_fulvus.jpg",
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
```
|
30
docs/comty-js/models/radio.mdx
Normal file
30
docs/comty-js/models/radio.mdx
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
id: radio
|
||||
title: Radio
|
||||
sidebar_label: Radio
|
||||
---
|
||||
|
||||
## Radio
|
||||
|
||||
The `Radio` class provides static methods for retrieving radio data.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `Radio` class offers methods for retrieving radio lists and trending radio stations.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `getRadioList({ limit = 50, offset = 0 } = {})`
|
||||
|
||||
Retrieves a list of radio stations.
|
||||
|
||||
* `{ limit, offset }`: *object, optional* An object containing the limit and offset for pagination.
|
||||
* `limit`: *number, optional* The maximum number of radio stations to retrieve (default: 50).
|
||||
* `offset`: *number, optional* The offset to start retrieving from (default: 0).
|
||||
* Returns: A promise that resolves with the radio list data.
|
||||
|
||||
* `getTrendings()`
|
||||
|
||||
Retrieves trending radio stations.
|
||||
|
||||
* Returns: A promise that resolves with the trending radio stations data.
|
@ -1 +0,0 @@
|
||||
# Search
|
32
docs/comty-js/models/search.mdx
Normal file
32
docs/comty-js/models/search.mdx
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
id: search
|
||||
title: Search
|
||||
sidebar_label: Search
|
||||
---
|
||||
|
||||
## Search
|
||||
|
||||
The `Search` class provides a static method for performing searches using the Comty API.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `Search` class encapsulates the search functionality, allowing you to search for content using keywords and optional parameters. It also supports external addons to extend the search functionality.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `search(keywords, params, returnFields)`
|
||||
|
||||
Performs a search using the provided keywords and optional parameters.
|
||||
|
||||
* `keywords`: *string* The keywords to search for.
|
||||
* `params`: *object, optional* Optional parameters for the search.
|
||||
* `limit`: *number, optional* The maximum number of results to return default: 50.
|
||||
* `offset`: *number, optional* The offset to start the search from default: 0.
|
||||
* `sort`: *string, optional* The sort order "asc" or "desc" default: "desc".
|
||||
* `fields`: *array, optional* An array of fields to return in the results. If empty, all fields will be returned.
|
||||
* `returnFields`: *array, optional* An array of fields to return in the results. If empty, all fields will be returned.
|
||||
* Returns: A promise that resolves with the search results.
|
||||
|
||||
### API Reference
|
||||
|
||||
* `search(keywords: string, params: object, returnFields: array)`: Promise[object] - Performs a search using the provided keywords and optional parameters.
|
@ -1 +0,0 @@
|
||||
# Session
|
97
docs/comty-js/models/spectrum.mdx
Normal file
97
docs/comty-js/models/spectrum.mdx
Normal file
@ -0,0 +1,97 @@
|
||||
---
|
||||
id: spectrum
|
||||
title: Streaming
|
||||
sidebar_label: Streaming
|
||||
---
|
||||
|
||||
## Streaming
|
||||
|
||||
The `Streaming` class provides static methods for interacting with streaming data on the Comty platform.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `Streaming` class offers methods for managing streaming profiles, streams, and websocket connections.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `getStream(stream_id)`
|
||||
|
||||
Retrieves data for a specific stream.
|
||||
|
||||
* `stream_id`: *string* The ID of the stream.
|
||||
* Returns: A promise that resolves with the stream data.
|
||||
|
||||
* `getOwnProfiles()`
|
||||
|
||||
Retrieves the streaming profiles owned by the current user.
|
||||
|
||||
* Returns: A promise that resolves with the profiles data.
|
||||
|
||||
* `getProfile(profile_id)`
|
||||
|
||||
Retrieves data for a specific streaming profile.
|
||||
|
||||
* `profile_id`: *string* The ID of the profile.
|
||||
* Returns: A promise that resolves with the profile data.
|
||||
|
||||
* `createProfile(payload)`
|
||||
|
||||
Creates a new streaming profile.
|
||||
|
||||
* `payload`: *object* The data for the new profile.
|
||||
* Returns: A promise that resolves with the created profile data.
|
||||
|
||||
* `updateProfile(profile_id, update)`
|
||||
|
||||
Updates a streaming profile.
|
||||
|
||||
* `profile_id`: *string* The ID of the profile to update.
|
||||
* `update`: *object* The data to update the profile with.
|
||||
* Returns: A promise that resolves with the updated profile data.
|
||||
|
||||
* `deleteProfile(profile_id)`
|
||||
|
||||
Deletes a streaming profile.
|
||||
|
||||
* `profile_id`: *string* The ID of the profile to delete.
|
||||
* Returns: A promise that resolves with the response data after deleting the profile.
|
||||
|
||||
* `addRestreamToProfile(profileId, restreamData)`
|
||||
|
||||
Adds a restream to a profile.
|
||||
|
||||
* `profileId`: *string* The ID of the profile to add the restream to.
|
||||
* `restreamData`: *object* The data for the restream.
|
||||
* Returns: A promise that resolves with the response data.
|
||||
|
||||
* `deleteRestreamFromProfile(profileId, restreamIndexData)`
|
||||
|
||||
Deletes a restream from a profile.
|
||||
|
||||
* `profileId`: *string* The ID of the profile to delete the restream from.
|
||||
* `restreamIndexData`: *object* The index data for the restream to delete.
|
||||
* Returns: A promise that resolves with the response data.
|
||||
|
||||
* `list({ limit, offset } = {})`
|
||||
|
||||
Lists streaming entries.
|
||||
|
||||
* `{ limit, offset }`: *object, optional* An object containing pagination parameters.
|
||||
* `limit`: *number, optional* The maximum number of items to retrieve.
|
||||
* `offset`: *number, optional* The offset to start retrieving from.
|
||||
* Returns: A promise that resolves with the streaming entries data.
|
||||
|
||||
* `createWebsocket(params = {})`
|
||||
|
||||
Creates a websocket connection.
|
||||
|
||||
* `params`: *object, optional* Additional parameters for the websocket connection.
|
||||
* Returns: A `RTEngineClient` websocket client.
|
||||
|
||||
* `createStreamWebsocket(stream_id, params = {})`
|
||||
|
||||
Creates a stream-specific websocket connection.
|
||||
|
||||
* `stream_id`: *string* The ID of the stream.
|
||||
* `params`: *object, optional* Additional parameters for the websocket connection.
|
||||
* Returns: A `RTEngineClient` websocket client.
|
63
docs/comty-js/models/spotify.mdx
Normal file
63
docs/comty-js/models/spotify.mdx
Normal file
@ -0,0 +1,63 @@
|
||||
---
|
||||
id: spotify
|
||||
title: SpotifySyncModel
|
||||
sidebar_label: SpotifySyncModel
|
||||
---
|
||||
|
||||
## SpotifySyncModel
|
||||
|
||||
The `SpotifySyncModel` class provides static methods for linking and interacting with the Spotify service.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `SpotifySyncModel` class offers methods for authorizing, linking, unlinking, and retrieving data from the Spotify service.
|
||||
|
||||
### Static Properties
|
||||
|
||||
* `spotify_redirect_uri`: *string* The redirect URI for Spotify authorization.
|
||||
* `spotify_authorize_endpoint`: *string* The Spotify authorization endpoint.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `authorizeAccount()`
|
||||
|
||||
Opens a new tab to authorize the user's Spotify account.
|
||||
|
||||
* Returns: void
|
||||
|
||||
* `get_client_id()`
|
||||
|
||||
Retrieves the Spotify client ID.
|
||||
|
||||
* Returns: A Promise that resolves with the client ID data.
|
||||
|
||||
* `syncAuthCode(code)`
|
||||
|
||||
Syncs the Spotify authorization code.
|
||||
|
||||
* `code`: *string* The Spotify authorization code.
|
||||
* Returns: A Promise that resolves with the sync data.
|
||||
|
||||
* `unlinkAccount()`
|
||||
|
||||
Unlinks the user's Spotify account.
|
||||
|
||||
* Returns: A Promise that resolves with the unlink data.
|
||||
|
||||
* `isAuthorized()`
|
||||
|
||||
Checks if the user is authorized with Spotify.
|
||||
|
||||
* Returns: A Promise that resolves with a boolean indicating whether the user is authorized.
|
||||
|
||||
* `getData()`
|
||||
|
||||
Retrieves Spotify data.
|
||||
|
||||
* Returns: A Promise that resolves with the Spotify data.
|
||||
|
||||
* `getCurrentPlaying()`
|
||||
|
||||
Retrieves the currently playing track from Spotify.
|
||||
|
||||
* Returns: A Promise that resolves with the currently playing track data.
|
108
docs/comty-js/models/tidal.mdx
Normal file
108
docs/comty-js/models/tidal.mdx
Normal file
@ -0,0 +1,108 @@
|
||||
---
|
||||
id: tidal
|
||||
title: TidalService
|
||||
sidebar_label: TidalService
|
||||
---
|
||||
|
||||
## TidalService
|
||||
|
||||
The `TidalService` class provides static methods for linking and interacting with the Tidal service.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `TidalService` class offers methods for linking and unlinking accounts, checking the connection status, and retrieving data from the Tidal service.
|
||||
|
||||
### Static Properties
|
||||
|
||||
* `api_instance`: Returns the API instance for the Tidal service.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `linkAccount()`
|
||||
|
||||
Opens a new tab to link the user's Tidal account.
|
||||
|
||||
* Returns: A Promise that resolves with the link data.
|
||||
* Throws: Error if not running in a browser environment
|
||||
|
||||
* `unlinkAccount()`
|
||||
|
||||
Unlinks the user's Tidal account.
|
||||
|
||||
* Returns: A Promise that resolves with the unlink data.
|
||||
* Throws: Error if not running in a browser environment
|
||||
|
||||
* `isActive()`
|
||||
|
||||
Checks if the user's Tidal account is linked.
|
||||
|
||||
* Returns: A Promise that resolves with a boolean indicating whether the account is linked.
|
||||
* Throws: Error if not running in a browser environment
|
||||
|
||||
* `getCurrentUser()`
|
||||
|
||||
Retrieves the current Tidal user.
|
||||
|
||||
* Returns: A Promise that resolves with the current user data.
|
||||
|
||||
* `getPlaybackUrl(track_id)`
|
||||
|
||||
Retrieves the playback URL for a given Tidal track ID.
|
||||
|
||||
* `track_id`: *string* The ID of the Tidal track.
|
||||
* Returns: A Promise that resolves with the playback URL data.
|
||||
|
||||
* `getTrackManifest(track_id)`
|
||||
|
||||
Retrieves the track manifest for a given Tidal track ID.
|
||||
|
||||
* `track_id`: *string* The ID of the Tidal track.
|
||||
* Returns: A Promise that resolves with the track manifest data.
|
||||
|
||||
* `getMyFavoriteTracks({ limit = 50, offset = 0 } = {})`
|
||||
|
||||
Retrieves the user's favorite Tidal tracks.
|
||||
|
||||
* `{ limit, offset }`: *object, optional* An object containing pagination parameters.
|
||||
* `limit`: *number* The maximum number of tracks to retrieve.
|
||||
* `offset`: *number* The offset to start retrieving from.
|
||||
* Returns: A Promise that resolves with the favorite tracks data.
|
||||
|
||||
* `getMyFavoritePlaylists({ limit = 50, offset = 0 } = {})`
|
||||
|
||||
Retrieves the user's favorite Tidal playlists.
|
||||
|
||||
* `{ limit, offset }`: *object, optional* An object containing pagination parameters.
|
||||
* `limit`: *number* The maximum number of playlists to retrieve.
|
||||
* `offset`: *number* The offset to start retrieving from.
|
||||
* Returns: A Promise that resolves with the favorite playlists data.
|
||||
|
||||
* `getPlaylistData({ playlist_id, resolve_items = false, limit = 50, offset = 0 })`
|
||||
|
||||
Retrieves Tidal playlist data.
|
||||
|
||||
* `playlist_id`: *string* The ID of the Tidal playlist.
|
||||
* `{ resolve_items, limit, offset }`: *object, optional* An object containing playlist options.
|
||||
* `resolve_items`: *boolean* Whether to resolve playlist items.
|
||||
* `limit`: *number* The maximum number of items to retrieve.
|
||||
* `offset`: *number* The offset to start retrieving from.
|
||||
* Returns: A Promise that resolves with the playlist data.
|
||||
|
||||
* `getPlaylistItems({ playlist_id, resolve_items = false, limit = 50, offset = 0 })`
|
||||
|
||||
Retrieves Tidal playlist items.
|
||||
|
||||
* `playlist_id`: *string* The ID of the Tidal playlist.
|
||||
* `{ resolve_items, limit, offset }`: *object, optional* An object containing playlist options.
|
||||
* `resolve_items`: *boolean* Whether to resolve playlist items.
|
||||
* `limit`: *number* The maximum number of items to retrieve.
|
||||
* `offset`: *number* The offset to start retrieving from.
|
||||
* Returns: A Promise that resolves with the playlist items data.
|
||||
|
||||
* `toggleTrackLike({ track_id, to })`
|
||||
|
||||
Toggles a Tidal track like.
|
||||
|
||||
* `track_id`: *string* The ID of the Tidal track.
|
||||
* `to`: *boolean* Whether to like or unlike the track.
|
||||
* Returns: A Promise that resolves with the response data.
|
@ -1 +0,0 @@
|
||||
# User
|
80
docs/comty-js/models/user.mdx
Normal file
80
docs/comty-js/models/user.mdx
Normal file
@ -0,0 +1,80 @@
|
||||
---
|
||||
id: user
|
||||
title: UserModel
|
||||
sidebar_label: UserModel
|
||||
---
|
||||
|
||||
## UserModel
|
||||
|
||||
The `UserModel` class provides static methods for interacting with user data on the Comty platform.
|
||||
|
||||
**Class Overview:**
|
||||
|
||||
The `UserModel` class offers a set of methods for retrieving and updating user data, including profile information, roles, badges, and configuration settings.
|
||||
|
||||
### Static Methods
|
||||
|
||||
* `data(payload)`
|
||||
|
||||
Retrieves the data of a user.
|
||||
|
||||
* `payload`: *object, optional* An object containing the username and user_id.
|
||||
* `username`: *string, optional* The username of the user.
|
||||
* `user_id`: *string, optional* The ID of the user.
|
||||
* `basic`: *boolean, optional* Whether to fetch only basic user information default: false.
|
||||
* Returns: A promise that resolves with the data of the user.
|
||||
|
||||
* `updateData(payload)`
|
||||
|
||||
Updates the user data with the given payload.
|
||||
|
||||
* `payload`: *object* The data to update the user with.
|
||||
* Returns: A promise that resolves with the updated user data.
|
||||
|
||||
* `unsetPublicName()`
|
||||
|
||||
Update the public name to null in the user data.
|
||||
|
||||
* Returns: A Promise that resolves with the response data after updating the public name
|
||||
|
||||
* `getRoles(user_id)`
|
||||
|
||||
Retrieves the roles of a user.
|
||||
|
||||
* `user_id`: *string, optional* The ID of the user. If not provided, the current user ID will be used.
|
||||
* Returns: A promise that resolves with an array of roles for the user.
|
||||
|
||||
* `getBadges(user_id)`
|
||||
|
||||
Retrieves the badges for a given user.
|
||||
|
||||
* `user_id`: *string, optional* The ID of the user. If not provided, the current session user ID will be used.
|
||||
* Returns: A promise that resolves with an array of badges for the user.
|
||||
|
||||
* `getConfig(key)`
|
||||
|
||||
Retrive user config from server
|
||||
|
||||
* `key`: *string* A key of config
|
||||
* Returns: A Promise that resolves with a config object
|
||||
|
||||
* `updateConfig(update)`
|
||||
|
||||
Update the configuration with the given update.
|
||||
|
||||
* `update`: *Object* The object containing the updated configuration data
|
||||
* Returns: A Promise that resolves with the response data after the configuration is updated
|
||||
|
||||
* `getPublicKey(user_id)`
|
||||
|
||||
Retrieves the public key for a given user.
|
||||
|
||||
* `user_id`: *string, optional* The ID of the user. If not provided, the current session user ID will be used.
|
||||
* Returns: A promise that resolves with the public key for the user.
|
||||
|
||||
* `updatePublicKey(public_key)`
|
||||
|
||||
Updates the public key for the current user.
|
||||
|
||||
* `public_key`: *string* The new public key to set.
|
||||
* Returns: A promise that resolves with the response data after updating the public key.
|
@ -37,17 +37,34 @@ const config = {
|
||||
presets: [
|
||||
[
|
||||
"classic",
|
||||
/** @type {import("@docusaurus/preset-classic").Options} */
|
||||
({
|
||||
{
|
||||
docs: {
|
||||
path: "../docs",
|
||||
sidebarPath: "./sidebars.js",
|
||||
//editUrl: "https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/",
|
||||
},
|
||||
theme: {
|
||||
customCss: "./src/css/custom.css",
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
plugins: [
|
||||
[
|
||||
"docusaurus-plugin-openapi-docs",
|
||||
{
|
||||
id: "api", // plugin id
|
||||
docsPluginId: "classic", // configured for preset-classic
|
||||
config: {
|
||||
petstore: {
|
||||
specPath: "examples/petstore.yaml",
|
||||
outputDir: "docs/petstore",
|
||||
sidebarOptions: {
|
||||
groupPathsBy: "tag",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
|
@ -18,6 +18,7 @@
|
||||
"@docusaurus/preset-classic": "3.5.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"docusaurus-plugin-openapi-docs": "^4.4.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comty/app",
|
||||
"version": "1.42.1@alpha",
|
||||
"version": "1.42.2@alpha",
|
||||
"license": "ComtyLicense",
|
||||
"main": "electron/main",
|
||||
"type": "module",
|
||||
@ -34,9 +34,8 @@
|
||||
"axios": "^1.7.7",
|
||||
"bear-react-carousel": "^4.0.10-alpha.0",
|
||||
"classnames": "2.3.1",
|
||||
"comty.js": "^0.66.1",
|
||||
"comty.js": "^0.67.0",
|
||||
"d3": "^7.9.0",
|
||||
"dashjs": "^5.0.0",
|
||||
"dompurify": "^3.0.0",
|
||||
"fast-average-color": "^9.2.0",
|
||||
"fuse.js": "6.5.3",
|
||||
@ -50,7 +49,6 @@
|
||||
"mime": "^3.0.0",
|
||||
"moment": "2.29.4",
|
||||
"motion": "^12.4.2",
|
||||
"mpegts.js": "^1.6.10",
|
||||
"music-metadata": "^11.2.1",
|
||||
"plyr": "^3.7.8",
|
||||
"prop-types": "^15.8.1",
|
||||
@ -73,6 +71,7 @@
|
||||
"react-useanimations": "^2.10.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rxjs": "^7.5.5",
|
||||
"shaka-player": "^4.14.12",
|
||||
"store": "^2.0.12",
|
||||
"swapy": "^1.0.5",
|
||||
"ua-parser-js": "^1.0.36",
|
||||
|
@ -10,39 +10,17 @@ export default class QueueManager {
|
||||
|
||||
currentItem = null
|
||||
|
||||
next = ({ random = false } = {}) => {
|
||||
if (this.nextItems.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.currentItem) {
|
||||
this.prevItems.push(this.currentItem)
|
||||
}
|
||||
|
||||
if (random) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * this.nextItems.length,
|
||||
)
|
||||
|
||||
this.currentItem = this.nextItems.splice(randomIndex, 1)[0]
|
||||
} else {
|
||||
this.currentItem = this.nextItems.shift()
|
||||
}
|
||||
|
||||
return this.currentItem
|
||||
}
|
||||
|
||||
set = (item) => {
|
||||
setCurrent = (item) => {
|
||||
if (typeof item === "number") {
|
||||
item = this.nextItems[item]
|
||||
}
|
||||
|
||||
if (this.currentItem && this.currentItem.id === item.id) {
|
||||
if (this.currentItem && this.currentItem._id === item._id) {
|
||||
return this.currentItem
|
||||
}
|
||||
|
||||
const itemInNext = this.nextItems.findIndex((i) => i.id === item.id)
|
||||
const itemInPrev = this.prevItems.findIndex((i) => i.id === item.id)
|
||||
const itemInNext = this.nextItems.findIndex((i) => i._id === item._id)
|
||||
const itemInPrev = this.prevItems.findIndex((i) => i._id === item._id)
|
||||
|
||||
if (itemInNext === -1 && itemInPrev === -1) {
|
||||
throw new Error("Item not found in the queue")
|
||||
@ -71,6 +49,28 @@ export default class QueueManager {
|
||||
return this.currentItem
|
||||
}
|
||||
|
||||
next = ({ random = false } = {}) => {
|
||||
if (this.nextItems.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.currentItem) {
|
||||
this.prevItems.push(this.currentItem)
|
||||
}
|
||||
|
||||
if (random) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * this.nextItems.length,
|
||||
)
|
||||
|
||||
this.currentItem = this.nextItems.splice(randomIndex, 1)[0]
|
||||
} else {
|
||||
this.currentItem = this.nextItems.shift()
|
||||
}
|
||||
|
||||
return this.currentItem
|
||||
}
|
||||
|
||||
previous = () => {
|
||||
if (this.prevItems.length === 0) {
|
||||
return this.currentItem
|
||||
@ -117,12 +117,4 @@ export default class QueueManager {
|
||||
this.prevItems = []
|
||||
this.currentItem = null
|
||||
}
|
||||
|
||||
async load(item) {
|
||||
if (typeof this.params.loadFunction === "function") {
|
||||
return await this.params.loadFunction(item)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,18 @@
|
||||
export default (props) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="14.0" height="9.0" viewBox="0 0 14.0 9.0"><path fill="#ffffffff" d="M4.5522,0C4.8969,0 5.205,0.1229 5.4844,0.3361C5.5808,0.4095 5.6939,0.5149 5.7751,0.6012C7.2178,2.136 7.8307,6.1801 8.804,7.8187C8.8851,7.9552 8.9397,8.0258 8.9618,8.0559C9.1839,8.3565 9.4261,8.535 9.7017,8.535C9.7951,8.535 9.8926,8.513 9.9941,8.4706C10.091,8.5776 10.1918,8.6744 10.2966,8.7588C10.0427,8.9239 9.7355,9 9.4478,9C9.1031,9 8.795,8.8771 8.5156,8.664C8.4374,8.6043 8.3179,8.4978 8.2249,8.3989C6.7822,6.864 6.1693,2.8199 5.1959,1.1813C4.9026,0.6879 4.5245,0.3133 4.0059,0.5294C3.9089,0.4222 3.8082,0.3258 3.7033,0.2411C3.9572,0.0761 4.2643,0 4.5522,0ZM2.3869,0.0214C5.5605,-0.4229 5.8126,8.535 7.7911,8.535C7.884,8.535 7.981,8.5132 8.0818,8.4714C8.1792,8.5788 8.2807,8.6743 8.386,8.7589C8.132,8.9238 7.8249,9 7.537,9C5.8045,9 4.9435,5.8448 4.2734,3.724C4.229,3.5835 4.1847,3.4448 4.1405,3.3079C3.8674,2.4631 3.5948,1.7027 3.2851,1.1814C3.2223,1.0754 3.1578,0.9796 3.0916,0.8947C2.7131,0.4102 2.305,0.329 1.8286,0.6833C0.8287,1.4214 0.3651,3.8953 0.2183,5.1524L0.2183,5.1524C0.2107,5.2178 0.1637,5.2595 0.1092,5.2595C0.1048,5.2595 0.1005,5.2593 0.0961,5.2587C0.0362,5.2514 -0.0064,5.1959 0.0008,5.1346L0.0008,5.1346C0.2175,3.7154 0.5767,1.2827 1.6123,0.3783C1.8147,0.1972 2.0896,0.0629 2.3869,0.0214ZM6.4629,0C8.1955,0 9.0565,3.1564 9.7265,5.276C9.771,5.4165 9.8153,5.5553 9.8595,5.692C10.1326,6.537 10.4052,7.2972 10.7148,7.8186C10.7777,7.9245 10.8422,8.0205 10.9085,8.1053C11.1207,8.3769 11.3516,8.535 11.6124,8.535C11.7848,8.535 11.9702,8.4663 12.1713,8.3167C13.1712,7.5786 13.6348,5.1048 13.7816,3.8477L13.7816,3.8477C13.7893,3.7822 13.8364,3.7406 13.8908,3.7406C13.8952,3.7406 13.8996,3.7408 13.9039,3.7414C13.9637,3.7486 14.0063,3.8042 13.9992,3.8653L13.9992,3.8653C13.7825,5.2845 13.4234,7.7172 12.3876,8.6217C12.1016,8.8779 11.7247,9 11.3584,9C8.4607,9 8.1211,0.465 6.2089,0.465C6.116,0.465 6.0191,0.4868 5.9182,0.5286C5.8209,0.4213 5.7194,0.3257 5.614,0.2412C5.868,0.0761 6.1751,0 6.4629,0ZM11.9867,3.7407C12.0531,3.7487 12.0957,3.8041 12.0885,3.8654L12.0885,3.8654C11.9128,5.0167 11.6434,6.8324 10.9962,7.9608C10.9326,7.8744 10.8704,7.7766 10.8094,7.6686C11.437,6.6147 11.7543,4.8471 11.8711,3.8477L11.8711,3.8477C11.8787,3.7822 11.9257,3.7405 11.9801,3.7405C11.9845,3.7405 11.9889,3.7408 11.9932,3.7413ZM9.1606,7.1514C9.2138,7.2632 9.2681,7.3719 9.3239,7.4773C9.2504,7.6486 9.1707,7.8106 9.0847,7.9608C9.0215,7.8746 8.9594,7.7769 8.8985,7.6691C8.9927,7.5108 9.0801,7.3368 9.1606,7.1514ZM10.076,3.7407C10.1424,3.7487 10.1851,3.8041 10.1779,3.8654L10.1779,3.8654C10.1111,4.303 10.0313,4.8324 9.9229,5.3838C9.8782,5.2436 9.8336,5.1029 9.7893,4.9621C9.8681,4.5458 9.9239,4.1593 9.9604,3.8477L9.9604,3.8477C9.968,3.7822 10.015,3.7405 10.0695,3.7405C10.0739,3.7405 10.0782,3.7408 10.0826,3.7413ZM3.0038,1.0393C3.0674,1.1256 3.1296,1.2234 3.1906,1.3314C2.563,2.3853 2.2457,4.153 2.1289,5.1523L2.1289,5.1523C2.1213,5.2178 2.0743,5.2595 2.0199,5.2595C2.0155,5.2595 2.0111,5.2592 2.0068,5.2587C1.9469,5.2513 1.9042,5.1958 1.9114,5.1346L1.9114,5.1346C2.0872,3.9834 2.3565,2.1676 3.0038,1.0393ZM4.0771,3.6161C4.1218,3.7564 4.1663,3.8971 4.2107,4.038C4.1319,4.4542 4.076,4.8406 4.0396,5.1523L4.0396,5.1523C4.032,5.2178 3.985,5.2595 3.9305,5.2595C3.9261,5.2595 3.9218,5.2592 3.9174,5.2587C3.8575,5.2513 3.8149,5.1958 3.8221,5.1346L3.8221,5.1346C3.8889,4.697 3.9687,4.1676 4.0771,3.6161ZM4.9153,1.0392C4.9787,1.1253 5.0406,1.2231 5.1014,1.3309C5.0072,1.489 4.92,1.6632 4.8394,1.8486C4.7862,1.7368 4.7319,1.6281 4.6762,1.5227C4.7495,1.3514 4.8293,1.1895 4.9153,1.0392Z" stroke="#00000010" stroke-width="1.0" fill-rule="evenodd" id="path_0" /></svg>
|
||||
export default (props) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14.0"
|
||||
height="9.0"
|
||||
viewBox="0 0 14.0 9.0"
|
||||
>
|
||||
<path
|
||||
fill="#ffffffff"
|
||||
d="M4.5522,0C4.8969,0 5.205,0.1229 5.4844,0.3361C5.5808,0.4095 5.6939,0.5149 5.7751,0.6012C7.2178,2.136 7.8307,6.1801 8.804,7.8187C8.8851,7.9552 8.9397,8.0258 8.9618,8.0559C9.1839,8.3565 9.4261,8.535 9.7017,8.535C9.7951,8.535 9.8926,8.513 9.9941,8.4706C10.091,8.5776 10.1918,8.6744 10.2966,8.7588C10.0427,8.9239 9.7355,9 9.4478,9C9.1031,9 8.795,8.8771 8.5156,8.664C8.4374,8.6043 8.3179,8.4978 8.2249,8.3989C6.7822,6.864 6.1693,2.8199 5.1959,1.1813C4.9026,0.6879 4.5245,0.3133 4.0059,0.5294C3.9089,0.4222 3.8082,0.3258 3.7033,0.2411C3.9572,0.0761 4.2643,0 4.5522,0ZM2.3869,0.0214C5.5605,-0.4229 5.8126,8.535 7.7911,8.535C7.884,8.535 7.981,8.5132 8.0818,8.4714C8.1792,8.5788 8.2807,8.6743 8.386,8.7589C8.132,8.9238 7.8249,9 7.537,9C5.8045,9 4.9435,5.8448 4.2734,3.724C4.229,3.5835 4.1847,3.4448 4.1405,3.3079C3.8674,2.4631 3.5948,1.7027 3.2851,1.1814C3.2223,1.0754 3.1578,0.9796 3.0916,0.8947C2.7131,0.4102 2.305,0.329 1.8286,0.6833C0.8287,1.4214 0.3651,3.8953 0.2183,5.1524L0.2183,5.1524C0.2107,5.2178 0.1637,5.2595 0.1092,5.2595C0.1048,5.2595 0.1005,5.2593 0.0961,5.2587C0.0362,5.2514 -0.0064,5.1959 0.0008,5.1346L0.0008,5.1346C0.2175,3.7154 0.5767,1.2827 1.6123,0.3783C1.8147,0.1972 2.0896,0.0629 2.3869,0.0214ZM6.4629,0C8.1955,0 9.0565,3.1564 9.7265,5.276C9.771,5.4165 9.8153,5.5553 9.8595,5.692C10.1326,6.537 10.4052,7.2972 10.7148,7.8186C10.7777,7.9245 10.8422,8.0205 10.9085,8.1053C11.1207,8.3769 11.3516,8.535 11.6124,8.535C11.7848,8.535 11.9702,8.4663 12.1713,8.3167C13.1712,7.5786 13.6348,5.1048 13.7816,3.8477L13.7816,3.8477C13.7893,3.7822 13.8364,3.7406 13.8908,3.7406C13.8952,3.7406 13.8996,3.7408 13.9039,3.7414C13.9637,3.7486 14.0063,3.8042 13.9992,3.8653L13.9992,3.8653C13.7825,5.2845 13.4234,7.7172 12.3876,8.6217C12.1016,8.8779 11.7247,9 11.3584,9C8.4607,9 8.1211,0.465 6.2089,0.465C6.116,0.465 6.0191,0.4868 5.9182,0.5286C5.8209,0.4213 5.7194,0.3257 5.614,0.2412C5.868,0.0761 6.1751,0 6.4629,0ZM11.9867,3.7407C12.0531,3.7487 12.0957,3.8041 12.0885,3.8654L12.0885,3.8654C11.9128,5.0167 11.6434,6.8324 10.9962,7.9608C10.9326,7.8744 10.8704,7.7766 10.8094,7.6686C11.437,6.6147 11.7543,4.8471 11.8711,3.8477L11.8711,3.8477C11.8787,3.7822 11.9257,3.7405 11.9801,3.7405C11.9845,3.7405 11.9889,3.7408 11.9932,3.7413ZM9.1606,7.1514C9.2138,7.2632 9.2681,7.3719 9.3239,7.4773C9.2504,7.6486 9.1707,7.8106 9.0847,7.9608C9.0215,7.8746 8.9594,7.7769 8.8985,7.6691C8.9927,7.5108 9.0801,7.3368 9.1606,7.1514ZM10.076,3.7407C10.1424,3.7487 10.1851,3.8041 10.1779,3.8654L10.1779,3.8654C10.1111,4.303 10.0313,4.8324 9.9229,5.3838C9.8782,5.2436 9.8336,5.1029 9.7893,4.9621C9.8681,4.5458 9.9239,4.1593 9.9604,3.8477L9.9604,3.8477C9.968,3.7822 10.015,3.7405 10.0695,3.7405C10.0739,3.7405 10.0782,3.7408 10.0826,3.7413ZM3.0038,1.0393C3.0674,1.1256 3.1296,1.2234 3.1906,1.3314C2.563,2.3853 2.2457,4.153 2.1289,5.1523L2.1289,5.1523C2.1213,5.2178 2.0743,5.2595 2.0199,5.2595C2.0155,5.2595 2.0111,5.2592 2.0068,5.2587C1.9469,5.2513 1.9042,5.1958 1.9114,5.1346L1.9114,5.1346C2.0872,3.9834 2.3565,2.1676 3.0038,1.0393ZM4.0771,3.6161C4.1218,3.7564 4.1663,3.8971 4.2107,4.038C4.1319,4.4542 4.076,4.8406 4.0396,5.1523L4.0396,5.1523C4.032,5.2178 3.985,5.2595 3.9305,5.2595C3.9261,5.2595 3.9218,5.2592 3.9174,5.2587C3.8575,5.2513 3.8149,5.1958 3.8221,5.1346L3.8221,5.1346C3.8889,4.697 3.9687,4.1676 4.0771,3.6161ZM4.9153,1.0392C4.9787,1.1253 5.0406,1.2231 5.1014,1.3309C5.0072,1.489 4.92,1.6632 4.8394,1.8486C4.7862,1.7368 4.7319,1.6281 4.6762,1.5227C4.7495,1.3514 4.8293,1.1895 4.9153,1.0392Z"
|
||||
stroke="#00000010"
|
||||
strokeWidth="1.0"
|
||||
fillRule="evenodd"
|
||||
id="path_0"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
17
packages/app/src/components/Icons/customIcons/ogg.jsx
Normal file
17
packages/app/src/components/Icons/customIcons/ogg.jsx
Normal file
@ -0,0 +1,17 @@
|
||||
export default (props) => (
|
||||
<svg
|
||||
{...props}
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
viewBox="540.525 331.625 397.476 228.02"
|
||||
>
|
||||
<g>
|
||||
<g class="fills">
|
||||
<path
|
||||
d="M566.514,558.789C557.237,556.959,552.372,545.298,557.454,537.075C561.388,530.710,562.690,530.500,598.152,530.504C627.957,530.508,630.206,530.633,633.197,532.456C642.490,538.123,642.729,551.085,633.644,556.734C630.081,558.950,629.333,559.003,599.501,559.140C582.726,559.217,567.882,559.059,566.514,558.789ZL566.514,558.789ZM696.501,545.129L696.501,530.630L727.251,530.315C755.020,530.031,758.261,529.824,760.681,528.181C766.279,524.381,766.966,522.177,767.277,507.000C767.560,493.247,767.534,493.049,765.783,495.781C764.803,497.311,762.075,499.495,759.721,500.635C755.730,502.568,754.107,502.679,735.721,502.290C716.779,501.888,715.728,501.759,709.082,499.012C699.817,495.182,690.705,486.371,686.639,477.308C674.978,451.317,691.052,422.089,719.501,417.552C726.702,416.403,749.805,416.159,755.907,417.167C758.573,417.608,761.034,419.033,763.657,421.656L767.501,425.500L767.501,416.500L796.574,416.500L796.288,477.250L796.001,538.000L793.340,543.000C789.950,549.370,785.327,553.787,779.001,556.699C774.098,558.955,773.245,559.006,735.251,559.314L696.501,559.627L696.501,545.129ZM760.784,471.236C764.920,468.523,766.886,464.782,766.883,459.632C766.880,454.197,764.574,450.143,760.102,447.712C756.132,445.555,729.230,444.688,722.499,446.501C712.023,449.322,708.272,462.239,715.859,469.367C719.642,472.920,722.937,473.422,741.048,473.205C755.422,473.032,758.518,472.723,760.784,471.236ZL760.784,471.236ZM838.501,545.127L838.501,530.608L868.751,530.304C896.301,530.027,899.279,529.832,902.115,528.114C907.978,524.565,908.964,521.547,909.339,506.000L909.677,492.000L907.319,495.500C902.827,502.167,900.617,502.666,877.501,502.233C857.909,501.866,856.745,501.732,851.225,499.218C839.447,493.853,831.849,486.244,827.304,475.264C825.585,471.108,825.113,467.894,825.063,460.000C824.978,446.311,827.507,439.785,836.522,430.439C847.667,418.882,858.037,415.956,885.320,416.669C901.497,417.092,903.545,417.756,907.441,423.837L909.467,427.000L909.484,421.723L909.501,416.445L923.751,416.723L938.001,417.000L938.001,537.000L935.141,542.500C931.304,549.878,929.086,552.131,922.001,555.850L916.001,559.000L838.501,559.645L838.501,545.127ZL838.501,545.127ZM902.308,471.500C910.768,465.999,910.662,453.060,902.115,447.885C899.370,446.223,896.869,446.000,881.001,446.000C863.755,446.000,862.864,446.098,859.736,448.329C855.133,451.612,853.169,455.866,853.775,461.242C854.350,466.341,858.178,471.021,862.963,472.477C864.634,472.985,873.651,473.311,883.001,473.200C896.159,473.045,900.522,472.661,902.308,471.500ZL902.308,471.500ZM575.501,501.549C558.549,498.445,544.412,483.434,541.552,465.500C540.174,456.858,540.187,377.030,541.567,368.519C544.660,349.456,558.992,335.470,578.564,332.415C583.180,331.695,592.449,331.445,603.001,331.758C618.530,332.217,620.650,332.505,627.501,335.082C633.871,337.477,636.055,338.975,642.001,345.024C647.378,350.494,649.581,353.636,651.501,358.573L654.001,365.000L654.001,470.000L651.548,475.500C646.428,486.978,639.529,493.863,627.777,499.221C622.199,501.764,621.280,501.863,601.001,502.114C589.451,502.256,577.976,502.002,575.501,501.549ZL575.501,501.549ZM615.558,472.501C617.514,471.957,620.439,470.131,622.058,468.441L625.001,465.370L625.279,418.441C625.587,366.622,625.731,367.940,619.266,363.329C616.138,361.098,615.247,361.000,598.001,361.000C577.994,361.000,575.687,361.608,571.886,367.886C570.128,370.789,569.981,373.956,569.702,414.712C569.495,445.077,569.750,459.713,570.539,462.643C571.767,467.204,576.236,472.111,579.787,472.799C584.348,473.682,612.139,473.450,615.558,472.501ZL615.558,472.501Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
@ -4,6 +4,7 @@ import VrChatIcon from "./customIcons/vrchat"
|
||||
import VerifiedBadge from "./customIcons/verifiedBadge"
|
||||
import Crown from "./customIcons/crown"
|
||||
import Lossless from "./customIcons/lossless"
|
||||
import Ogg from "./customIcons/ogg"
|
||||
|
||||
// import icons lib
|
||||
import * as lib1 from "react-icons/fi"
|
||||
@ -18,26 +19,17 @@ const marginedStyle = {
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
marginRight: "10px",
|
||||
verticalAlign: "-0.125em"
|
||||
verticalAlign: "-0.125em",
|
||||
}
|
||||
|
||||
const customs = {
|
||||
Lossless: (props) => <Lossless
|
||||
style={marginedStyle}
|
||||
{...props}
|
||||
/>,
|
||||
verifiedBadge: (props) => <VerifiedBadge
|
||||
style={marginedStyle}
|
||||
{...props}
|
||||
/>,
|
||||
VrChat: (props) => <VrChatIcon
|
||||
style={marginedStyle}
|
||||
{...props}
|
||||
/>,
|
||||
Crown: (props) => <Crown
|
||||
style={marginedStyle}
|
||||
{...props}
|
||||
/>
|
||||
Lossless: (props) => <Lossless style={marginedStyle} {...props} />,
|
||||
verifiedBadge: (props) => (
|
||||
<VerifiedBadge style={marginedStyle} {...props} />
|
||||
),
|
||||
VrChat: (props) => <VrChatIcon style={marginedStyle} {...props} />,
|
||||
Crown: (props) => <Crown style={marginedStyle} {...props} />,
|
||||
Ogg: (props) => <Ogg style={marginedStyle} {...props} />,
|
||||
}
|
||||
|
||||
export const Icons = {
|
||||
|
@ -6,14 +6,6 @@ import { Icons } from "@components/Icons"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const typeToNavigationType = {
|
||||
playlist: "playlist",
|
||||
album: "album",
|
||||
track: "track",
|
||||
single: "track",
|
||||
ep: "album",
|
||||
}
|
||||
|
||||
const Playlist = (props) => {
|
||||
const [coverHover, setCoverHover] = React.useState(false)
|
||||
|
||||
@ -28,14 +20,28 @@ const Playlist = (props) => {
|
||||
return props.onClick(playlist)
|
||||
}
|
||||
|
||||
return app.location.push(`/music/list/${playlist._id}`)
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (playlist.type) {
|
||||
params.set("type", playlist.type)
|
||||
}
|
||||
|
||||
if (playlist.service) {
|
||||
params.set("service", playlist.service)
|
||||
}
|
||||
|
||||
return app.location.push(
|
||||
`/music/list/${playlist._id}?${params.toString()}`,
|
||||
)
|
||||
}
|
||||
|
||||
const onClickPlay = (e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (playlist.items) {
|
||||
app.cores.player.start(playlist.items)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -68,8 +74,16 @@ const Playlist = (props) => {
|
||||
<div className="playlist_info_title" onClick={onClick}>
|
||||
<h1>{playlist.title}</h1>
|
||||
</div>
|
||||
|
||||
{props.row && (
|
||||
<div className="playlist_details">
|
||||
{playlist.service === "tidal" && (
|
||||
<p>
|
||||
<Icons.SiTidal />
|
||||
Tidal
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p>
|
||||
<Icons.MdAlbum />
|
||||
{playlist.type ?? "playlist"}
|
||||
|
@ -45,6 +45,13 @@
|
||||
justify-content: center;
|
||||
|
||||
.playlist_info_title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 7px;
|
||||
|
||||
h1 {
|
||||
word-break: break-all;
|
||||
white-space: wrap;
|
||||
|
@ -16,6 +16,7 @@ const typeToKind = {
|
||||
ep: "releases",
|
||||
compilation: "releases",
|
||||
playlist: "playlists",
|
||||
single: "tracks",
|
||||
}
|
||||
|
||||
const PlaylistHeader = ({
|
||||
@ -77,7 +78,6 @@ const PlaylistHeader = ({
|
||||
|
||||
<div className="play_info_details">
|
||||
<div className="play_info_title">
|
||||
{playlist.service === "tidal" && <Icons.SiTidal />}{" "}
|
||||
{typeof playlist.title === "function" ? (
|
||||
playlist.title()
|
||||
) : (
|
||||
@ -86,19 +86,24 @@ const PlaylistHeader = ({
|
||||
</div>
|
||||
|
||||
<div className="play_info_statistics">
|
||||
{playlist.service === "tidal" && (
|
||||
<div className="play_info_statistics_item">
|
||||
<p>
|
||||
<Icons.SiTidal /> From Tidal
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{PlaylistTypeDecorators[playlistType] && (
|
||||
<div className="play_info_statistics_item">
|
||||
{PlaylistTypeDecorators[playlistType]()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="play_info_statistics_item">
|
||||
<p>
|
||||
<Icons.MdLibraryMusic /> {playlist.total_items}{" "}
|
||||
Items
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{playlist.total_duration > 0 && (
|
||||
<div className="play_info_statistics_item">
|
||||
<p>
|
||||
@ -107,7 +112,6 @@ const PlaylistHeader = ({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{playlist.publisher && (
|
||||
<div className="play_info_statistics_item">
|
||||
<p onClick={handlePublisherClick}>
|
||||
|
@ -143,6 +143,18 @@ const TrackEditor = (props) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="track-editor-field">
|
||||
<div className="track-editor-field-header">
|
||||
<Icons.FiEye />
|
||||
<span>Public</span>
|
||||
</div>
|
||||
|
||||
<antd.Switch
|
||||
checked={track.public}
|
||||
onChange={(value) => handleChange("public", value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="track-editor-field">
|
||||
<div className="track-editor-field-header">
|
||||
<Icons.MdLyrics />
|
||||
|
@ -9,28 +9,25 @@ import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||
import "./index.less"
|
||||
|
||||
const ExtraActions = (props) => {
|
||||
const [trackInstance, setTrackInstance] = React.useState({})
|
||||
const [track, setTrack] = React.useState({})
|
||||
|
||||
const onPlayerStateChange = React.useCallback((state) => {
|
||||
const instance = app.cores.player.track()
|
||||
const track = app.cores.player.track()
|
||||
|
||||
if (instance) {
|
||||
setTrackInstance(instance)
|
||||
if (track) {
|
||||
setTrack(track)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [playerState] = usePlayerStateContext(onPlayerStateChange)
|
||||
usePlayerStateContext(onPlayerStateChange)
|
||||
|
||||
const handleClickLike = async () => {
|
||||
if (!trackInstance) {
|
||||
if (!track) {
|
||||
console.error("Cannot like a track if nothing is playing")
|
||||
return false
|
||||
}
|
||||
|
||||
await trackInstance.manifest.serviceOperations.toggleItemFavorite(
|
||||
"tracks",
|
||||
trackInstance.manifest._id,
|
||||
)
|
||||
await track.serviceOperations.toggleItemFavorite("tracks", track._id)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -39,18 +36,15 @@ const ExtraActions = (props) => {
|
||||
<Button
|
||||
type="ghost"
|
||||
icon={<Icons.MdAbc />}
|
||||
disabled={!trackInstance?.manifest?.lyrics_enabled}
|
||||
disabled={!track?.lyrics_enabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!app.isMobile && (
|
||||
<LikeButton
|
||||
liked={
|
||||
trackInstance?.manifest?.serviceOperations
|
||||
?.isItemFavorited
|
||||
}
|
||||
liked={track?.serviceOperations?.isItemFavorited}
|
||||
onClick={handleClickLike}
|
||||
disabled={!trackInstance?.manifest?._id}
|
||||
disabled={!track?._id}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 70%;
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
|
||||
padding: 2px 25px;
|
||||
|
@ -39,21 +39,21 @@ const EventsHandlers = {
|
||||
|
||||
const track = app.cores.player.track()
|
||||
|
||||
return await track.manifest.serviceOperations.toggleItemFavorite(
|
||||
return await track.serviceOperations.toggleItemFavorite(
|
||||
"track",
|
||||
ctx.track_manifest._id,
|
||||
track._id,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const Controls = (props) => {
|
||||
const [trackInstance, setTrackInstance] = React.useState({})
|
||||
const [trackManifest, setTrackManifest] = React.useState({})
|
||||
|
||||
const onPlayerStateChange = React.useCallback((state) => {
|
||||
const instance = app.cores.player.track()
|
||||
const track = app.cores.player.track()
|
||||
|
||||
if (instance) {
|
||||
setTrackInstance(instance)
|
||||
if (track) {
|
||||
setTrackManifest(track)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -131,12 +131,9 @@ const Controls = (props) => {
|
||||
|
||||
{app.isMobile && (
|
||||
<LikeButton
|
||||
liked={
|
||||
trackInstance?.manifest?.serviceOperations
|
||||
?.isItemFavorited
|
||||
}
|
||||
liked={trackManifest?.serviceOperations?.isItemFavorited}
|
||||
onClick={() => handleAction("like")}
|
||||
disabled={!trackInstance?.manifest?._id}
|
||||
disabled={!trackManifest?._id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
84
packages/app/src/components/Player/Indicators/index.jsx
Normal file
84
packages/app/src/components/Player/Indicators/index.jsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React from "react"
|
||||
import { Tooltip } from "antd"
|
||||
import { Icons } from "@components/Icons"
|
||||
|
||||
function getIndicators(track, playerState) {
|
||||
const indicators = []
|
||||
|
||||
if (playerState.live) {
|
||||
indicators.push({
|
||||
icon: <Icons.FiRadio style={{ color: "var(--colorPrimary)" }} />,
|
||||
})
|
||||
}
|
||||
|
||||
if (playerState.format_metadata && playerState.format_metadata?.trackInfo) {
|
||||
const dmuxData = playerState.format_metadata
|
||||
|
||||
// this commonly used my mpd's
|
||||
const trackInfo = dmuxData.trackInfo[0]
|
||||
const trackAudio = trackInfo?.audio
|
||||
|
||||
const codec = trackInfo?.codecName ?? dmuxData.codec
|
||||
const sampleRate = trackAudio?.samplingFrequency ?? dmuxData.sampleRate
|
||||
const bitDepth = trackAudio?.bitDepth ?? dmuxData.bitsPerSample
|
||||
const bitrate = trackAudio?.bitrate ?? dmuxData.bitrate
|
||||
|
||||
if (codec) {
|
||||
if (codec.toLowerCase().includes("flac")) {
|
||||
indicators.push({
|
||||
icon: <Icons.Lossless />,
|
||||
tooltip: `${sampleRate / 1000} kHz / ${bitDepth ?? 16} Bits`,
|
||||
})
|
||||
}
|
||||
|
||||
if (codec.toLowerCase().includes("vorbis")) {
|
||||
indicators.push({
|
||||
icon: <Icons.Ogg />,
|
||||
tooltip: `Vorbis ${sampleRate / 1000} kHz / ${bitrate / 1000} kbps`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return indicators
|
||||
}
|
||||
|
||||
const Indicators = ({ track, playerState }) => {
|
||||
if (!track) {
|
||||
return null
|
||||
}
|
||||
|
||||
const indicators = React.useMemo(
|
||||
() => getIndicators(track, playerState),
|
||||
[track, playerState],
|
||||
)
|
||||
|
||||
if (indicators.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="toolbar_player_indicators_wrapper">
|
||||
<div className="toolbar_player_indicators">
|
||||
{indicators.map((indicator, index) => {
|
||||
if (indicator.tooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
key={indicators.length}
|
||||
title={indicator.tooltip}
|
||||
>
|
||||
{indicator.icon}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return React.cloneElement(indicator.icon, {
|
||||
key: index,
|
||||
})
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Indicators
|
@ -9,6 +9,7 @@ import LiveInfo from "@components/Player/LiveInfo"
|
||||
import SeekBar from "@components/Player/SeekBar"
|
||||
import Controls from "@components/Player/Controls"
|
||||
import Actions from "@components/Player/Actions"
|
||||
import Indicators from "@components/Player/Indicators"
|
||||
|
||||
import RGBStringToValues from "@utils/rgbToValues"
|
||||
|
||||
@ -25,40 +26,6 @@ function isOverflown(parent, element) {
|
||||
return elementRect.width > parentRect.width
|
||||
}
|
||||
|
||||
const Indicators = ({ track, playerState }) => {
|
||||
if (!track) {
|
||||
return null
|
||||
}
|
||||
|
||||
const indicators = []
|
||||
|
||||
if (track.metadata) {
|
||||
if (track.metadata.lossless) {
|
||||
indicators.push(
|
||||
<antd.Tooltip title="Lossless Audio">
|
||||
<Icons.Lossless />
|
||||
</antd.Tooltip>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (playerState.live) {
|
||||
indicators.push(
|
||||
<Icons.FiRadio style={{ color: "var(--colorPrimary)" }} />,
|
||||
)
|
||||
}
|
||||
|
||||
if (indicators.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="toolbar_player_indicators_wrapper">
|
||||
<div className="toolbar_player_indicators">{indicators}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ServiceIndicator = (props) => {
|
||||
if (!props.service) {
|
||||
return null
|
||||
@ -96,14 +63,12 @@ const Player = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const { title, artist, service, cover_analysis, cover } =
|
||||
playerState.track_manifest ?? {}
|
||||
const { title, artist, service, cover } = playerState.track_manifest ?? {}
|
||||
|
||||
const playing = playerState.playback_status === "playing"
|
||||
const stopped = playerState.playback_status === "stopped"
|
||||
|
||||
const titleText = !playing && stopped ? "Stopped" : (title ?? "Untitled")
|
||||
const subtitleText = ""
|
||||
|
||||
React.useEffect(() => {
|
||||
const titleIsOverflown = isOverflown(
|
||||
@ -115,13 +80,11 @@ const Player = (props) => {
|
||||
}, [title])
|
||||
|
||||
React.useEffect(() => {
|
||||
const trackInstance = app.cores.player.track()
|
||||
const track = app.cores.player.track()
|
||||
|
||||
if (playerState.track_manifest && trackInstance) {
|
||||
if (
|
||||
typeof trackInstance.manifest.analyzeCoverColor === "function"
|
||||
) {
|
||||
trackInstance.manifest
|
||||
if (playerState.track_manifest && track) {
|
||||
if (typeof track.analyzeCoverColor === "function") {
|
||||
track
|
||||
.analyzeCoverColor()
|
||||
.then((analysis) => {
|
||||
setCoverAnalysis(analysis)
|
||||
@ -203,9 +166,11 @@ const Player = (props) => {
|
||||
</Marquee>
|
||||
)}
|
||||
|
||||
{!playerState.radioId && (
|
||||
<p className="toolbar_player_info_subtitle">
|
||||
{artist ?? ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{playerState.radioId && (
|
||||
|
@ -159,6 +159,13 @@
|
||||
}
|
||||
|
||||
.toolbar_player_info_subtitle {
|
||||
display: inline-block;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
|
||||
@ -246,6 +253,8 @@
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
background-color: rgba(var(--layoutBackgroundColor), 0.7);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
backdrop-filter: blur(5px);
|
||||
|
@ -184,12 +184,12 @@ const Searcher = (props) => {
|
||||
if (typeof props.model === "function") {
|
||||
result = await props.model(value, {
|
||||
...props.modelParams,
|
||||
limit_per_section: app.isMobile ? 3 : 5,
|
||||
limit: app.isMobile ? 3 : 5,
|
||||
})
|
||||
} else {
|
||||
result = await SearchModel.search(value, {
|
||||
...props.modelParams,
|
||||
limit_per_section: app.isMobile ? 3 : 5,
|
||||
limit: app.isMobile ? 3 : 5,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,6 @@ html {
|
||||
|
||||
align-items: center;
|
||||
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
@ -101,6 +100,7 @@ html {
|
||||
padding: 0 10px;
|
||||
|
||||
background-color: var(--background-color-primary);
|
||||
border: 3px solid var(--border-color) !important;
|
||||
|
||||
.ant-input-prefix {
|
||||
font-size: 2rem;
|
||||
@ -127,6 +127,9 @@ html {
|
||||
flex-wrap: wrap;
|
||||
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
max-height: 70vh;
|
||||
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
|
||||
@ -137,6 +140,7 @@ html {
|
||||
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color-primary);
|
||||
border: 3px solid var(--border-color);
|
||||
|
||||
.ant-result,
|
||||
.ant-result-title,
|
||||
|
@ -1,10 +1,17 @@
|
||||
import { MediaPlayer, Debug } from "dashjs"
|
||||
import shaka from "shaka-player/dist/shaka-player.compiled.js"
|
||||
|
||||
import PlayerProcessors from "./PlayerProcessors"
|
||||
import AudioPlayerStorage from "../player.storage"
|
||||
import TrackManifest from "../classes/TrackManifest"
|
||||
|
||||
import findInitializationChunk from "../helpers/findInitializationChunk"
|
||||
import parseSourceFormatMetadata from "../helpers/parseSourceFormatMetadata"
|
||||
import handleInlineDashManifest from "../helpers/handleInlineDashManifest"
|
||||
|
||||
export default class AudioBase {
|
||||
constructor(player) {
|
||||
this.player = player
|
||||
this.console = player.console
|
||||
}
|
||||
|
||||
audio = new Audio()
|
||||
@ -16,6 +23,7 @@ export default class AudioBase {
|
||||
processors = {}
|
||||
|
||||
waitUpdateTimeout = null
|
||||
_firstSegmentReceived = false
|
||||
|
||||
initialize = async () => {
|
||||
// create a audio context
|
||||
@ -26,73 +34,289 @@ export default class AudioBase {
|
||||
latencyHint: "playback",
|
||||
})
|
||||
|
||||
// configure some settings for audio
|
||||
// configure some settings for audio with optimized settings
|
||||
this.audio.crossOrigin = "anonymous"
|
||||
this.audio.preload = "metadata"
|
||||
this.audio.preload = "auto"
|
||||
this.audio.loop = this.player.state.playback_mode === "repeat"
|
||||
this.audio.volume = 1
|
||||
|
||||
// listen all events
|
||||
for (const [key, value] of Object.entries(this.audioEvents)) {
|
||||
this.audio.addEventListener(key, value)
|
||||
}
|
||||
|
||||
// setup demuxer for mpd
|
||||
// setup shaka player for mpd
|
||||
this.createDemuxer()
|
||||
|
||||
// create element source
|
||||
// create element source with low latency buffer
|
||||
this.elementSource = this.context.createMediaElementSource(this.audio)
|
||||
|
||||
// initialize audio processors
|
||||
await this.processorsManager.initialize()
|
||||
await this.processorsManager.initialize(),
|
||||
await this.processorsManager.attachAllNodes()
|
||||
}
|
||||
|
||||
createDemuxer() {
|
||||
this.demuxer = MediaPlayer().create()
|
||||
itemInit = async (manifest) => {
|
||||
if (!manifest) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.demuxer.updateSettings({
|
||||
if (
|
||||
typeof manifest === "string" ||
|
||||
(!manifest.source && !manifest.dash_manifest)
|
||||
) {
|
||||
this.console.time("resolve")
|
||||
manifest = await this.player.serviceProviders.resolve(manifest)
|
||||
this.console.timeEnd("resolve")
|
||||
}
|
||||
|
||||
if (!(manifest instanceof TrackManifest)) {
|
||||
this.console.time("init manifest")
|
||||
manifest = new TrackManifest(manifest, this.player)
|
||||
this.console.timeEnd("init manifest")
|
||||
}
|
||||
|
||||
if (manifest.mpd_mode === true && !manifest.dash_manifest) {
|
||||
this.console.time("fetch dash manifest")
|
||||
manifest.dash_manifest = await fetch(manifest.source).then((r) =>
|
||||
r.text(),
|
||||
)
|
||||
this.console.timeEnd("fetch dash manifest")
|
||||
}
|
||||
|
||||
return manifest
|
||||
}
|
||||
|
||||
play = async (manifest, params = {}) => {
|
||||
// Pre-initialize audio context if needed
|
||||
if (this.context.state === "suspended") {
|
||||
await this.context.resume()
|
||||
}
|
||||
|
||||
manifest = await this.itemInit(manifest)
|
||||
|
||||
this.console.time("load source")
|
||||
await this.loadSource(manifest)
|
||||
this.console.timeEnd("load source")
|
||||
|
||||
this.player.queue.currentItem = manifest
|
||||
this.player.state.track_manifest = manifest.toSeriableObject()
|
||||
this.player.nativeControls.update(manifest.toSeriableObject())
|
||||
|
||||
// reset audio properties
|
||||
this.audio.currentTime = params.time ?? 0
|
||||
this.audio.volume = 1
|
||||
|
||||
if (this.processors && this.processors.gain) {
|
||||
this.processors.gain.set(this.player.state.volume)
|
||||
}
|
||||
|
||||
if (this.audio.paused) {
|
||||
try {
|
||||
this.console.time("play")
|
||||
await this.audio.play()
|
||||
this.console.timeEnd("play")
|
||||
} catch (error) {
|
||||
this.console.error(
|
||||
"Error during audio.play():",
|
||||
error,
|
||||
"State:",
|
||||
this.audio.readyState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let initChunk = manifest.source
|
||||
|
||||
if (this.demuxer && manifest.dash_manifest) {
|
||||
initChunk = findInitializationChunk(
|
||||
manifest.source,
|
||||
manifest.dash_manifest,
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
this.player.state.format_metadata =
|
||||
await parseSourceFormatMetadata(initChunk)
|
||||
} catch (e) {
|
||||
this.player.state.format_metadata = null
|
||||
console.warn("Could not parse audio metadata from source:", e)
|
||||
}
|
||||
}
|
||||
|
||||
pause = async () => {
|
||||
this.audio.pause()
|
||||
}
|
||||
|
||||
resume = async () => {
|
||||
this.audio.play()
|
||||
}
|
||||
|
||||
async loadSource(manifest) {
|
||||
if (!manifest || !(manifest instanceof TrackManifest)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// reset some state
|
||||
this._firstSegmentReceived = false
|
||||
this.player.state.format_metadata = null
|
||||
|
||||
const isMpd = manifest.mpd_mode
|
||||
|
||||
if (isMpd) {
|
||||
const audioSrcAtt = this.audio.getAttribute("src")
|
||||
|
||||
if (audioSrcAtt && !audioSrcAtt.startsWith("blob:")) {
|
||||
this.audio.removeAttribute("src")
|
||||
this.audio.load()
|
||||
}
|
||||
|
||||
if (!this.demuxer) {
|
||||
this.console.log("Creating demuxer cause not initialized")
|
||||
this.createDemuxer()
|
||||
}
|
||||
|
||||
if (manifest._preloaded) {
|
||||
this.console.log(
|
||||
`using preloaded source >`,
|
||||
manifest._preloaded,
|
||||
)
|
||||
|
||||
return await this.demuxer.load(manifest._preloaded)
|
||||
}
|
||||
|
||||
const inlineManifest =
|
||||
"inline://" + manifest.source + "::" + manifest.dash_manifest
|
||||
|
||||
return await this.demuxer
|
||||
.load(inlineManifest, 0, "application/dash+xml")
|
||||
.catch((err) => {
|
||||
this.console.error("Error loading inline manifest", err)
|
||||
})
|
||||
}
|
||||
|
||||
// if not using demuxer, destroy previous instance
|
||||
if (this.demuxer) {
|
||||
await this.demuxer.unload()
|
||||
await this.demuxer.destroy()
|
||||
this.demuxer = null
|
||||
}
|
||||
|
||||
// load source
|
||||
this.audio.src = manifest.source
|
||||
return this.audio.load()
|
||||
}
|
||||
|
||||
async createDemuxer() {
|
||||
// Destroy previous instance if exists
|
||||
if (this.demuxer) {
|
||||
await this.demuxer.unload()
|
||||
await this.demuxer.detach()
|
||||
await this.demuxer.destroy()
|
||||
}
|
||||
|
||||
this.demuxer = new shaka.Player()
|
||||
|
||||
this.demuxer.attach(this.audio)
|
||||
|
||||
this.demuxer.configure({
|
||||
manifest: {
|
||||
//updatePeriod: 5,
|
||||
disableVideo: true,
|
||||
disableText: true,
|
||||
dash: {
|
||||
ignoreMinBufferTime: true,
|
||||
ignoreMaxSegmentDuration: true,
|
||||
autoCorrectDrift: false,
|
||||
enableFastSwitching: true,
|
||||
useStreamOnceInPeriodFlattening: false,
|
||||
},
|
||||
},
|
||||
streaming: {
|
||||
buffer: {
|
||||
resetSourceBuffersForTrackSwitch: true,
|
||||
bufferingGoal: 15,
|
||||
rebufferingGoal: 1,
|
||||
bufferBehind: 30,
|
||||
stallThreshold: 0.5,
|
||||
},
|
||||
},
|
||||
// debug: {
|
||||
// logLevel: Debug.LOG_LEVEL_DEBUG,
|
||||
// },
|
||||
})
|
||||
|
||||
this.demuxer.initialize(this.audio, null, false)
|
||||
shaka.net.NetworkingEngine.registerScheme(
|
||||
"inline",
|
||||
handleInlineDashManifest,
|
||||
)
|
||||
|
||||
this.demuxer.addEventListener("error", (event) => {
|
||||
console.error("Demuxer error", event)
|
||||
})
|
||||
}
|
||||
|
||||
timeTick = async () => {
|
||||
if (
|
||||
!this.audio ||
|
||||
!this.audio.duration ||
|
||||
this.audio.duration === Infinity
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const remainingTime = this.audio.duration - this.audio.currentTime
|
||||
|
||||
// if remaining time is less than 3s, try to init next item
|
||||
if (parseInt(remainingTime) <= 10) {
|
||||
// check if queue has next item
|
||||
if (this.player.queue.nextItems[0]) {
|
||||
this.player.queue.nextItems[0] = await this.itemInit(
|
||||
this.player.queue.nextItems[0],
|
||||
)
|
||||
|
||||
if (
|
||||
this.demuxer &&
|
||||
this.player.queue.nextItems[0].source &&
|
||||
this.player.queue.nextItems[0].mpd_mode &&
|
||||
!this.player.queue.nextItems[0]._preloaded
|
||||
) {
|
||||
const manifest = this.player.queue.nextItems[0]
|
||||
|
||||
// preload next item
|
||||
this.console.time("preload next item")
|
||||
this.player.queue.nextItems[0]._preloaded =
|
||||
await this.demuxer.preload(
|
||||
"inline://" +
|
||||
manifest.source +
|
||||
"::" +
|
||||
manifest.dash_manifest,
|
||||
0,
|
||||
"application/dash+xml",
|
||||
)
|
||||
this.console.timeEnd("preload next item")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
this.audio.pause()
|
||||
this.audio.src = null
|
||||
this.audio.currentTime = 0
|
||||
|
||||
if (this.demuxer) {
|
||||
this.demuxer.destroy()
|
||||
}
|
||||
|
||||
this.createDemuxer()
|
||||
}
|
||||
|
||||
audioEvents = {
|
||||
ended: () => {
|
||||
try {
|
||||
this.player.next()
|
||||
},
|
||||
loadeddata: () => {
|
||||
this.player.state.loading = false
|
||||
},
|
||||
loadedmetadata: () => {
|
||||
if (this.audio.duration === Infinity) {
|
||||
this.player.state.live = true
|
||||
} else {
|
||||
this.player.state.live = false
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
play: () => {
|
||||
this.player.state.playback_status = "playing"
|
||||
},
|
||||
pause: () => {
|
||||
this.player.state.playback_status = "paused"
|
||||
|
||||
if (typeof this._timeTickInterval !== "undefined") {
|
||||
clearInterval(this._timeTickInterval)
|
||||
}
|
||||
},
|
||||
playing: () => {
|
||||
this.player.state.loading = false
|
||||
|
||||
@ -102,15 +326,24 @@ export default class AudioBase {
|
||||
clearTimeout(this.waitUpdateTimeout)
|
||||
this.waitUpdateTimeout = null
|
||||
}
|
||||
|
||||
if (typeof this._timeTickInterval !== "undefined") {
|
||||
clearInterval(this._timeTickInterval)
|
||||
}
|
||||
|
||||
this.timeTick()
|
||||
|
||||
this._timeTickInterval = setInterval(this.timeTick, 1000)
|
||||
},
|
||||
pause: () => {
|
||||
this.player.state.playback_status = "paused"
|
||||
loadeddata: () => {
|
||||
this.player.state.loading = false
|
||||
},
|
||||
durationchange: () => {
|
||||
this.player.eventBus.emit(
|
||||
`player.durationchange`,
|
||||
this.audio.duration,
|
||||
)
|
||||
loadedmetadata: () => {
|
||||
if (this.audio.duration === Infinity) {
|
||||
this.player.state.live = true
|
||||
} else {
|
||||
this.player.state.live = false
|
||||
}
|
||||
},
|
||||
waiting: () => {
|
||||
if (this.waitUpdateTimeout) {
|
||||
|
@ -4,12 +4,15 @@ import AudioPlayerStorage from "../player.storage"
|
||||
export default class PlayerState {
|
||||
static defaultState = {
|
||||
loading: false,
|
||||
|
||||
playback_status: "stopped",
|
||||
playback_mode: AudioPlayerStorage.get("mode") ?? "normal",
|
||||
|
||||
track_manifest: null,
|
||||
demuxer_metadata: null,
|
||||
|
||||
muted: app.isMobile ? false : (AudioPlayerStorage.get("mute") ?? false),
|
||||
volume: app.isMobile ? 1 : (AudioPlayerStorage.get("volume") ?? 0.3),
|
||||
playback_mode: AudioPlayerStorage.get("mode") ?? "normal",
|
||||
}
|
||||
|
||||
constructor(player) {
|
||||
@ -23,12 +26,21 @@ export default class PlayerState {
|
||||
if (change.type === "update") {
|
||||
const stateKey = change.path[0]
|
||||
|
||||
this.player.eventBus.emit(`player.state.update:${stateKey}`, change.object[stateKey])
|
||||
this.player.eventBus.emit("player.state.update", change.object)
|
||||
this.player.eventBus.emit(
|
||||
`player.state.update:${stateKey}`,
|
||||
change.object[stateKey],
|
||||
)
|
||||
this.player.eventBus.emit(
|
||||
"player.state.update",
|
||||
change.object,
|
||||
)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
this.player.console.error(`Failed to dispatch state updater >`, error)
|
||||
this.player.console.error(
|
||||
`Failed to dispatch state updater >`,
|
||||
error,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -3,11 +3,13 @@ import ComtyMusicServiceInterface from "../providers/comtymusic"
|
||||
export default class ServiceProviders {
|
||||
providers = [
|
||||
// add by default here
|
||||
new ComtyMusicServiceInterface()
|
||||
new ComtyMusicServiceInterface(),
|
||||
]
|
||||
|
||||
findProvider(providerId) {
|
||||
return this.providers.find((provider) => provider.constructor.id === providerId)
|
||||
return this.providers.find(
|
||||
(provider) => provider.constructor.id === providerId,
|
||||
)
|
||||
}
|
||||
|
||||
register(provider) {
|
||||
@ -15,32 +17,42 @@ export default class ServiceProviders {
|
||||
}
|
||||
|
||||
has(providerId) {
|
||||
return this.providers.some((provider) => provider.constructor.id === providerId)
|
||||
return this.providers.some(
|
||||
(provider) => provider.constructor.id === providerId,
|
||||
)
|
||||
}
|
||||
|
||||
operation = async (operationName, providerId, manifest, args) => {
|
||||
const provider = await this.findProvider(providerId)
|
||||
|
||||
if (!provider) {
|
||||
console.error(`Failed to resolve manifest, provider [${providerId}] not registered`)
|
||||
console.error(
|
||||
`Failed to resolve manifest, provider [${providerId}] not registered`,
|
||||
)
|
||||
return manifest
|
||||
}
|
||||
|
||||
const operationFn = provider[operationName]
|
||||
|
||||
if (typeof operationFn !== "function") {
|
||||
console.error(`Failed to resolve manifest, provider [${providerId}] operation [${operationName}] not found`)
|
||||
console.error(
|
||||
`Failed to resolve manifest, provider [${providerId}] operation [${operationName}] not found`,
|
||||
)
|
||||
return manifest
|
||||
}
|
||||
|
||||
return await operationFn(manifest, args)
|
||||
}
|
||||
|
||||
resolve = async (providerId, manifest) => {
|
||||
const provider = await this.findProvider(providerId)
|
||||
resolve = async (manifest) => {
|
||||
let providerId = manifest.service ?? "default"
|
||||
|
||||
const provider = this.findProvider(providerId)
|
||||
|
||||
if (!provider) {
|
||||
console.error(`Failed to resolve manifest, provider [${providerId}] not registered`)
|
||||
console.error(
|
||||
`Failed to resolve manifest, provider [${providerId}] not registered`,
|
||||
)
|
||||
return manifest
|
||||
}
|
||||
|
||||
@ -49,7 +61,7 @@ export default class ServiceProviders {
|
||||
|
||||
resolveMany = async (manifests) => {
|
||||
manifests = manifests.map(async (manifest) => {
|
||||
return await this.resolve(manifest.service ?? "default", manifest)
|
||||
return await this.resolve(manifest)
|
||||
})
|
||||
|
||||
manifests = await Promise.all(manifests)
|
||||
|
202
packages/app/src/cores/player/classes/SyncRoom.js
Normal file
202
packages/app/src/cores/player/classes/SyncRoom.js
Normal file
@ -0,0 +1,202 @@
|
||||
import { RTEngineClient } from "linebridge-client"
|
||||
import SessionModel from "@models/session"
|
||||
|
||||
export default class SyncRoom {
|
||||
constructor(player) {
|
||||
this.player = player
|
||||
}
|
||||
|
||||
static pushInterval = 1000
|
||||
static maxTimeOffset = parseFloat(0.15)
|
||||
|
||||
state = {
|
||||
joined_room: null,
|
||||
last_track_id: null,
|
||||
}
|
||||
|
||||
pushInterval = null
|
||||
|
||||
socket = null
|
||||
|
||||
start = async () => {
|
||||
if (!this.socket) {
|
||||
await this.createSocket()
|
||||
}
|
||||
|
||||
await this.pushState()
|
||||
setInterval(this.pushState, SyncRoom.pushInterval)
|
||||
|
||||
this.player.eventBus.on("player.state.update", this.pushState)
|
||||
|
||||
this.socket.on(
|
||||
`sync_room:${app.userData._id}:request_lyrics`,
|
||||
async () => {
|
||||
let lyrics = null
|
||||
|
||||
if (this.player.queue.currentItem) {
|
||||
lyrics =
|
||||
await this.player.queue.currentItem.manifest.serviceOperations.fetchLyrics(
|
||||
{
|
||||
preferTranslation: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
this.socket.emit(
|
||||
`sync_room:${app.userData._id}:request_lyrics`,
|
||||
lyrics,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
stop = async () => {
|
||||
if (this.pushInterval) {
|
||||
clearInterval(this.pushInterval)
|
||||
}
|
||||
|
||||
if (this.socket) {
|
||||
await this.socket.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
pushState = async () => {
|
||||
if (!this.socket) {
|
||||
return null
|
||||
}
|
||||
|
||||
let track_manifest = null
|
||||
const currentItem = this.player.queue.currentItem
|
||||
|
||||
if (currentItem) {
|
||||
track_manifest = {
|
||||
...currentItem.toSeriableObject(),
|
||||
}
|
||||
}
|
||||
|
||||
// check if has changed the track
|
||||
if (
|
||||
this.state.last_track_id &&
|
||||
this.state.last_track_id !== track_manifest?._id
|
||||
) {
|
||||
// try to get lyrics
|
||||
const lyrics = await currentItem.serviceOperations
|
||||
.fetchLyrics()
|
||||
.catch(() => null)
|
||||
|
||||
this.socket.emit(`sync_room:push_lyrics`, lyrics)
|
||||
}
|
||||
|
||||
this.state.last_track_id = track_manifest?._id
|
||||
|
||||
await this.socket.emit(`sync_room:push`, {
|
||||
...this.player.state,
|
||||
track_manifest: track_manifest,
|
||||
duration: this.player.duration(),
|
||||
currentTime: this.player.seek(),
|
||||
})
|
||||
}
|
||||
|
||||
syncState = async (data) => {
|
||||
console.log(data)
|
||||
|
||||
if (!data || !data.track_manifest) {
|
||||
return false
|
||||
}
|
||||
|
||||
// first check if manifest id is different
|
||||
if (
|
||||
!this.player.state.track_manifest ||
|
||||
data.track_manifest._id !== this.player.state.track_manifest._id
|
||||
) {
|
||||
if (data.track_manifest && data.track_manifest.encoded_manifest) {
|
||||
let mpd = new Blob(
|
||||
[window.atob(data.track_manifest.encoded_manifest)],
|
||||
{
|
||||
type: "application/dash+xml",
|
||||
},
|
||||
)
|
||||
|
||||
data.track_manifest.dash_manifest = URL.createObjectURL(mpd)
|
||||
}
|
||||
|
||||
// start the player
|
||||
this.player.start(data.track_manifest)
|
||||
}
|
||||
|
||||
// check if currentTime is more than maxTimeOffset
|
||||
const serverTime = data.currentTime ?? 0
|
||||
const currentTime = this.player.seek()
|
||||
const offset = serverTime - currentTime
|
||||
|
||||
console.log({
|
||||
serverTime: serverTime,
|
||||
currentTime: currentTime,
|
||||
maxTimeOffset: SyncRoom.maxTimeOffset,
|
||||
offset: offset,
|
||||
})
|
||||
|
||||
if (
|
||||
typeof serverTime === "number" &&
|
||||
typeof currentTime === "number" &&
|
||||
Math.abs(offset) > SyncRoom.maxTimeOffset
|
||||
) {
|
||||
// seek to currentTime
|
||||
this.player.seek(serverTime)
|
||||
}
|
||||
|
||||
// check if playback is paused
|
||||
if (
|
||||
!app.cores.player.base().audio.paused &&
|
||||
data.playback_status === "paused"
|
||||
) {
|
||||
this.player.pausePlayback()
|
||||
}
|
||||
|
||||
if (
|
||||
app.cores.player.base().audio.paused &&
|
||||
data.playback_status === "playing"
|
||||
) {
|
||||
this.player.resumePlayback()
|
||||
}
|
||||
}
|
||||
|
||||
join = async (user_id) => {
|
||||
if (!this.socket) {
|
||||
await this.createSocket()
|
||||
}
|
||||
|
||||
this.socket.emit(`sync_room:join`, user_id)
|
||||
|
||||
this.socket.on(`sync:receive`, this.syncState)
|
||||
|
||||
this.state.joined_room = {
|
||||
user_id: user_id,
|
||||
members: [],
|
||||
}
|
||||
}
|
||||
|
||||
leave = async () => {
|
||||
await this.socket.emit(`sync_room:leave`, this.state.joined_room)
|
||||
|
||||
this.state.joined_room = null
|
||||
|
||||
if (this.socket) {
|
||||
await this.socket.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
createSocket = async () => {
|
||||
if (this.socket) {
|
||||
await this.socket.disconnect()
|
||||
}
|
||||
|
||||
this.socket = new RTEngineClient({
|
||||
refName: "sync-room",
|
||||
url: app.cores.api.client().mainOrigin + "/music",
|
||||
token: SessionModel.token,
|
||||
})
|
||||
|
||||
await this.socket.connect()
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import TrackManifest from "./TrackManifest"
|
||||
|
||||
export default class TrackInstance {
|
||||
constructor(manifest, player) {
|
||||
if (typeof manifest === "undefined") {
|
||||
throw new Error("Manifest is required")
|
||||
}
|
||||
|
||||
if (!player) {
|
||||
throw new Error("Player core is required")
|
||||
}
|
||||
|
||||
if (!(manifest instanceof TrackManifest)) {
|
||||
manifest = new TrackManifest(manifest, player)
|
||||
}
|
||||
|
||||
if (!manifest.source) {
|
||||
throw new Error("Manifest must have a source")
|
||||
}
|
||||
|
||||
this.player = player
|
||||
this.manifest = manifest
|
||||
|
||||
this.id = this.manifest.id ?? this.manifest._id
|
||||
}
|
||||
|
||||
play = async (params = {}) => {
|
||||
const startTime = performance.now()
|
||||
|
||||
const isMpd = this.manifest.source.endsWith(".mpd")
|
||||
const audioEl = this.player.base.audio
|
||||
|
||||
if (!isMpd) {
|
||||
// if a demuxer exists (from a previous MPD track), destroy it
|
||||
if (this.player.base.demuxer) {
|
||||
this.player.base.demuxer.destroy()
|
||||
this.player.base.demuxer = null
|
||||
}
|
||||
|
||||
// set the audio source directly
|
||||
if (audioEl.src !== this.manifest.source) {
|
||||
audioEl.src = this.manifest.source
|
||||
audioEl.load() // important to apply the new src and stop previous playback
|
||||
}
|
||||
} else {
|
||||
// ensure the direct 'src' attribute is removed if it was set
|
||||
const currentSrc = audioEl.getAttribute("src")
|
||||
|
||||
if (currentSrc && !currentSrc.startsWith("blob:")) {
|
||||
// blob: indicates MSE is likely already in use
|
||||
audioEl.removeAttribute("src")
|
||||
audioEl.load() // tell the element to update its state after src removal
|
||||
}
|
||||
|
||||
// ensure a demuxer instance exists
|
||||
if (!this.player.base.demuxer) {
|
||||
this.player.base.createDemuxer()
|
||||
}
|
||||
|
||||
// attach the mpd source to the demuxer
|
||||
await this.player.base.demuxer.attachSource(this.manifest.source)
|
||||
}
|
||||
|
||||
// reset audio properties
|
||||
audioEl.currentTime = params.time ?? 0
|
||||
audioEl.volume = 1
|
||||
|
||||
if (this.player.base.processors && this.player.base.processors.gain) {
|
||||
this.player.base.processors.gain.set(this.player.state.volume)
|
||||
}
|
||||
|
||||
if (audioEl.paused) {
|
||||
try {
|
||||
await audioEl.play()
|
||||
} catch (error) {
|
||||
console.error("[INSTANCE] Error during audio.play():", error)
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"[INSTANCE] Audio is already playing or will start shortly.",
|
||||
)
|
||||
}
|
||||
|
||||
this._loadMs = performance.now() - startTime
|
||||
|
||||
console.log(`[INSTANCE] [tooks ${this._loadMs}ms] Playing >`, this)
|
||||
}
|
||||
|
||||
pause = async () => {
|
||||
console.log("[INSTANCE] Pausing >", this)
|
||||
|
||||
this.player.base.audio.pause()
|
||||
}
|
||||
|
||||
resume = async () => {
|
||||
console.log("[INSTANCE] Resuming >", this)
|
||||
|
||||
this.player.base.audio.play()
|
||||
}
|
||||
}
|
@ -27,16 +27,32 @@ export default class TrackManifest {
|
||||
|
||||
if (typeof params.album !== "undefined") {
|
||||
this.album = params.album
|
||||
|
||||
if (typeof this.album === "object") {
|
||||
this.album = this.album.title
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof params.artist !== "undefined") {
|
||||
this.artist = params.artist
|
||||
|
||||
if (typeof this.artist === "object") {
|
||||
this.artist = this.artist.name
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof params.source !== "undefined") {
|
||||
this.source = params.source
|
||||
}
|
||||
|
||||
if (typeof params.dash_manifest !== "undefined") {
|
||||
this.dash_manifest = params.dash_manifest
|
||||
}
|
||||
|
||||
if (typeof params.encoded_manifest !== "undefined") {
|
||||
this.encoded_manifest = params.encoded_manifest
|
||||
}
|
||||
|
||||
if (typeof params.metadata !== "undefined") {
|
||||
this.metadata = params.metadata
|
||||
}
|
||||
@ -45,6 +61,15 @@ export default class TrackManifest {
|
||||
this.liked = params.liked
|
||||
}
|
||||
|
||||
if (typeof params.public !== "undefined") {
|
||||
this.public = params.public
|
||||
}
|
||||
|
||||
if (this.source) {
|
||||
this.mpd_mode =
|
||||
this.source.startsWith("blob:") || this.source.endsWith(".mpd")
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
@ -60,9 +85,10 @@ export default class TrackManifest {
|
||||
|
||||
// set default service to default
|
||||
service = "default"
|
||||
mpd_mode = false
|
||||
|
||||
async initialize() {
|
||||
if (!this.params.file) {
|
||||
if (!this.params.file || !(this.params.file instanceof File)) {
|
||||
return this
|
||||
}
|
||||
|
||||
@ -93,7 +119,12 @@ export default class TrackManifest {
|
||||
analyzeCoverColor = async () => {
|
||||
const fac = new FastAverageColor()
|
||||
|
||||
return await fac.getColorAsync(this.cover)
|
||||
const img = new Image()
|
||||
|
||||
img.src = this.cover + "?t=a"
|
||||
img.crossOrigin = "anonymous"
|
||||
|
||||
return await fac.getColorAsync(img)
|
||||
}
|
||||
|
||||
serviceOperations = {
|
||||
@ -164,8 +195,11 @@ export default class TrackManifest {
|
||||
album: this.album,
|
||||
artist: this.artist,
|
||||
source: this.source,
|
||||
dash_manifest: this.dash_manifest,
|
||||
encoded_manifest: this.encoded_manifest,
|
||||
metadata: this.metadata,
|
||||
liked: this.liked,
|
||||
service: this.service,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,81 @@
|
||||
export default (baseUri, mpdText, periodId = null, repId = null) => {
|
||||
// parse xml
|
||||
const parser = new DOMParser()
|
||||
const xml = parser.parseFromString(mpdText, "application/xml")
|
||||
|
||||
// check parse errors
|
||||
const err = xml.querySelector("parsererror")
|
||||
|
||||
if (err) {
|
||||
console.error("Failed to parse MPD:", err.textContent)
|
||||
return null
|
||||
}
|
||||
|
||||
// select period (by ID or first)
|
||||
let period = null
|
||||
|
||||
if (periodId) {
|
||||
period = xml.querySelector(`Period[id="${periodId}"]`)
|
||||
}
|
||||
|
||||
// if not found, select first
|
||||
if (!period) {
|
||||
period = xml.querySelector("Period")
|
||||
}
|
||||
|
||||
// ultimately, return err
|
||||
if (!period) {
|
||||
console.error("Cannot find a <Period> on provided MPD")
|
||||
return null
|
||||
}
|
||||
|
||||
// select representation (by ID or first)
|
||||
let rep = null
|
||||
|
||||
if (repId) {
|
||||
rep = xml.querySelector(`Representation[id="${repId}"]`)
|
||||
}
|
||||
|
||||
if (!rep) {
|
||||
rep = period.querySelector("AdaptationSet Representation")
|
||||
}
|
||||
|
||||
if (!rep) {
|
||||
console.error("Cannot find a <Representation> on Period")
|
||||
return null
|
||||
}
|
||||
|
||||
// read the associated SegmentTemplate (it may be in AdaptationSet or in Representation)
|
||||
let tmpl = rep.querySelector("SegmentTemplate")
|
||||
|
||||
if (!tmpl) {
|
||||
// fallback: look in the parent AdaptationSet
|
||||
const adaptation = rep.closest("AdaptationSet")
|
||||
tmpl = adaptation && adaptation.querySelector("SegmentTemplate")
|
||||
}
|
||||
|
||||
if (!tmpl) {
|
||||
console.error(
|
||||
"Could not find <SegmentTemplate> in either Representation or AdaptationSet.",
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// extract the initialization attribute
|
||||
const initAttr = tmpl.getAttribute("initialization")
|
||||
|
||||
if (!initAttr) {
|
||||
console.warn(
|
||||
"The <SegmentTemplate> does not declare initialization; it may be self-initializing.",
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// replace $RepresentationID$ if necessary
|
||||
const initPath = initAttr.replace(
|
||||
/\$RepresentationID\$/g,
|
||||
rep.getAttribute("id"),
|
||||
)
|
||||
|
||||
return new URL(initPath, baseUri).toString()
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
export default (uri) => {
|
||||
const manifest = uri.split("inline://")[1]
|
||||
const [baseUri, manifestString] = manifest.split("::")
|
||||
|
||||
const response = {
|
||||
data: new Uint8Array(new TextEncoder().encode(manifestString)).buffer,
|
||||
headers: {},
|
||||
uri: baseUri,
|
||||
originalUri: baseUri,
|
||||
timeMs: performance.now(),
|
||||
fromCache: true,
|
||||
}
|
||||
|
||||
return Promise.resolve(response)
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { parseWebStream } from "music-metadata"
|
||||
|
||||
export default async (source) => {
|
||||
const stream = await fetch(source, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
//Range: "bytes=0-1024",
|
||||
},
|
||||
}).then((response) => response.body)
|
||||
|
||||
return (await parseWebStream(stream)).format
|
||||
}
|
@ -2,12 +2,13 @@ import { Core } from "@ragestudio/vessel"
|
||||
|
||||
import ActivityEvent from "@classes/ActivityEvent"
|
||||
import QueueManager from "@classes/QueueManager"
|
||||
import TrackInstance from "./classes/TrackInstance"
|
||||
import TrackManifest from "./classes/TrackManifest"
|
||||
import MediaSession from "./classes/MediaSession"
|
||||
import ServiceProviders from "./classes/Services"
|
||||
import PlayerState from "./classes/PlayerState"
|
||||
import PlayerUI from "./classes/PlayerUI"
|
||||
import AudioBase from "./classes/AudioBase"
|
||||
import SyncRoom from "./classes/SyncRoom"
|
||||
|
||||
import setSampleRate from "./helpers/setSampleRate"
|
||||
|
||||
@ -23,16 +24,14 @@ export default class Player extends Core {
|
||||
// player config
|
||||
static defaultSampleRate = 48000
|
||||
|
||||
base = new AudioBase(this)
|
||||
state = new PlayerState(this)
|
||||
ui = new PlayerUI(this)
|
||||
serviceProviders = new ServiceProviders()
|
||||
nativeControls = new MediaSession(this)
|
||||
syncRoom = new SyncRoom(this)
|
||||
|
||||
base = new AudioBase(this)
|
||||
|
||||
queue = new QueueManager({
|
||||
loadFunction: this.createInstance,
|
||||
})
|
||||
queue = new QueueManager()
|
||||
|
||||
public = {
|
||||
start: this.start,
|
||||
@ -68,10 +67,16 @@ export default class Player extends Core {
|
||||
base: () => {
|
||||
return this.base
|
||||
},
|
||||
sync: () => this.syncRoom,
|
||||
inOnSyncMode: this.inOnSyncMode,
|
||||
state: this.state,
|
||||
ui: this.ui.public,
|
||||
}
|
||||
|
||||
inOnSyncMode() {
|
||||
return !!this.syncRoom.state.joined_room
|
||||
}
|
||||
|
||||
async afterInitialize() {
|
||||
if (app.isMobile) {
|
||||
this.state.volume = 1
|
||||
@ -81,53 +86,20 @@ export default class Player extends Core {
|
||||
await this.base.initialize()
|
||||
}
|
||||
|
||||
//
|
||||
// Instance managing methods
|
||||
//
|
||||
async abortPreloads() {
|
||||
for await (const instance of this.queue.nextItems) {
|
||||
if (instance.abortController?.abort) {
|
||||
instance.abortController.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Playback methods
|
||||
//
|
||||
async play(instance, params = {}) {
|
||||
if (!instance) {
|
||||
throw new Error("Audio instance is required")
|
||||
}
|
||||
|
||||
// resume audio context if needed
|
||||
if (this.base.context.state === "suspended") {
|
||||
this.base.context.resume()
|
||||
}
|
||||
|
||||
// update manifest
|
||||
this.state.track_manifest =
|
||||
this.queue.currentItem.manifest.toSeriableObject()
|
||||
|
||||
// play
|
||||
//await this.queue.currentItem.audio.play()
|
||||
await this.queue.currentItem.play(params)
|
||||
|
||||
// update native controls
|
||||
this.nativeControls.update(this.queue.currentItem.manifest)
|
||||
|
||||
return this.queue.currentItem
|
||||
}
|
||||
|
||||
// TODO: Improve performance for large playlists
|
||||
async start(manifest, { time, startIndex = 0, radioId } = {}) {
|
||||
this.console.debug("start():", {
|
||||
manifest: manifest,
|
||||
time: time,
|
||||
startIndex: startIndex,
|
||||
radioId: radioId,
|
||||
})
|
||||
|
||||
this.ui.attachPlayerComponent()
|
||||
|
||||
if (this.queue.currentItem) {
|
||||
await this.queue.currentItem.pause()
|
||||
await this.base.pause()
|
||||
}
|
||||
|
||||
//await this.abortPreloads()
|
||||
await this.queue.flush()
|
||||
|
||||
this.state.loading = true
|
||||
@ -147,32 +119,31 @@ export default class Player extends Core {
|
||||
return false
|
||||
}
|
||||
|
||||
if (playlist.some((item) => typeof item === "string")) {
|
||||
playlist = await this.serviceProviders.resolveMany(playlist)
|
||||
// resolve only the first item if needed
|
||||
if (
|
||||
typeof playlist[0] === "string" ||
|
||||
(!playlist[0].source && !playlist[0].dash_manifest)
|
||||
) {
|
||||
playlist[0] = await this.serviceProviders.resolve(playlist[0])
|
||||
}
|
||||
|
||||
if (playlist.some((item) => !item.source)) {
|
||||
playlist = await this.serviceProviders.resolveMany(playlist)
|
||||
}
|
||||
// create instance for the first element
|
||||
playlist[0] = new TrackManifest(playlist[0], this)
|
||||
|
||||
for await (let [index, _manifest] of playlist.entries()) {
|
||||
let instance = new TrackInstance(_manifest, this)
|
||||
this.queue.add(playlist)
|
||||
|
||||
this.queue.add(instance)
|
||||
}
|
||||
const item = this.queue.setCurrent(startIndex)
|
||||
|
||||
const item = this.queue.set(startIndex)
|
||||
|
||||
this.play(item, {
|
||||
this.base.play(item, {
|
||||
time: time ?? 0,
|
||||
})
|
||||
|
||||
// send the event to the server
|
||||
if (item.manifest._id && item.manifest.service === "default") {
|
||||
if (item._id && item.service === "default") {
|
||||
new ActivityEvent("player.play", {
|
||||
identifier: "unique", // this must be unique to prevent duplicate events and ensure only have unique track events
|
||||
track_id: item.manifest._id,
|
||||
service: item.manifest.service,
|
||||
track_id: item._id,
|
||||
service: item.service,
|
||||
})
|
||||
}
|
||||
|
||||
@ -182,13 +153,15 @@ export default class Player extends Core {
|
||||
// similar to player.start, but add to the queue
|
||||
// if next is true, it will add to the queue to the top of the queue
|
||||
async addToQueue(manifest, { next = false } = {}) {
|
||||
if (typeof manifest === "string") {
|
||||
manifest = await this.serviceProviders.resolve(manifest)
|
||||
if (this.inOnSyncMode()) {
|
||||
return false
|
||||
}
|
||||
|
||||
let instance = new TrackInstance(manifest, this)
|
||||
if (this.state.playback_status === "stopped") {
|
||||
return this.start(manifest)
|
||||
}
|
||||
|
||||
this.queue.add(instance, next === true ? "start" : "end")
|
||||
this.queue.add(manifest, next === true ? "start" : "end")
|
||||
|
||||
console.log("Added to queue", {
|
||||
manifest,
|
||||
@ -197,6 +170,10 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
next() {
|
||||
if (this.inOnSyncMode()) {
|
||||
return false
|
||||
}
|
||||
|
||||
//const isRandom = this.state.playback_mode === "shuffle"
|
||||
const item = this.queue.next()
|
||||
|
||||
@ -204,19 +181,27 @@ export default class Player extends Core {
|
||||
return this.stopPlayback()
|
||||
}
|
||||
|
||||
return this.play(item)
|
||||
return this.base.play(item)
|
||||
}
|
||||
|
||||
previous() {
|
||||
if (this.inOnSyncMode()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const item = this.queue.previous()
|
||||
|
||||
return this.play(item)
|
||||
return this.base.play(item)
|
||||
}
|
||||
|
||||
//
|
||||
// Playback Control
|
||||
//
|
||||
async togglePlayback() {
|
||||
if (this.inOnSyncMode()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.state.playback_status === "paused") {
|
||||
await this.resumePlayback()
|
||||
} else {
|
||||
@ -238,7 +223,7 @@ export default class Player extends Core {
|
||||
this.base.processors.gain.fade(0)
|
||||
|
||||
setTimeout(() => {
|
||||
this.queue.currentItem.pause()
|
||||
this.base.pause()
|
||||
resolve()
|
||||
}, Player.gradualFadeMs)
|
||||
|
||||
@ -258,7 +243,7 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
// ensure audio elemeto starts from 0 volume
|
||||
this.queue.currentItem.resume().then(() => {
|
||||
this.base.resume().then(() => {
|
||||
resolve()
|
||||
})
|
||||
this.base.processors.gain.fade(this.state.volume)
|
||||
@ -282,14 +267,13 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
stopPlayback() {
|
||||
this.base.flush()
|
||||
this.queue.flush()
|
||||
|
||||
this.state.playback_status = "stopped"
|
||||
this.state.track_manifest = null
|
||||
this.queue.currentItem = null
|
||||
|
||||
//this.abortPreloads()
|
||||
this.base.flush()
|
||||
this.queue.flush()
|
||||
|
||||
this.nativeControls.flush()
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,6 @@ export default class SFXCore extends Core {
|
||||
|
||||
if (slider) {
|
||||
// check if is up or down
|
||||
this.console.log(slider)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,8 +33,6 @@ export class ThemeProvider extends React.Component {
|
||||
}
|
||||
|
||||
handleUpdate = (update) => {
|
||||
console.log("[THEME] Update", update)
|
||||
|
||||
this.setState({
|
||||
useAlgorigthm: variantKeyToColor(app.cores.style.currentVariantKey),
|
||||
useCompactMode: update["compact-mode"],
|
||||
@ -235,12 +233,8 @@ export default class StyleCore extends Core {
|
||||
|
||||
this.isOnTemporalVariant = false
|
||||
|
||||
this.console.log(`Input variant key [${variantKey}]`)
|
||||
|
||||
const color = variantKeyToColor(variantKey)
|
||||
|
||||
this.console.log(`Applying variant [${color}]`)
|
||||
|
||||
const values = this.public.theme.variants[color]
|
||||
|
||||
if (!values) {
|
||||
|
@ -1,2 +0,0 @@
|
||||
export { default as useHacks } from "./useHacks"
|
||||
export { default as useCenteredContainer } from "./useCenteredContainer"
|
43
packages/app/src/hooks/useCoverAnalysis/index.js
Normal file
43
packages/app/src/hooks/useCoverAnalysis/index.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
const getDominantColorStr = (analysis) => {
|
||||
if (!analysis) return "0,0,0"
|
||||
return analysis.value?.join(", ") || "0,0,0"
|
||||
}
|
||||
|
||||
export default (trackManifest) => {
|
||||
const [coverAnalysis, setCoverAnalysis] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const getCoverAnalysis = async () => {
|
||||
const track = app.cores.player.track()
|
||||
|
||||
if (!track?.analyzeCoverColor) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const analysis = await track.analyzeCoverColor()
|
||||
setCoverAnalysis(analysis)
|
||||
} catch (error) {
|
||||
console.error("Failed to get cover analysis:", error)
|
||||
setCoverAnalysis(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (trackManifest) {
|
||||
getCoverAnalysis()
|
||||
} else {
|
||||
setCoverAnalysis(null)
|
||||
}
|
||||
}, [trackManifest])
|
||||
|
||||
const dominantColor = {
|
||||
"--dominant-color": getDominantColorStr(coverAnalysis),
|
||||
}
|
||||
|
||||
return {
|
||||
coverAnalysis,
|
||||
dominantColor,
|
||||
}
|
||||
}
|
47
packages/app/src/hooks/useFullScreen/index.js
Normal file
47
packages/app/src/hooks/useFullScreen/index.js
Normal file
@ -0,0 +1,47 @@
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
|
||||
const toggleFullScreen = (to) => {
|
||||
const targetState = to ?? !document.fullscreenElement
|
||||
|
||||
try {
|
||||
if (targetState) {
|
||||
document.documentElement.requestFullscreen()
|
||||
} else if (document.fullscreenElement) {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Fullscreen toggle failed:", error)
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ onEnter, onExit } = {}) => {
|
||||
const [isFullScreen, setIsFullScreen] = useState(false)
|
||||
|
||||
const handleFullScreenChange = useCallback(() => {
|
||||
const fullScreenState = !!document.fullscreenElement
|
||||
setIsFullScreen(fullScreenState)
|
||||
|
||||
if (fullScreenState) {
|
||||
onEnter?.()
|
||||
} else {
|
||||
onExit?.()
|
||||
}
|
||||
}, [onEnter, onExit])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("fullscreenchange", handleFullScreenChange)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
"fullscreenchange",
|
||||
handleFullScreenChange,
|
||||
)
|
||||
}
|
||||
}, [handleFullScreenChange])
|
||||
|
||||
return {
|
||||
isFullScreen,
|
||||
toggleFullScreen,
|
||||
handleFullScreenChange,
|
||||
}
|
||||
}
|
69
packages/app/src/hooks/useLyrics/index.js
Normal file
69
packages/app/src/hooks/useLyrics/index.js
Normal file
@ -0,0 +1,69 @@
|
||||
import { useState, useCallback, useEffect } from "react"
|
||||
import parseTimeToMs from "@utils/parseTimeToMs"
|
||||
|
||||
export default ({ trackManifest }) => {
|
||||
const [lyrics, setLyrics] = useState(null)
|
||||
|
||||
const processLyrics = useCallback((rawLyrics) => {
|
||||
if (!rawLyrics) return false
|
||||
|
||||
return rawLyrics.sync_audio_at && !rawLyrics.sync_audio_at_ms
|
||||
? {
|
||||
...rawLyrics,
|
||||
sync_audio_at_ms: parseTimeToMs(rawLyrics.sync_audio_at),
|
||||
}
|
||||
: rawLyrics
|
||||
}, [])
|
||||
|
||||
const loadCurrentTrackLyrics = useCallback(async () => {
|
||||
let data = null
|
||||
|
||||
const track = app.cores.player.track()
|
||||
|
||||
if (!trackManifest || !track) {
|
||||
return null
|
||||
}
|
||||
|
||||
// if is in sync mode, fetch lyrics from sync room
|
||||
if (app.cores.player.inOnSyncMode()) {
|
||||
const syncRoomSocket = app.cores.player.sync().socket
|
||||
|
||||
if (syncRoomSocket) {
|
||||
data = await syncRoomSocket
|
||||
.call("sync_room:request_lyrics")
|
||||
.catch(() => null)
|
||||
}
|
||||
} else {
|
||||
data = await track.serviceOperations.fetchLyrics().catch(() => null)
|
||||
}
|
||||
|
||||
// if no data founded, flush lyrics
|
||||
if (!data) {
|
||||
return setLyrics(null)
|
||||
}
|
||||
|
||||
// process & set lyrics
|
||||
data = processLyrics(data)
|
||||
setLyrics(data)
|
||||
|
||||
console.log("Track Lyrics:", data)
|
||||
}, [trackManifest, processLyrics])
|
||||
|
||||
// Load lyrics when track manifest changes or when translation is toggled
|
||||
useEffect(() => {
|
||||
if (!trackManifest) {
|
||||
setLyrics(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (!lyrics || lyrics.track_id !== trackManifest._id) {
|
||||
loadCurrentTrackLyrics()
|
||||
}
|
||||
}, [trackManifest, lyrics?.track_id, loadCurrentTrackLyrics])
|
||||
|
||||
return {
|
||||
lyrics,
|
||||
setLyrics,
|
||||
loadCurrentTrackLyrics,
|
||||
}
|
||||
}
|
60
packages/app/src/hooks/useSyncRoom/index.js
Normal file
60
packages/app/src/hooks/useSyncRoom/index.js
Normal file
@ -0,0 +1,60 @@
|
||||
import { useState, useRef, useCallback, useEffect } from "react"
|
||||
|
||||
export default () => {
|
||||
const [syncRoom, setSyncRoom] = useState(null)
|
||||
const syncSocket = useRef(null)
|
||||
|
||||
const subscribeLyricsUpdates = useCallback(
|
||||
(callback) => {
|
||||
if (!syncSocket.current) {
|
||||
return null
|
||||
}
|
||||
|
||||
syncSocket.current.on("sync:lyrics:receive", callback)
|
||||
|
||||
return () => syncSocket.current.off("sync:lyrics:receive", callback)
|
||||
},
|
||||
[syncSocket.current],
|
||||
)
|
||||
|
||||
const unsubscribeLyricsUpdates = useCallback(
|
||||
(callback) => {
|
||||
if (!syncSocket.current) {
|
||||
return null
|
||||
}
|
||||
|
||||
syncSocket.current.off("sync:lyrics:receive", callback)
|
||||
},
|
||||
[syncSocket.current],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const roomId = new URLSearchParams(window.location.search).get("sync")
|
||||
|
||||
if (roomId) {
|
||||
app.cores.player
|
||||
.sync()
|
||||
.join(roomId)
|
||||
.then(() => {
|
||||
setSyncRoom(roomId)
|
||||
syncSocket.current = app.cores.player.sync().socket
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (syncSocket.current) {
|
||||
app.cores.player.sync().leave()
|
||||
|
||||
setSyncRoom(null)
|
||||
syncSocket.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
syncRoom,
|
||||
subscribeLyricsUpdates,
|
||||
unsubscribeLyricsUpdates,
|
||||
isInSyncMode: app.cores.player.inOnSyncMode(),
|
||||
}
|
||||
}
|
19
packages/app/src/hooks/useTrackManifest/index.js
Normal file
19
packages/app/src/hooks/useTrackManifest/index.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
export default (playerTrackManifest) => {
|
||||
const [trackManifest, setTrackManifest] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
JSON.stringify(playerTrackManifest) !==
|
||||
JSON.stringify(trackManifest)
|
||||
) {
|
||||
setTrackManifest(playerTrackManifest)
|
||||
}
|
||||
}, [playerTrackManifest, trackManifest])
|
||||
|
||||
return {
|
||||
trackManifest,
|
||||
setTrackManifest,
|
||||
}
|
||||
}
|
@ -71,8 +71,8 @@ const PlayerButton = (props) => {
|
||||
openPlayerView()
|
||||
}
|
||||
|
||||
if (track.manifest?.analyzeCoverColor) {
|
||||
track.manifest
|
||||
if (track?.analyzeCoverColor) {
|
||||
track
|
||||
.analyzeCoverColor()
|
||||
.then((analysis) => {
|
||||
setCoverAnalyzed(analysis)
|
||||
|
@ -41,14 +41,14 @@
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
border: 2px solid var(--border-color);
|
||||
border: 3px solid var(--border-color);
|
||||
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(var(--bg_color_6), 0.1);
|
||||
background-color: @modal_background_color;
|
||||
|
||||
backdrop-filter: blur(@modal_background_blur);
|
||||
-webkit-backdrop-filter: blur(@modal_background_blur);
|
||||
|
22
packages/app/src/pages/lyrics/components/Background.jsx
Normal file
22
packages/app/src/pages/lyrics/components/Background.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
const Background = ({ trackManifest, hasVideoSource }) => {
|
||||
if (!trackManifest || hasVideoSource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lyrics-background-wrapper">
|
||||
<div className="lyrics-background-cover">
|
||||
<img
|
||||
src={trackManifest.cover}
|
||||
alt="Album cover"
|
||||
loading="eager"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Background);
|
@ -3,13 +3,13 @@ import { Tag, Button } from "antd"
|
||||
import classnames from "classnames"
|
||||
import Marquee from "react-fast-marquee"
|
||||
|
||||
import useHideOnMouseStop from "@hooks/useHideOnMouseStop"
|
||||
|
||||
import { Icons } from "@components/Icons"
|
||||
import Controls from "@components/Player/Controls"
|
||||
import Indicators from "@components/Player/Indicators"
|
||||
import SeekBar from "@components/Player/SeekBar"
|
||||
import LiveInfo from "@components/Player/LiveInfo"
|
||||
|
||||
import useHideOnMouseStop from "@hooks/useHideOnMouseStop"
|
||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||
|
||||
function isOverflown(element) {
|
||||
@ -23,7 +23,7 @@ function isOverflown(element) {
|
||||
)
|
||||
}
|
||||
|
||||
const PlayerController = React.forwardRef((props, ref) => {
|
||||
const PlayerController = (props, ref) => {
|
||||
const [playerState] = usePlayerStateContext()
|
||||
|
||||
const titleRef = React.useRef()
|
||||
@ -34,51 +34,13 @@ const PlayerController = React.forwardRef((props, ref) => {
|
||||
})
|
||||
const [titleIsOverflown, setTitleIsOverflown] = React.useState(false)
|
||||
|
||||
const [currentTime, setCurrentTime] = React.useState(0)
|
||||
const [trackDuration, setTrackDuration] = React.useState(0)
|
||||
const [draggingTime, setDraggingTime] = React.useState(false)
|
||||
const [currentDragWidth, setCurrentDragWidth] = React.useState(0)
|
||||
const [syncInterval, setSyncInterval] = React.useState(null)
|
||||
|
||||
async function onDragEnd(seekTime) {
|
||||
setDraggingTime(false)
|
||||
|
||||
app.cores.player.controls.seek(seekTime)
|
||||
|
||||
syncPlayback()
|
||||
}
|
||||
|
||||
async function syncPlayback() {
|
||||
if (!playerState.track_manifest) {
|
||||
return false
|
||||
}
|
||||
|
||||
const currentTrackTime = app.cores.player.controls.seek()
|
||||
|
||||
setCurrentTime(currentTrackTime)
|
||||
}
|
||||
|
||||
//* Handle when playback status change
|
||||
React.useEffect(() => {
|
||||
if (playerState.playback_status === "playing") {
|
||||
setSyncInterval(setInterval(syncPlayback, 1000))
|
||||
} else {
|
||||
if (syncInterval) {
|
||||
clearInterval(syncInterval)
|
||||
}
|
||||
}
|
||||
}, [playerState.playback_status])
|
||||
|
||||
React.useEffect(() => {
|
||||
setTitleIsOverflown(isOverflown(titleRef.current))
|
||||
setTrackDuration(app.cores.player.controls.duration())
|
||||
}, [playerState.track_manifest])
|
||||
|
||||
React.useEffect(() => {
|
||||
syncPlayback()
|
||||
}, [])
|
||||
|
||||
const isStopped = playerState.playback_status === "stopped"
|
||||
if (playerState.playback_status === "stopped") {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -101,12 +63,7 @@ const PlayerController = React.forwardRef((props, ref) => {
|
||||
},
|
||||
)}
|
||||
>
|
||||
{playerState.playback_status === "stopped" ||
|
||||
(!playerState.track_manifest?.title &&
|
||||
"Nothing is playing")}
|
||||
|
||||
{playerState.playback_status !== "stopped" &&
|
||||
playerState.track_manifest?.title}
|
||||
{playerState.track_manifest?.title}
|
||||
</h4>
|
||||
}
|
||||
|
||||
@ -115,17 +72,11 @@ const PlayerController = React.forwardRef((props, ref) => {
|
||||
//gradient
|
||||
//gradientColor={bgColor}
|
||||
//gradientWidth={20}
|
||||
play={!isStopped}
|
||||
play={playerState.playback_status === "playing"}
|
||||
>
|
||||
<h4>
|
||||
{isStopped ? (
|
||||
"Nothing is playing"
|
||||
) : (
|
||||
<>
|
||||
{playerState.track_manifest
|
||||
?.title ?? "Untitled"}
|
||||
</>
|
||||
)}
|
||||
{playerState.track_manifest?.title ??
|
||||
"Untitled"}
|
||||
</h4>
|
||||
</Marquee>
|
||||
)}
|
||||
@ -146,41 +97,13 @@ const PlayerController = React.forwardRef((props, ref) => {
|
||||
|
||||
{!playerState.live && <SeekBar />}
|
||||
|
||||
<div className="lyrics-player-controller-tags">
|
||||
{playerState.track_manifest?.metadata?.lossless && (
|
||||
<Tag
|
||||
icon={
|
||||
<Icons.Lossless
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
<Indicators
|
||||
track={playerState.track_manifest}
|
||||
playerState={playerState}
|
||||
/>
|
||||
}
|
||||
bordered={false}
|
||||
/>
|
||||
)}
|
||||
{playerState.track_manifest?.explicit && (
|
||||
<Tag bordered={false}>Explicit</Tag>
|
||||
)}
|
||||
{props.lyrics?.sync_audio_at && (
|
||||
<Tag bordered={false} icon={<Icons.TbMovie />}>
|
||||
Video
|
||||
</Tag>
|
||||
)}
|
||||
{props.lyrics?.available_langs?.length > 1 && (
|
||||
<Button
|
||||
icon={<Icons.MdTranslate />}
|
||||
type={
|
||||
props.translationEnabled ? "primary" : "default"
|
||||
}
|
||||
onClick={() => props.toggleTranslationEnabled()}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default PlayerController
|
||||
|
@ -4,6 +4,7 @@ import { motion, AnimatePresence } from "motion/react"
|
||||
|
||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||
|
||||
// eslint-disable-next-line
|
||||
const LyricsText = React.forwardRef((props, textRef) => {
|
||||
const [playerState] = usePlayerStateContext()
|
||||
|
||||
@ -74,6 +75,7 @@ const LyricsText = React.forwardRef((props, textRef) => {
|
||||
} else {
|
||||
setVisible(true)
|
||||
|
||||
if (textRef.current) {
|
||||
// find line element by id
|
||||
const lineElement = textRef.current.querySelector(
|
||||
`#lyrics-line-${currentLineIndex}`,
|
||||
@ -90,6 +92,7 @@ const LyricsText = React.forwardRef((props, textRef) => {
|
||||
textRef.current.scrollTop = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [currentLineIndex])
|
||||
|
||||
//* Handle when playback status change
|
||||
|
@ -1,171 +1,61 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react"
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
|
||||
import parseTimeToMs from "@utils/parseTimeToMs"
|
||||
import useFullScreen from "@hooks/useFullScreen"
|
||||
import useSyncRoom from "@hooks/useSyncRoom"
|
||||
import useCoverAnalysis from "@hooks/useCoverAnalysis"
|
||||
import useLyrics from "@hooks/useLyrics"
|
||||
import useMaxScreen from "@hooks/useMaxScreen"
|
||||
import useTrackManifest from "@hooks/useTrackManifest"
|
||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||
|
||||
import PlayerController from "./components/controller"
|
||||
import LyricsVideo from "./components/video"
|
||||
import LyricsText from "./components/text"
|
||||
import Background from "./components/Background"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const getDominantColorStr = (analysis) => {
|
||||
if (!analysis) return "0,0,0"
|
||||
return analysis.value?.join(", ") || "0,0,0"
|
||||
}
|
||||
|
||||
const toggleFullScreen = (to) => {
|
||||
const targetState = to ?? !document.fullscreenElement
|
||||
|
||||
try {
|
||||
if (targetState) {
|
||||
document.documentElement.requestFullscreen()
|
||||
} else if (document.fullscreenElement) {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Fullscreen toggle failed:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const EnhancedLyricsPage = () => {
|
||||
useMaxScreen()
|
||||
|
||||
const [playerState] = usePlayerStateContext()
|
||||
const [trackManifest, setTrackManifest] = React.useState(null)
|
||||
const [lyrics, setLyrics] = React.useState(null)
|
||||
const [translationEnabled, setTranslationEnabled] = React.useState(false)
|
||||
const [coverAnalysis, setCoverAnalysis] = React.useState(null)
|
||||
|
||||
const videoRef = useRef()
|
||||
const textRef = useRef()
|
||||
const isMounted = useRef(true)
|
||||
const currentTrackId = useRef(null)
|
||||
const videoRef = React.useRef()
|
||||
const textRef = React.useRef()
|
||||
|
||||
const dominantColor = useMemo(
|
||||
() => ({ "--dominant-color": getDominantColorStr(coverAnalysis) }),
|
||||
[coverAnalysis],
|
||||
)
|
||||
|
||||
const handleFullScreenChange = useCallback(() => {
|
||||
if (!document.fullscreenElement && app?.location?.last) {
|
||||
app.location.back()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const loadCurrentTrackLyrics = useCallback(async () => {
|
||||
if (!playerState.track_manifest) return
|
||||
|
||||
const instance = app.cores.player.track()
|
||||
if (!instance) return
|
||||
|
||||
try {
|
||||
const result =
|
||||
await instance.manifest.serviceOperations.fetchLyrics({
|
||||
preferTranslation: translationEnabled,
|
||||
const { toggleFullScreen } = useFullScreen({
|
||||
onExit: () => app?.location?.last && app.location.back(),
|
||||
})
|
||||
|
||||
if (!isMounted.current) return
|
||||
const { trackManifest } = useTrackManifest(playerState.track_manifest)
|
||||
|
||||
const processedLyrics =
|
||||
result.sync_audio_at && !result.sync_audio_at_ms
|
||||
? {
|
||||
...result,
|
||||
sync_audio_at_ms: parseTimeToMs(
|
||||
result.sync_audio_at,
|
||||
),
|
||||
}
|
||||
: result
|
||||
const { dominantColor } = useCoverAnalysis(trackManifest)
|
||||
|
||||
console.log("Fetched Lyrics >", processedLyrics)
|
||||
setLyrics(processedLyrics || false)
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch lyrics", error)
|
||||
setLyrics(false)
|
||||
}
|
||||
}, [translationEnabled, playerState.track_manifest])
|
||||
const { syncRoom, subscribeLyricsUpdates, unsubscribeLyricsUpdates } =
|
||||
useSyncRoom()
|
||||
|
||||
// Track manifest comparison
|
||||
useEffect(() => {
|
||||
const newManifest = playerState.track_manifest
|
||||
const { lyrics, setLyrics } = useLyrics({
|
||||
trackManifest,
|
||||
})
|
||||
|
||||
if (JSON.stringify(newManifest) !== JSON.stringify(trackManifest)) {
|
||||
setTrackManifest(newManifest)
|
||||
}
|
||||
}, [playerState.track_manifest])
|
||||
|
||||
// Lyrics loading trigger
|
||||
useEffect(() => {
|
||||
if (!trackManifest) {
|
||||
setLyrics(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (!lyrics || lyrics.track_id !== trackManifest._id) {
|
||||
loadCurrentTrackLyrics()
|
||||
}
|
||||
}, [trackManifest, lyrics?.track_id])
|
||||
|
||||
// Cover analysis
|
||||
useEffect(() => {
|
||||
const getCoverAnalysis = async () => {
|
||||
const trackInstance = app.cores.player.track()
|
||||
if (!trackInstance?.manifest.analyzeCoverColor) return
|
||||
|
||||
try {
|
||||
const analysis =
|
||||
await trackInstance.manifest.analyzeCoverColor()
|
||||
if (isMounted.current) setCoverAnalysis(analysis)
|
||||
} catch (error) {
|
||||
console.error("Failed to get cover analysis", error)
|
||||
}
|
||||
}
|
||||
|
||||
if (playerState.track_manifest) {
|
||||
getCoverAnalysis()
|
||||
}
|
||||
}, [playerState.track_manifest])
|
||||
|
||||
// Initialization and cleanup
|
||||
useEffect(() => {
|
||||
isMounted.current = true
|
||||
// Inicialización y limpieza
|
||||
React.useEffect(() => {
|
||||
toggleFullScreen(true)
|
||||
document.addEventListener("fullscreenchange", handleFullScreenChange)
|
||||
|
||||
if (syncRoom) {
|
||||
subscribeLyricsUpdates(setLyrics)
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMounted.current = false
|
||||
toggleFullScreen(false)
|
||||
document.removeEventListener(
|
||||
"fullscreenchange",
|
||||
handleFullScreenChange,
|
||||
)
|
||||
|
||||
if (syncRoom) {
|
||||
unsubscribeLyricsUpdates(setLyrics)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Translation toggler
|
||||
const handleTranslationToggle = useCallback(
|
||||
(to) => setTranslationEnabled((prev) => to ?? !prev),
|
||||
[],
|
||||
)
|
||||
|
||||
// Memoized background component
|
||||
const renderBackground = useMemo(() => {
|
||||
if (!playerState.track_manifest || lyrics?.video_source) return null
|
||||
|
||||
return (
|
||||
<div className="lyrics-background-wrapper">
|
||||
<div className="lyrics-background-cover">
|
||||
<img
|
||||
src={playerState.track_manifest.cover}
|
||||
alt="Album cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [playerState.track_manifest, lyrics?.video_source])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames("lyrics", {
|
||||
@ -175,15 +65,21 @@ const EnhancedLyricsPage = () => {
|
||||
>
|
||||
<div className="lyrics-background-color" />
|
||||
|
||||
{renderBackground}
|
||||
{playerState.playback_status === "stopped" && (
|
||||
<div className="lyrics-stopped-decorator">
|
||||
<img src="./basic_alt.svg" alt="Basic Logo" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Background
|
||||
trackManifest={trackManifest}
|
||||
hasVideoSource={!!lyrics?.video_source}
|
||||
/>
|
||||
|
||||
<LyricsVideo ref={videoRef} lyrics={lyrics} />
|
||||
<LyricsText ref={textRef} lyrics={lyrics} />
|
||||
<PlayerController
|
||||
lyrics={lyrics}
|
||||
translationEnabled={translationEnabled}
|
||||
toggleTranslationEnabled={handleTranslationToggle}
|
||||
/>
|
||||
|
||||
<PlayerController lyrics={lyrics} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -15,10 +15,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lyrics-stopped-decorator {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
padding: 35vh;
|
||||
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.9);
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.lyrics-background-color {
|
||||
position: absolute;
|
||||
|
||||
z-index: 100;
|
||||
z-index: 105;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -170,11 +194,6 @@
|
||||
opacity: 1;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.lyrics-player-controller-tags {
|
||||
opacity: 1;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.lyrics-player-controller-info {
|
||||
@ -187,6 +206,8 @@
|
||||
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.lyrics-player-controller-info-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
@ -219,6 +240,7 @@
|
||||
.lyrics-player-controller-info-details {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
|
||||
align-items: center;
|
||||
|
||||
@ -229,7 +251,7 @@
|
||||
font-weight: 400;
|
||||
|
||||
// do not wrap text
|
||||
white-space: nowrap;
|
||||
word-break: break-word;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
@ -243,22 +265,24 @@
|
||||
transition: all 150ms ease-in-out;
|
||||
}
|
||||
|
||||
.lyrics-player-controller-tags {
|
||||
.toolbar_player_indicators_wrapper {
|
||||
position: absolute;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
padding: 4px;
|
||||
|
||||
gap: 10px;
|
||||
.toolbar_player_indicators {
|
||||
padding: 4px 6px;
|
||||
font-size: 0.9rem;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: all 150ms ease-in-out;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,19 @@ import MusicService from "@models/music"
|
||||
import "./index.less"
|
||||
|
||||
const ListView = (props) => {
|
||||
const { type, id } = props.params
|
||||
const { id } = props.params
|
||||
|
||||
const [loading, result, error, makeRequest] = app.cores.api.useRequest(
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
const type = query.get("type")
|
||||
const service = query.get("service")
|
||||
|
||||
const [loading, result, error] = app.cores.api.useRequest(
|
||||
MusicService.getReleaseData,
|
||||
id,
|
||||
{
|
||||
type: type,
|
||||
service: service,
|
||||
},
|
||||
)
|
||||
|
||||
if (error) {
|
||||
@ -29,6 +37,8 @@ const ListView = (props) => {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
console.log(result)
|
||||
|
||||
return (
|
||||
<PlaylistView
|
||||
playlist={result}
|
||||
|
@ -10,7 +10,11 @@ const MusicNavbar = React.forwardRef((props, ref) => {
|
||||
useUrlQuery
|
||||
renderResults={false}
|
||||
model={async (keywords, params) =>
|
||||
SearchModel.search(keywords, params, ["tracks"])
|
||||
SearchModel.search(keywords, params, [
|
||||
"tracks",
|
||||
"albums",
|
||||
"artists",
|
||||
])
|
||||
}
|
||||
onSearchResult={props.setSearchResults}
|
||||
onEmpty={() => props.setSearchResults(false)}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user