UNPKG

kashi

Version:

Singing at the top of my lungs

298 lines (221 loc) โ€ข 11.2 kB
<div align="center"> <a href="https://gitlab.com/lucasmc64/kashi">GitLab</a> โ€ข <a href="https://www.npmjs.com/package/kashi">NPM</a> โ€ข <a href="https://unpkg.com/browse/kashi@latest">CDN</a> โ€ข <a href="https://ko-fi.com/lucasmc64">Ko-fi</a> </div> ![Kashi](https://gitlab.com/lucasmc64/kashi/-/raw/main/readme/cover.png) # ๐ŸŽค Kashi ## ๐ŸŽฏ Goal This project is a dependency-free library that aims to provide a way to correctly read, format, structure, and display song lyrics. ## ๐Ÿ“œ Features - [x] ๐ŸŽต Process and list song lyrics in .lrc files - [x] ๐Ÿ’ช Easily integrates with React and other frameworks - [x] โœ‰๏ธ Implements the Observer pattern and emits events at each step of the process whenever something changes - [x] โœ๏ธ Allows you to enter custom text when the lyrics line is empty - [x] ๐ŸŽค Synchronizes the lyrics with the music that is playing - [x] ๐ŸŽฉ Supports multiple lyrics for the same song (useful to keep track of the original lyrics and their translation) - [ ] ๐Ÿงž Supports the [Walaoke extension](<https://en.wikipedia.org/wiki/LRC_(file_format)#Walaoke_extension>) - [ ] ๐Ÿ•–๏ธ Supports the [A2 extension](<https://en.wikipedia.org/wiki/LRC_(file_format)#A2_extension_(Enhanced_LRC_format)>) > **Note:** Support for fetching lyrics using a URL has been removed because it only supported public URLs. > > Therefore, instead of refactoring to handle requests to private APIs, I opted to support only files. If a request is necessary, you can make it externally and then pass the returned file to Kashi! ## ๐ŸŽจ How to use it in my project? ### <span id="classic-scripts">๐Ÿ™Œ Classic scripts</span> The project is also exported using [UMD](https://github.com/umdjs/umd), which means that all variables and classes such as the `Kashi` class is exposed globally, and can be used in any script imported after the library. > **Note**: You should **change `X.Y.Z` to the library version number** according to your needs, this is just an example. It is recommended to always use the latest version. > > It is recommended that you pin to the latest stable version of the lib. However, if you always want to use the latest version, you can also use `latest` instead of a predefined version number. **Be careful** though, breaking changes may be introduced and your project may need to adapt to the changes. ```html <!DOCTYPE html> <html> <head> <!-- ... --> <script src="https://unpkg.com/kashi@X.Y.Z/kashi.js" defer></script> <!-- ... --> </head> <body> <div id="kashi"></div> </body> </html> ``` ```js "use strict"; new Kashi({ files. // Loaded from some input[type="file"] or anywhere else container: document.getElementById("kashi"), }); ``` ### ๐Ÿ“ฆ๏ธ ES6 Modules The HTML is very similar to the classic scripts version, just change the `.js` extension to `.mjs` (and maybe will be necessary to adds a `type="module"` too), which will result in something similar to this: > **Note**: Please read the [previous section](#classic-scripts) to get the details involved in importing and choosing a package version, they are the same here. ```html <script src="https://unpkg.com/kashi@X.Y.Z/kashi.mjs" type="module" defer ></script> ``` ```javascript import { Kashi } from "kashi"; // Usage is essentially the same as in the previous section new Kashi({ files. // Loaded from some input[type="file"] or anywhere else container: document.getElementById("kashi"), }); ``` ### โš›๏ธ React Install the lib using your favorite package manager. ```bash npm install kashi ``` Since the library was designed primarily to be used with vanilla JS, a _helper_ component needs to be created to encapsulate Kashi's behavior and make it simple to reuse throughout the application. > **Note**: There is TypeScript support, and even if your project doesn't use the JS superset, it should help VSCode and other editors provide autocomplete/code suggestions. ```tsx import { memo, useEffect, useRef } from "react"; import { Kashi, KashiProps } from "kashi"; import { api } from "@/services/axios"; // Example using Vite, React and TypeScript export const KashiWrapper = memo( (props: Omit<KashiProps, "container"> & { url: string }) => { const ref = useRef<HTMLDivElement>(null); useEffect(() => { async function loadKashi() { if (!ref.current) { return; } if (!props.url) { ref.current.innerHTML = ""; return; } try { const { url, ...rest } = props; const response = await api.get(url, { responseType: "blob" }); new Kashi({ ...rest, files: [response.data], container: ref.current, }); } catch (error) { console.error("Error loading Kashi:", error); } } loadKashi(); return () => { // Required to avoid duplication when React is in Strict Mode if (ref.current) { ref.current.innerHTML = ""; } }; }, [ref.current, props]); return <div className="kashi-wrapper" ref={ref} />; }, (prevProps, nextProps) => { function compareArrays(arr1: unknown[], arr2: unknown[]) { if (arr1.length !== arr2.length) { return false; } return arr1.every((item, index) => item === arr2[index]); } function compareObjects( obj1: Omit<KashiProps, "container">, obj2: Omit<KashiProps, "container">, ) { const keys1 = Object.keys(obj1) as Array< keyof Omit<KashiProps, "container"> >; const keys2 = Object.keys(obj2) as Array< keyof Omit<KashiProps, "container"> >; if (keys1.length !== keys2.length) { return false; } for (const key of keys1) { const val1 = obj1[key]; const val2 = obj2[key]; if (Array.isArray(val1) && Array.isArray(val2)) { if (!compareArrays(val1, val2)) { return false; } } else if (val1 !== val2) { return false; } } return true; } return compareObjects(prevProps, nextProps); }, ); ``` ## ๐Ÿง Constructor properties You must pass some properties to Kashi to define what lyrics display and where. Here are its specifications: | Property | Type | Default value | Is required? | Description | | --------------- | ---------------- | ------------- | ------------ | ----------------------------------------- | | `files` | `Blob[]` | - | Yes | Lyrics files | | `container` | `HTMLDivElement` | - | Yes | Element where the lyrics will be inserted | | `emptyLineText` | `string` | `...` | No | Custom text for empty lines of the lyrics | | `noLyricsText` | `string` | - | No | Custom text for when there are no lyrics | ## ๐Ÿ‘พ Generated HTML structure The `div#kashi` represents the `container` passed to `Kashi` where the song lyrics will be inserted. Each line of lyrics present in the lrc files will be wrapped by a `<div></div>` tag and inserted into the `container`. Here's an example: ```html <div id="kashi"> <div data-time="00:17.55" data-ms-time="17550" data-empty="false" data-aria-current="false" > <span>Telling myself, "I won't go there"</span> <br /> <span> Dizendo a mim mesmo, "eu nรฃo vou lรก" </span> </div> <div data-time="00:21.24" data-ms-time="21240" data-empty="false" data-aria-current="false" > <span>Oh, but I know that I won't care</span> <br /> <span>Oh, mas eu sei que nรฃo vou me importar</span> </div> <!-- ... --> </div> ``` ## ๐Ÿ“‚ Methods and attributes The instance generated by `Kashi` has some public methods and attributes that can be used to query or change properties on the fly. | Name | Type | Description | | ------------------ | --------- | -------------------------------------------------------------------------------------- | | `files` | Attribute | Returns the files from the current lyrics | | `emptyLineText` | Attribute | Returns the text set for empty lines | | `noLyricsText` | Attribute | Returns the text set for when there are no lyrics | | `setFiles` | Method | Function capable of changing the current lyrics files by passing the the new **files** | | `setEmptyLineText` | Method | Function capable of changing the text defined for empty lines | | `setNoLyricsText` | Method | Function capable of changing the text defined for when there are no lyrics | | `subscribe` | Method | Function capable of defining a callback to be executed when a given event is triggered | | `unsubscribe` | Method | Function capable of making a callback to stop listening to an event | | `notify` | Method | Function capable of triggering an event | ## ๐Ÿพ Events When creating a new instance using `Kashi` you will have access to the `subscribe`, `unsubscribe` and `notify` methods, these methods can be used respectively to listen for an event, stop listening for an event and manually trigger an event. Below is the list of events triggered internally: | Event | Data | Trigger | | ------------------- | --------------------------- | ------------------------------------------ | | `filesSet` | `{ files: Blob[] }` | When calling the `setFiles` method | | `emptyLineTextSet` | `{ emptyLineText: string }` | When calling the `setEmptyLineText` method | | `noLyricsTextSet` | `{ noLyricsText: text }` | When calling the `setNoLyricsText` method | | `lyricLinesUpdated` | `{ lyricLines: string[] }` | When inserting/updating lyrics in HTML | ## ๐Ÿค” How do I run the project on my machine? The first step is to clone the project, either via terminal or even by downloading the compressed file (.zip). After that, go ahead. ### ๐Ÿ› ๏ธ Requirements - [NodeJS and NPM](https://nodejs.org) ### โœจ Running the project With the dependencies properly installed, still in the terminal, run `npm start`. Create a simple demo project using vanilla HTML/JS and use the files in the `dist` folder for testing. You can also create a demo project using React and use [npm link](https://docs.npmjs.com/cli/v9/commands/npm-link). ### ๐ŸŽ‰ If everything went well... Now you are running the project beautifully! ## โœ๏ธ License This project is under the GPL v3 license. See the [LICENSE](https://gitlab.com/lucasmc64/kashi/-/blob/main/LICENSE) for more information. --- Made with ๐Ÿ’™ by lucasmc64 ๐Ÿ‘‹ [Get in touch!](https://www.linkedin.com/in/lucasmc64/)