Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions i18n.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* This code snippet exports translations from two different locales (en and de) stored in separate JSON files.
* It also exports the defaultLocale as 'en'.
*/
import en from '@/locales/en.json';
import de from '@/locales/de.json';

Expand Down
227 changes: 225 additions & 2 deletions src/common/bridge.js

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions src/common/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import browser from 'webextension-polyfill';
import { storageGetUser } from '~/entries/background/common/storage';
import { getApiUrl } from '~/config';

/**
* Returns the name of the browser based on the user agent string.
* If the browser is not recognized, it returns 'unknown'.
*
* @returns {string} The name of the browser.
*/
function getBrowser() {
if (!navigator?.userAgent) {
return null;
Expand All @@ -23,6 +29,11 @@ function getBrowser() {
return 'unknown';
}

/**
* Asynchronous function that retrieves the user data.
*
* @returns {Promise} A promise that resolves with the user data if available, otherwise null.
*/
async function getUser() {
if (typeof window !== 'undefined' && window.streamfinityUser) {
return window.streamfinityUser;
Expand Down Expand Up @@ -172,6 +183,14 @@ const log = {
},
};

/**
* Creates a logger instance with the specified section and options.
*
* @param {string} section - The section name for the logger.
* @param {Object} options - Additional options for the logger.
* @param {Function} options.forwardCallback - A callback function to forward log messages.
* @returns {Object} A logger instance with the specified section and options.
*/
export function createLogger(section, options = {}) {
const logger = { ...log };
logger.section = section;
Expand Down
87 changes: 87 additions & 0 deletions src/common/pretty.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
import moment from 'moment';
import { createLogger } from '~/common/log';

/**
* Format a number into a pretty representation with the 'en-US' locale.
*
* @param {number} number - The number to be formatted.
* @returns {string} The formatted number as a string.
*/
export function prettyNumber(number) {
return new Intl.NumberFormat('en-US').format(number);
}

/**
* Format a number into a compact representation with the 'en-US' locale.
*
* @param {number} number - The number to be formatted.
* @returns {string} The compact formatted number as a string.
*/
export function prettyShortNumber(number) {
return new Intl.NumberFormat('en-US', { notation: 'compact' }).format(number);
}

/**
* Format a number into a price representation with the 'en-US' locale.
*
* @param {number} number - The number to be formatted as a price.
* @returns {string} The formatted price as a string with 2 decimal places.
*/
export function prettyPrice(number) {
return new Intl.NumberFormat('en-US', {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
}).format(number);
}

/**
* Format a number into a currency representation with the specified currency and the 'en-US' locale.
*
* @param {number} number - The number to be formatted as currency.
* @param {string} [currency='EUR'] - The currency code to be used for formatting (default is 'EUR').
* @returns {string} The formatted currency as a string with 2 decimal places.
*/
export function prettyCurrency(number, currency = 'EUR') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
Expand All @@ -25,6 +50,15 @@ export function prettyCurrency(number, currency = 'EUR') {
}).format(number);
}

/**
* Format a duration in seconds into a human-readable time format.
*
* If the duration is greater than or equal to 1 hour (3600 seconds), it will be formatted as 'hh:mm:ss'.
* Otherwise, it will be formatted as 'mm:ss'.
*
* @param {number} seconds - The duration in seconds to be formatted.
* @returns {string} The formatted duration as a string in the 'hh:mm:ss' or 'mm:ss' format.
*/
export function prettyDuration(seconds) {
if (seconds >= 3600) {
return moment.unix(seconds).format('hh:mm:ss');
Expand All @@ -33,10 +67,23 @@ export function prettyDuration(seconds) {
return moment.unix(seconds).format('mm:ss');
}

/**
* Capitalizes the first letter of a given string.
*
* @param {string} string - The input string to capitalize.
* @returns {string} The input string with the first letter capitalized.
*/
export function ucfirst(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

/**
* Limit the length of a text by a specified count.
*
* @param {string} text - The text to be limited.
* @param {number} [count=25] - The maximum number of characters to keep in the text (default is 25).
* @returns {string} The limited text with ellipsis (...) if it exceeds the count.
*/
export function strLimit(text, count = 25) {
const parts = [...text];

Expand All @@ -47,6 +94,13 @@ export function strLimit(text, count = 25) {
return text;
}

/**
* Generate a slug from the given text by converting it to lowercase, removing diacritics, trimming, replacing spaces with hyphens,
* removing non-word characters except hyphens, and collapsing multiple hyphens into a single one.
*
* @param {string} text - The text to generate a slug from.
* @returns {string} The generated slug from the text.
*/
export function strSlug(text) {
return text
.toString()
Expand All @@ -59,6 +113,15 @@ export function strSlug(text) {
.replace(/--+/g, '-');
}

/**
* Convert a duration in the format 'mm:ss' or 'hh:mm:ss' to seconds.
*
* If the duration is in the format 'mm:ss', it will be converted directly to seconds.
* If the duration is in the format 'hh:mm:ss', it will be converted to total seconds.
*
* @param {string} duration - The duration string in the format 'mm:ss' or 'hh:mm:ss'.
* @returns {number} The total duration in seconds.
*/
export function durationToSeconds(duration) {
let modifiedDuration = duration;
// is in format "00:00", moment assumes that last segment is minutes
Expand All @@ -71,6 +134,13 @@ export function durationToSeconds(duration) {

const log = createLogger('why');

/**
* Function: why
* Description: This function takes an error object as input and extracts the error message from it. If the error is a string, it directly assigns it as the message. If the error is an object, it checks for various properties like 'message', 'response.data', 'response._data', 'data.errors', 'data.messages', 'data.error', and 'data.message' to determine the message. If the error is neither a string nor an object, it converts it to a string and assigns it as the message. Finally, it returns the extracted message.
*
* @param {any} e - The error object from which to extract the message
* @returns {string} - The extracted error message
*/
export function why(e) {
let message;

Expand Down Expand Up @@ -103,6 +173,14 @@ export function why(e) {
return message;
}

/**
* Function: getVideoUrlWithTimestamp
* Description: This function generates a URL for a video with an optional timestamp parameter. If the timestamp is provided, it appends it to the video's external tracking URL as a query parameter. If no timestamp is provided, it returns the video's external tracking URL as is.
*
* @param {Object} video - The video object for which the URL is generated.
* @param {number} ts - The optional timestamp in seconds to be added as a query parameter (default is null).
* @returns {string} - The URL of the video with an optional timestamp query parameter.
*/
function getVideoUrlWithTimestamp(video, ts) {
if (!ts) {
return video.external_tracking_url;
Expand All @@ -111,6 +189,15 @@ function getVideoUrlWithTimestamp(video, ts) {
return `${video.external_tracking_url}?t=${ts}`;
}

/**
* Function that builds a reaction URL based on the provided reaction object.
* If the reaction is from a video, it appends the timestamp to the video URL.
* If the reaction is from a stream, it finds the corresponding video and appends the timestamp.
* If none of the above, it returns the service external URL from the reaction info.
*
* @param {Object} reaction - The reaction object containing information about the reaction source.
* @returns {string} The URL with timestamp or service external URL based on the reaction type.
*/
export function buildReactionFromUrl(reaction) {
if (reaction.from_video) {
return getVideoUrlWithTimestamp(reaction.from_video, reaction.video_seconds_from);
Expand Down
86 changes: 78 additions & 8 deletions src/common/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,32 @@ import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { why } from '~/common/pretty';

/**
* Combines multiple class names into a single string using Tailwind CSS utility classes.
*
* @param {...string} inputs - The class names to be combined.
* @returns {string} - The combined class names string.
*/
export function cn(...inputs) {
return twMerge(clsx(inputs));
}

/**
* Builds the frontend URL by combining the VITE_FRONTEND_URL environment variable with the provided path.
*
* @param {string} path - The path to be appended to the frontend URL.
* @returns {string} - The complete frontend URL.
*/
export function buildFrontendUrl(path) {
return `${import.meta.env.VITE_FRONTEND_URL}${path}`;
}

/**
* Extracts the video ID from a YouTube video link.
*
* @param {string} link - The YouTube video link from which to extract the video ID.
* @returns {string|null} - The extracted video ID if found, otherwise null.
*/
export function getIdFromLink(link) {
const match = link.match(/watch(.*)v=(?<id>[A-Za-z0-9\-_]+)/);

Expand All @@ -23,10 +41,12 @@ export function getIdFromLink(link) {
}

/**
* @param callback
* @param {number} intervalMs
* @param {number} maxTries
* @returns {(Promise<HTMLElement>|function)[]}
* Retries a callback function until a condition is met or a maximum number of tries is reached, clearing the interval on success or failure.
*
* @param {Function} callback - The callback function to be executed on each interval.
* @param {number} [intervalMs=300] - The interval in milliseconds between each callback execution.
* @param {number} [maxTries=300] - The maximum number of tries before rejecting the promise.
* @returns {[Promise<unknown>, Function]} - A promise that resolves when the condition is met, and a function to manually clear the interval.
*/
export function retryFindWithClearFn(callback, intervalMs = 300, maxTries = 300) {
let intervalId;
Expand Down Expand Up @@ -60,10 +80,12 @@ export function retryFindWithClearFn(callback, intervalMs = 300, maxTries = 300)
}

/**
* @param callback
* @param {number} intervalMs
* @param {number} maxTries
* @returns {Promise<HTMLElement>}
* Retry finding an element using the provided callback function until the element is found or the maximum number of tries is reached.
*
* @param {Function} callback - The callback function to be executed to find the element.
* @param {number} [intervalMs=300] - The interval in milliseconds between each try.
* @param {number} [maxTries=300] - The maximum number of tries before giving up.
* @returns {Promise} A promise that resolves with the found element or rejects if the element is not found within the maximum tries.
*/
export async function retryFind(callback, intervalMs = 300, maxTries = 300) {
const [findFn] = retryFindWithClearFn(callback, intervalMs, maxTries);
Expand All @@ -74,14 +96,29 @@ export async function retryFind(callback, intervalMs = 300, maxTries = 300) {

// Get elements on page

/**
* Returns the YouTube player element on the page.
*
* @returns {Element|null} The YouTube player element if found, otherwise null.
*/
export function getYouTubePlayer() {
return document.querySelector('video.video-stream.html5-main-video');
}

/**
* Returns the YouTube player progress bar element on the page.
*
* @returns {Element|null} The YouTube player progress bar element if found, otherwise null.
*/
export function getYouTubePlayerProgressBar() {
return document.querySelector('.ytp-progress-bar-container .ytp-progress-bar .ytp-timed-markers-container');
}

/**
* Returns the publish date of the current video element on the page.
*
* @returns {moment|null} The publish date of the current video as a moment object if found, otherwise null.
*/
export function getCurrentVideoPublishDate() {
const metaPublish = document.querySelector('meta[itemprop="datePublished"]');
if (metaPublish) {
Expand All @@ -93,6 +130,13 @@ export function getCurrentVideoPublishDate() {

// Retry find elements on page

/**
* Find the YouTube player element by retrying for a specified number of times at a given interval.
*
* @param {number} interval - The interval in milliseconds between each retry (default is 300ms).
* @param {number} maxTries - The maximum number of retries before giving up (default is 300).
* @returns {Promise<Element>} - A promise that resolves with the found YouTube player element, or rejects if not found.
*/
export function findYouTubePlayer(interval = 300, maxTries = 300) {
return retryFind(
() => getYouTubePlayer(),
Expand All @@ -101,6 +145,13 @@ export function findYouTubePlayer(interval = 300, maxTries = 300) {
);
}

/**
* Find the video player bar element by querying the DOM with the specified interval and maximum number of tries.
*
* @param {number} interval - The interval in milliseconds to wait between each try.
* @param {number} maxTries - The maximum number of tries before giving up.
* @returns {Array<Function>} An array containing an async function that returns a promise to find the element and a function to clear the interval.
*/
export function findVideoPlayerBar(interval = 300, maxTries = 300) {
return retryFindWithClearFn(
() => document.querySelector('.ytp-progress-bar-container .ytp-progress-bar .ytp-progress-list'),
Expand All @@ -111,12 +162,24 @@ export function findVideoPlayerBar(interval = 300, maxTries = 300) {

// Toaster

/**
* Displays a success toast message using the provided message and options.
*
* @param {string} message - The message to be displayed in the toast.
* @param {object} [options] - The options for customizing the toast display (optional).
*/
export function toastSuccess(message, options) {
toast.success(message, {
...(options || {}),
});
}

/**
* Displays a warning toast message using the provided warning message and options.
*
* @param {string} warning - The warning message to be displayed in the toast.
* @param {object} [options] - The options for customizing the toast display (optional).
*/
export function toastWarn(warning, options) {
toast(warning, {
...options || {},
Expand All @@ -126,6 +189,13 @@ export function toastWarn(warning, options) {
});
}

/**
* Display an error toast message using react-hot-toast with a custom error message generated by the 'why' function.
*
* @param {any} error - The error object or message to display in the toast.
* @param {object} options - Additional options for customizing the toast display (optional).
* @returns {void}
*/
export function toastError(error, options) {
toast.error(() => why(error), {
...(options || {}),
Expand Down
8 changes: 8 additions & 0 deletions src/components/Icons/PlusSparklesIcon.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';

/**
* Function component that renders a SVG icon of PlusSparkles.
*
* @param {string} className - Additional CSS class for styling.
* @param {number} width - The width of the SVG icon.
* @param {number} height - The height of the SVG icon.
* @returns {JSX.Element} SVG icon of PlusSparkles.
*/
function PlusSparkles({ className = '', width = 20, height = 20 }) {
return (
<svg
Expand Down
Loading