- {downloading &&
}
-
-
- {props.version.steam &&
}
- {!props.version.steam &&
}
-
-
{props.version.name || props.version.BSVersion}
+ const renderVersionText = () => {
+ if(props.version.name){
+ return (
+
+
+
+ )
+ }
+ return (
+
+ {props.version.BSVersion}
-
- {downloading &&
}
-
-
+ )
+ }
- )
+
+ return (
+
+
+ {renderIcon()}
+ {renderVersionText()}
+
+
+ )
}
\ No newline at end of file
diff --git a/src/renderer/components/nav-bar/nav-bar-items/maps-nav-bar-item.component.tsx b/src/renderer/components/nav-bar/nav-bar-items/maps-nav-bar-item.component.tsx
new file mode 100644
index 00000000..0b366265
--- /dev/null
+++ b/src/renderer/components/nav-bar/nav-bar-items/maps-nav-bar-item.component.tsx
@@ -0,0 +1,24 @@
+import { Link } from "react-router-dom";
+import { BsmIcon } from "renderer/components/svgs/bsm-icon.component";
+import { useObservable } from "renderer/hooks/use-observable.hook";
+import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
+import { PageStateService } from "renderer/services/page-state.service";
+import { NavBarItem } from "./nav-bar-item.component";
+
+export function MapsNavBarItem() {
+
+ const pageState = PageStateService.getInstance();
+
+ const route = useObservable(pageState.route$);
+
+ const color = useThemeColor("first-color");
+
+ return (
+
+
+
+ Maps
+
+
+ )
+}
diff --git a/src/renderer/components/nav-bar/nav-bar-items/nav-bar-item.component.tsx b/src/renderer/components/nav-bar/nav-bar-items/nav-bar-item.component.tsx
new file mode 100644
index 00000000..91a47f11
--- /dev/null
+++ b/src/renderer/components/nav-bar/nav-bar-items/nav-bar-item.component.tsx
@@ -0,0 +1,25 @@
+import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
+import { BsmButton } from "../../shared/bsm-button.component";
+
+type Props = {
+ children: JSX.Element,
+ isDownloading?: boolean
+ progress?: number,
+ isActive?: boolean,
+ onCancel?: (e: React.MouseEvent) => void
+}
+
+export function NavBarItem({progress, isDownloading, children, isActive, onCancel}: Props) {
+
+ const {firstColor, secondColor} = useThemeColor();
+
+ return (
+
+ {isDownloading && }
+
+ {children}
+ {isDownloading && }
+
+
+ )
+}
diff --git a/src/renderer/components/nav-bar/nav-bar-spliter.component.tsx b/src/renderer/components/nav-bar/nav-bar-spliter.component.tsx
new file mode 100644
index 00000000..35521307
--- /dev/null
+++ b/src/renderer/components/nav-bar/nav-bar-spliter.component.tsx
@@ -0,0 +1,5 @@
+export function NavBarSpliter() {
+ return (
+
+ )
+}
diff --git a/src/renderer/components/nav-bar/nav-bar.component.tsx b/src/renderer/components/nav-bar/nav-bar.component.tsx
index 638a155c..fe3421e3 100644
--- a/src/renderer/components/nav-bar/nav-bar.component.tsx
+++ b/src/renderer/components/nav-bar/nav-bar.component.tsx
@@ -1,10 +1,12 @@
import './nav-bar.component.css'
-import { BsVersionItem } from './bs-version-item.component';
+import { BsVersionItem } from './nav-bar-items/bs-version-item.component';
import { BSVersionManagerService } from '../../services/bs-version-manager.service';
import { Link } from 'react-router-dom';
import { BsmIcon } from '../svgs/bsm-icon.component';
import { useObservable } from 'renderer/hooks/use-observable.hook';
import { BsManagerIcon } from './bsmanager-icon.component';
+import { MapsNavBarItem } from './nav-bar-items/maps-nav-bar-item.component';
+import { NavBarSpliter } from './nav-bar-spliter.component';
export function NavBar() {
@@ -13,21 +15,22 @@ export function NavBar() {
const installedVersions = useObservable(bsVersionServoce.installedVersions$);
return (
-
-
-
-
-
+
-
+
)
}
diff --git a/src/renderer/components/notification/notification-item.component.tsx b/src/renderer/components/notification/notification-item.component.tsx
index e80654b0..a0838bd9 100644
--- a/src/renderer/components/notification/notification-item.component.tsx
+++ b/src/renderer/components/notification/notification-item.component.tsx
@@ -1,5 +1,5 @@
import { NotificationResult, NotificationType, Notification } from "renderer/services/notification.service"
-import { motion } from "framer-motion"
+import { motion, PanInfo } from "framer-motion"
import { BsmImage } from "../shared/bsm-image.component";
import BeatRunningImg from "../../../../assets/images/apngs/beat-running.png";
import BeatConflictImg from "../../../../assets/images/apngs/beat-conflict.png";
@@ -10,27 +10,37 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
export function NotificationItem({resolver, notification}: {resolver?: (value: NotificationResult|string) => void, notification: Notification}) {
- const t = useTranslation();
+ const t = useTranslation();
- const renderImage = () => {
- if(notification.type === NotificationType.SUCCESS){ return BeatRunningImg; }
- if(notification.type === NotificationType.WARNING){ return BeatWaitingImg; }
- if(notification.type === NotificationType.ERROR){ return BeatConflictImg; }
- return BeatImpatientImg;
- }
+ const renderImage = (() => {
+ if(notification.type === NotificationType.SUCCESS){ return BeatRunningImg; }
+ if(notification.type === NotificationType.WARNING){ return BeatWaitingImg; }
+ if(notification.type === NotificationType.ERROR){ return BeatConflictImg; }
+ return BeatImpatientImg;
+ })();
- const renderNeonColors = () => {
- if(notification.type === NotificationType.SUCCESS){ return "bg-green-400 shadow-green-400"; }
- if(notification.type === NotificationType.WARNING){ return "bg-yellow-400 shadow-yellow-400"; }
- if(notification.type === NotificationType.ERROR){ return "bg-red-500 shadow-red-500"; }
- return "bg-gray-800 shadow-gray-800 dark:bg-white dark:shadow-white";
- }
+ const renderNeonColors = (() => {
+ if(notification.type === NotificationType.SUCCESS){ return "bg-green-400 shadow-green-400"; }
+ if(notification.type === NotificationType.WARNING){ return "bg-yellow-400 shadow-yellow-400"; }
+ if(notification.type === NotificationType.ERROR){ return "bg-red-500 shadow-red-500"; }
+ return "bg-gray-800 shadow-gray-800 dark:bg-white dark:shadow-white";
+ })();
+
+
+ const handleDragEnd = (e: MouseEvent, info: PanInfo) => {
+ const offset = info.offset.x;
+ const velocity = info.velocity.x;
+
+ if(offset > 30 && velocity > 300){
+ resolver(NotificationResult.CLOSE);
+ }
+ }
return (
-
+
-
+
{t(notification.title)}
{notification.desc &&
{t(notification.desc)}
}
@@ -42,8 +52,8 @@ export function NotificationItem({resolver, notification}: {resolver?: (value: N
)}
-
+
resolver(NotificationResult.CLOSE)}/>
-
+
)
}
diff --git a/src/renderer/components/notification/notification-overlay.component.tsx b/src/renderer/components/notification/notification-overlay.component.tsx
index 0795bd71..f46e29ce 100644
--- a/src/renderer/components/notification/notification-overlay.component.tsx
+++ b/src/renderer/components/notification/notification-overlay.component.tsx
@@ -1,7 +1,7 @@
import { useObservable } from "renderer/hooks/use-observable.hook"
import { NotificationService } from "renderer/services/notification.service"
import { NotificationItem } from "./notification-item.component";
-import { AnimatePresence, motion } from "framer-motion";
+import { AnimatePresence } from "framer-motion";
export function NotificationOverlay() {
@@ -11,10 +11,10 @@ export function NotificationOverlay() {
return (
-
+
{notifications?.map(n => )}
-
+
)
}
diff --git a/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx b/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx
index cc94cffa..d5150484 100644
--- a/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx
+++ b/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx
@@ -9,27 +9,34 @@ export function BsmProgressBar() {
const progressBarService = ProgressBarService.getInstance();
- const progress = useObservable(progressBarService.progression$);
+ const progressData = useObservable(progressBarService.progressData$);
const visible = useObservable(progressBarService.visible$);
+ const style = useObservable(progressBarService.style$);
+
+ const progressLabel = (() => {
+ if(!progressData){ return ""; }
+ if(progressData?.label){ return progressData.label; }
+ return `${Math.floor(progressData.progression)}%`
+ })();
+ const progressValue: number = progressData?.progression;
+
const {firstColor, secondColor} = useThemeColor();
- return (
- { visible &&
-
-
- { !!progress && (
- <>
-
-
-

-
-
{`${Math.floor(progress)}%`}
+ return (
+
{ visible &&
+
+
+ { !!progressValue && (
+
+
+

+
+
{progressLabel}
+
+ )}
+ { !progressValue &&

}
- >
- )}
- { !progress &&
}
-
-
- }
+
+ }
)
}
diff --git a/src/renderer/components/settings/setting-color-chooser.component.tsx b/src/renderer/components/settings/setting-color-chooser.component.tsx
index 9f5e7337..7ca60ff0 100644
--- a/src/renderer/components/settings/setting-color-chooser.component.tsx
+++ b/src/renderer/components/settings/setting-color-chooser.component.tsx
@@ -1,25 +1,25 @@
-import { useState } from "react";
+import { useRef, useState } from "react";
import { HexColorPicker } from "react-colorful";
import { motion, AnimatePresence } from "framer-motion"
-import OutsideClickHandler from "react-outside-click-handler";
+import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
export default function SettingColorChooser({color, onChange, pickerClassName}: {color?: string, onChange?: (color: string) => void, pickerClassName?: string}) {
const [colorVisible, setColorVisible] = useState(false);
+ const ref = useRef(null);
+ useClickOutside(ref, (e) => setColorVisible(false));
- return (
-
setColorVisible(false)}>
-
+ return (
+
setColorVisible(!colorVisible)} style={{backgroundColor: color}}/>
{colorVisible &&
-
+
}
-
- )
+ )
}
diff --git a/src/renderer/components/settings/setting-container.component.tsx b/src/renderer/components/settings/setting-container.component.tsx
index 6cc41c30..9b625748 100644
--- a/src/renderer/components/settings/setting-container.component.tsx
+++ b/src/renderer/components/settings/setting-container.component.tsx
@@ -8,7 +8,7 @@ export function SettingContainer({className, title, minorTitle, description, chi
return (
{ title &&
{t(title)}
}
- { minorTitle &&
{t(minorTitle)}
}
+ { minorTitle &&
{t(minorTitle)}
}
{ description &&
{t(description)}
}
{ children }
diff --git a/src/renderer/components/settings/supporters-view/supporter-item.component.tsx b/src/renderer/components/settings/supporters-view/supporter-item.component.tsx
index ac0902b7..fd27540e 100644
--- a/src/renderer/components/settings/supporters-view/supporter-item.component.tsx
+++ b/src/renderer/components/settings/supporters-view/supporter-item.component.tsx
@@ -1,16 +1,16 @@
-import React, { CSSProperties } from "react";
+import { CSSProperties } from "react";
import { Supporter } from "shared/models/supporters";
import { motion } from "framer-motion";
import txtBg from "../../../../../assets/images/gifs/txt-bg.gif"
-import { IpcService } from "renderer/services/ipc.service";
+import { LinkOpenerService } from "renderer/services/link-opener.service";
interface Props { supporter: Supporter, delay?: number }
export function SupporterItem({supporter, delay}: Props) {
- const ipcService = IpcService.getInstance();
+ const linkOpener = LinkOpenerService.getInstance();
- const openSupporterLink = () => supporter.link && ipcService.sendLazy("new-window", {args: supporter.link});
+ const openSupporterLink = () => supporter.link && linkOpener.open(supporter.link);
const additionnalStyles: CSSProperties = (() => {
const commonStyles: CSSProperties = {backgroundImage: `url(${txtBg})`, backgroundRepeat: "no-repeat", backgroundPosition: "center"}
diff --git a/src/renderer/components/settings/supporters-view/supporters-block.component.tsx b/src/renderer/components/settings/supporters-view/supporters-block.component.tsx
index caa4f931..0e422917 100644
--- a/src/renderer/components/settings/supporters-view/supporters-block.component.tsx
+++ b/src/renderer/components/settings/supporters-view/supporters-block.component.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "renderer/hooks/use-translation.hook";
import { Supporter } from "shared/models/supporters";
import { SupporterItem } from "./supporter-item.component";
@@ -5,6 +6,8 @@ interface Props {className?: string, title: string, supporters: Supporter[]}
export function SupportersBlock({className, title, supporters}: Props) {
+ const t = useTranslation();
+
const someDelay = () => {
const [min, max] = [0, .55]
const baseDelay = .15;
@@ -13,7 +16,7 @@ export function SupportersBlock({className, title, supporters}: Props) {
return (
-
{title}
+
{t(title)}
{supporters.map(s => )}
diff --git a/src/renderer/components/settings/supporters-view/supporters-view.component.tsx b/src/renderer/components/settings/supporters-view/supporters-view.component.tsx
index 961c0467..847e97f1 100644
--- a/src/renderer/components/settings/supporters-view/supporters-view.component.tsx
+++ b/src/renderer/components/settings/supporters-view/supporters-view.component.tsx
@@ -6,12 +6,14 @@ import { SupportersService } from "renderer/services/supporters.service";
import { SupportersBlock } from "./supporters-block.component";
import ManheraChanGif from "../../../../../assets/images/gifs/menhera-chan.gif"
import ManheraSadGif from "../../../../../assets/images/gifs/menhera-sad.gif"
+import { useTranslation } from "renderer/hooks/use-translation.hook";
interface Props {isVisible: boolean, setVisible: (b: boolean) => void}
export function SupportersView({isVisible, setVisible}: Props) {
const supportersService = SupportersService.getInstance();
+ const t = useTranslation();
const [supporters, setSupporters] = useState([] as Supporter[]);
const [sponsors, setSponsors] = useState([] as Supporter[]);
@@ -33,11 +35,11 @@ export function SupportersView({isVisible, setVisible}: Props) {
setVisible(false)}>
{(!!sponsors.length || !!supporters.length) &&

}
- {!!sponsors.length &&
}
- {!!supporters.length && }
+ {!!sponsors.length && }
+ {!!supporters.length && }
{(!sponsors.length && !supporters.length) && (
<>
- No supporter yet
+ {t("pages.settings.patreon.view.no-supporters")}
>
)}
diff --git a/src/renderer/components/shared/UwU/spoiler-click.component.tsx b/src/renderer/components/shared/UwU/spoiler-click.component.tsx
new file mode 100644
index 00000000..a9f6e1a6
--- /dev/null
+++ b/src/renderer/components/shared/UwU/spoiler-click.component.tsx
@@ -0,0 +1,21 @@
+import {motion, useCycle} from "framer-motion"
+import { useState } from "react"
+
+type Props = {className?: string, nbClickToShow?: number, children?: JSX.Element}
+
+export function SpoilerClick({className, children, nbClickToShow = 5}: Props) {
+
+ const [remainingClicks, setRemainingClicks] = useState(nbClickToShow);
+ const [translateY, setTranslateY] = useState("100%");
+
+ const handleClicks = () => {
+ if(remainingClicks - 1 > 0){ setRemainingClicks(nb => nb - 1); }
+ setTranslateY(`${(((remainingClicks - 1) / nbClickToShow) * 100)}%`)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.css b/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.css
new file mode 100644
index 00000000..62be5b72
--- /dev/null
+++ b/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.css
@@ -0,0 +1,17 @@
+.loader {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ display: inline-block;
+ box-sizing: border-box;
+ animation: rotation 1s linear infinite;
+}
+
+@keyframes rotation {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.tsx b/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.tsx
new file mode 100644
index 00000000..c5d9d7bd
--- /dev/null
+++ b/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.tsx
@@ -0,0 +1,17 @@
+import { CSSProperties } from "react"
+import "./bsm-basic-spinner.component.css"
+
+type Props = {
+ className?: string,
+ style?: CSSProperties,
+ spinnerClassName?: string,
+ thikness?: string
+}
+
+export function BsmBasicSpinner({className, style, spinnerClassName, thikness = "5px"}: Props) {
+ return (
+
+
+
+ )
+}
diff --git a/src/renderer/components/shared/bsm-button.component.tsx b/src/renderer/components/shared/bsm-button.component.tsx
index 4589d684..4671a62f 100644
--- a/src/renderer/components/shared/bsm-button.component.tsx
+++ b/src/renderer/components/shared/bsm-button.component.tsx
@@ -1,15 +1,16 @@
import { BsmIcon, BsmIconType } from "../svgs/bsm-icon.component"
-import OutsideClickHandler from 'react-outside-click-handler';
-import React from "react";
+import { useRef, CSSProperties } from "react";
import { BsmImage } from "./bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
+import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
+import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
type BsmButtonType = "primary"|"success"|"cancel"|"error";
-type propsType = {
+type Props = {
className?: string,
- style?: React.CSSProperties,
+ style?: CSSProperties,
imgClassName?: string,
iconClassName?: string,
icon?: BsmIconType,
@@ -22,50 +23,46 @@ type propsType = {
onClickOutside?: (e: MouseEvent) => void,
onClick?: (e: React.MouseEvent) => void,
typeColor?:BsmButtonType,
- color?: string
+ color?: string,
+ title?: string,
+ iconColor?: string,
+ textClassName?: string
}
-export function BsmButton({className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color}: propsType) {
+export function BsmButton({className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor, textClassName}: Props) {
- const t = useTranslation();
- const secondColor = useThemeColor("second-color");
+ const t = useTranslation();
+ const secondColor = useThemeColor("second-color");
+ const ref = useRef(null);
+ useClickOutside(ref, onClickOutside);
- const primaryColor = typeColor === "primary" ? useThemeColor("first-color") : null;
+ const primaryColor = typeColor === "primary" && useThemeColor("first-color");
- const textColor = !primaryColor ? null : (() => {
- const hex = primaryColor.replaceAll("#", "");
- const uicolors = [parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255];
- const c = uicolors.map(col => {
- if (col <= 0.03928) { return col / 12.92; }
- return Math.pow((col + 0.055) / 1.055, 2.4);
- });
- const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
- return (L > 0.179) ? "#000" : "fff";
- })();
+ const textColor = (() => {
+ if(primaryColor){ return getCorrectTextColor(primaryColor); }
+ return typeColor ? "white" : undefined;
+ })();
- const renderTypeColor = (() => {
- if(typeColor === "primary"){ return null; }
- if(!typeColor){ return `bg-light-main-color-2 dark:bg-main-color-2 ${!withBar && "hover:bg-light-main-color-3 dark:hover:bg-main-color-3"}`; }
- if(typeColor === "cancel"){ return "bg-gray-500"; }
- if(typeColor === "error"){ return "bg-red-500"; }
- if(typeColor === "success"){ return "bg-green-500"; }
- return "";
- })()
+ const renderTypeColor = (() => {
+ if(typeColor === "primary"){ return ""; }
+ if(!typeColor){ return `bg-light-main-color-2 dark:bg-main-color-2 ${!withBar && "hover:brightness-125"}`; }
+ if(typeColor === "cancel"){ return "bg-gray-500"; }
+ if(typeColor === "error"){ return "bg-red-500"; }
+ if(typeColor === "success"){ return "bg-green-500"; }
+ return "";
+ })();
- return (
- onClickOutside && onClickOutside(e)}>
- onClick && onClick(e)} className={`${className} overflow-hidden cursor-pointer group ${!withBar && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, ...((!!primaryColor || !!color) && {backgroundColor: primaryColor ?? color})}}>
- { image &&
}
- { icon && }
- {text && (type === "submit" ? : {t(text)})}
- { withBar && (
-
- )}
-
-
-
- )
+ return (
+
+ { image &&
}
+ { icon && }
+ {text && (type === "submit" ? : {t(text)})}
+ { withBar && (
+
+ )}
+
+ )
}
diff --git a/src/renderer/components/shared/bsm-checkbox.component.tsx b/src/renderer/components/shared/bsm-checkbox.component.tsx
new file mode 100644
index 00000000..59ab476d
--- /dev/null
+++ b/src/renderer/components/shared/bsm-checkbox.component.tsx
@@ -0,0 +1,26 @@
+import { BsmIcon } from "../svgs/bsm-icon.component"
+import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
+import { getCorrectTextColor } from "renderer/helpers/correct-text-color"
+
+type Props = {className?:string, checked?: boolean, onChange?: (val: boolean) => void, disabled?: boolean}
+
+export function BsmCheckbox({className, checked, onChange, disabled} : Props) {
+
+ const checkedColor = useThemeColor("first-color");
+ const iconColor = getCorrectTextColor(checkedColor);
+
+ const handleClick = () => {
+ if(disabled){ return; }
+ onChange && onChange(!checked);
+ }
+
+ return (
+
+ {!disabled && }
+
+ {e.stopPropagation(); handleClick()}}>
+
+
+
+ )
+}
diff --git a/src/renderer/components/shared/bsm-dropdown-button.component.tsx b/src/renderer/components/shared/bsm-dropdown-button.component.tsx
index 1a43937d..6c067286 100644
--- a/src/renderer/components/shared/bsm-dropdown-button.component.tsx
+++ b/src/renderer/components/shared/bsm-dropdown-button.component.tsx
@@ -1,27 +1,63 @@
-import { useState } from "react"
-import { BsmIconType } from "../svgs/bsm-icon.component"
+import { useRef, useState } from "react"
+import { BsmIconType, BsmIcon } from "../svgs/bsm-icon.component"
import { BsmButton } from "./bsm-button.component"
-import { BsmIcon } from "../svgs/bsm-icon.component"
import { useTranslation } from "renderer/hooks/use-translation.hook"
+import { AnimatePresence } from "framer-motion"
+import { useClickOutside } from "renderer/hooks/use-click-outside.hook"
export interface DropDownItem {text: string, icon?: BsmIconType, onClick?: () => void}
-export function BsmDropdownButton({className, items, align}: {className?: string, items?: DropDownItem[], align?: "left"|"right"}) {
+type Props = {
+ className?: string,
+ items?: DropDownItem[],
+ align?: "left"|"right"|"center",
+ withBar?: boolean,
+ icon?: BsmIconType,
+ buttonClassName?: string,
+ menuTranslationY?: string|number
+ children?: JSX.Element,
+ text?: string,
+ textClassName?: string
+}
+
+export function BsmDropdownButton({className, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY, children, text, textClassName}: Props) {
const [expanded, setExpanded] = useState(false)
- const t = useTranslation()
+ const t = useTranslation();
+ const ref = useRef(null)
+ useClickOutside(ref, () => setExpanded(false));
+
+ const defaultButtonClassName = "relative z-[1] p-1 rounded-md text-inherit w-full h-full shadow-md shadow-black"
+
+ const handleClickOutside = () => {
+ if(children){ return; }
+ setExpanded(false);
+ }
+
+ const alignClass = (() => {
+ if(align === "center"){ return "right-1/2 origin-top-right translate-x-[50%]" }
+ if(align === "left"){ return "left-0 origin-top-left"; }
+ return "right-0 origin-top-right";
+ })()
return (
-
-
setExpanded(!expanded)} className='relative z-[1] p-1 rounded-md text-inherit w-full h-full shadow-md shadow-black' icon="settings" active={expanded} onClickOutside={() => {setExpanded(false)}}/>
-
- { items?.map((i, index) => !!i &&(
-
- {i.icon &&
}
+
+
setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} onClickOutside={handleClickOutside} withBar={withBar} text={text}/>
+
+ { items?.map((i) => i && (
+
i.onClick?.()} className="flex w-full px-3 py-2 hover:backdrop-brightness-150">
+ {i.icon && }
{t(i.text)}
))}
+ {!!children && (
+
+ {expanded && (
+ children
+ )}
+
+ )}
)
}
diff --git a/src/renderer/components/shared/bsm-image.component.tsx b/src/renderer/components/shared/bsm-image.component.tsx
index ad0c3ceb..3fc99e5d 100644
--- a/src/renderer/components/shared/bsm-image.component.tsx
+++ b/src/renderer/components/shared/bsm-image.component.tsx
@@ -1,6 +1,42 @@
-export function BsmImage({className, image, errorImage, placeholder, loading}: {className?: string, image: string, errorImage?: string, placeholder?: string, loading?: "lazy"|"eager"}) {
+import { CSSProperties, forwardRef, SyntheticEvent, useState } from "react";
- return (
-

{ currentTarget.onerror = null; currentTarget.src = errorImage ? errorImage : "" }} style={{backgroundImage: `url(${placeholder})`, backgroundSize: "cover"}}/>
- )
+type Props = {
+ className?: string,
+ image: string,
+ errorImage?: string,
+ placeholder?: string,
+ loading?: "lazy"|"eager",
+ style?: CSSProperties
+ title?: string,
+ onClick?: (e: MouseEvent) => void
}
+
+export const BsmImage = forwardRef(({className, image, errorImage, placeholder, loading, style, title, onClick}: Props, ref) => {
+
+ const [isLoaded, setIsLoaded] = useState(false);
+
+ image = image || (placeholder||errorImage);
+
+ const styles: CSSProperties = (() => {
+ return {
+ ...style,
+ ...(!isLoaded && {backgroundImage: `url(${placeholder})`}),
+ ...(!isLoaded && {backgroundSize: "cover"}),
+ ...(!isLoaded && {backgroundPosition: "center"}),
+ }
+ })();
+
+ const handleError = (e: SyntheticEvent
) => {
+ e.currentTarget.onerror = null;
+ e.currentTarget.src = errorImage || "";
+ }
+
+ const handleLoaded = () => {
+ setIsLoaded(() => true);
+ }
+
+ return (
+ // @ts-ignore
+
onClick?.(e)} alt=" "/>
+ )
+})
diff --git a/src/renderer/components/shared/bsm-link.component.tsx b/src/renderer/components/shared/bsm-link.component.tsx
new file mode 100644
index 00000000..a3b9930e
--- /dev/null
+++ b/src/renderer/components/shared/bsm-link.component.tsx
@@ -0,0 +1,23 @@
+import { LinkOpenerService } from "renderer/services/link-opener.service"
+
+type Props = {
+ className?: string,
+ href?: string,
+ internal?: boolean
+ children?: React.ReactNode,
+ style?: React.CSSProperties
+}
+
+export function BsmLink({className, href, children, style, internal}: Props) {
+
+ const linkOpener = LinkOpenerService.getInstance();
+
+ const openLink = () => {
+ if(!href){ return; }
+ linkOpener.open(href, internal);
+ }
+
+ return (
+ {e.stopPropagation(); openLink()}} style={style}>{children}
+ )
+}
diff --git a/src/renderer/components/shared/bsm-map-preview.component.tsx b/src/renderer/components/shared/bsm-map-preview.component.tsx
new file mode 100644
index 00000000..815f942c
--- /dev/null
+++ b/src/renderer/components/shared/bsm-map-preview.component.tsx
@@ -0,0 +1,28 @@
+import { AnimatePresence, motion } from "framer-motion";
+import { useRef } from "react";
+import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
+import { useObservable } from "renderer/hooks/use-observable.hook"
+import { LinkOpenerService } from "renderer/services/link-opener.service";
+
+export function BsmIframeView() {
+
+ const linkService = LinkOpenerService.getInstance()
+
+ const iframeUrl = useObservable(linkService.iframeLink$);
+ const ref = useRef(null);
+ useClickOutside(ref, () => linkService.closeIframe());
+
+ return (
+
+ {iframeUrl && (
+
+
+
+
+
+ )}
+
+
+ )
+}
diff --git a/src/renderer/components/shared/bsm-range.component.tsx b/src/renderer/components/shared/bsm-range.component.tsx
new file mode 100644
index 00000000..62eb3b88
--- /dev/null
+++ b/src/renderer/components/shared/bsm-range.component.tsx
@@ -0,0 +1,63 @@
+import { getTrackBackground, Range } from "react-range"
+import { getCorrectTextColor } from "renderer/helpers/correct-text-color"
+import { ThemeColor, useThemeColor } from "renderer/hooks/use-theme-color.hook"
+
+type Props = {
+ colorType?: ThemeColor
+ values: number[],
+ onChange?: (values: number[]) => void,
+ onFinalChange?: (values: number[]) => void,
+ min: number,
+ max: number,
+ step?: number,
+ renderLabel?: (value: number) => JSX.Element,
+}
+
+export function BsmRange({colorType = "first-color", values, onChange, onFinalChange, min, max, renderLabel, step = 1} : Props) {
+
+ const color = useThemeColor(colorType);
+ const labelTextColor = getCorrectTextColor(color);
+
+ return (
+ onChange?.(v)}
+ onFinalChange={(v) => onFinalChange?.(v)}
+ renderTrack={({props, children}) => (
+
+ {children}
+
+ )}
+ renderThumb={({ index, props }) => (
+
+ {renderLabel && renderLabel(values[index])}
+
+ )}
+ />
+ )
+}
diff --git a/src/renderer/components/shared/bsm-select.component.tsx b/src/renderer/components/shared/bsm-select.component.tsx
new file mode 100644
index 00000000..8b9a87ba
--- /dev/null
+++ b/src/renderer/components/shared/bsm-select.component.tsx
@@ -0,0 +1,31 @@
+import { ChangeEvent, CSSProperties } from "react"
+import { useTranslation } from "renderer/hooks/use-translation.hook"
+
+type Props = {
+ className?: string,
+ style?: CSSProperties,
+ options?: BsmSelectOption[],
+ onChange?: (value: string) => void
+}
+
+export function BsmSelect({className, style, options, onChange}: Props) {
+
+ const t = useTranslation();
+
+ const handleChange = (event: ChangeEvent) => {
+ onChange?.(event.target.value);
+ }
+
+ return (
+
+ )
+}
+
+export interface BsmSelectOption{
+ text: string,
+ value: string
+}
diff --git a/src/renderer/components/shared/tab-nav-bar.component.tsx b/src/renderer/components/shared/tab-nav-bar.component.tsx
index d2ccd337..4907d3cb 100644
--- a/src/renderer/components/shared/tab-nav-bar.component.tsx
+++ b/src/renderer/components/shared/tab-nav-bar.component.tsx
@@ -1,30 +1,43 @@
-import { useState } from "react";
+import { DetailedHTMLProps, Fragment, useState } from "react";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { useTranslation } from "renderer/hooks/use-translation.hook";
-export function TabNavBar(props: {tabsText: string[], onTabChange: Function, className?: string}) {
+type Props = {
+ tabsText: string[],
+ onTabChange: (index: number) => void,
+ className?: string,
+ renderTab?: (props: DetailedHTMLProps, any>, text: string, index?: number) => JSX.Element
+}
+
+export function TabNavBar(props: Props) {
const [currentTabIndex, setCurrentTabIndex] = useState(0);
const t = useTranslation();
const secondColor = useThemeColor("second-color");
- const selectYear = (tab: string) => {
- const newIndex = props.tabsText.indexOf(tab);
- setCurrentTabIndex(newIndex);
- props.onTabChange(newIndex);
+ const selectTab = (index: number) => {
+ setCurrentTabIndex(index);
+ props.onTabChange(index);
}
return (
-
+
+
+ {props.tabsText.map((text, index) => (
+ props.renderTab ? (
+
+ {props.renderTab({onClick: () => selectTab(index)}, t(text), index)}
+
+ ) : (
+ - selectTab(index)}>{t(text)}
+ )
+
+ ))}
+
+
)
}
diff --git a/src/renderer/components/slideshow/slideshow.component.tsx b/src/renderer/components/slideshow/slideshow.component.tsx
index 37d44681..6318a927 100644
--- a/src/renderer/components/slideshow/slideshow.component.tsx
+++ b/src/renderer/components/slideshow/slideshow.component.tsx
@@ -1,10 +1,22 @@
+import { BsmImage } from '../shared/bsm-image.component';
import './slideshow.component.css';
-export function Slideshow(props: {className: string, images: string[]}) {
+export function Slideshow(props: {className: string}) {
+
+ const slideshowImages = [
+ require('../../../../assets/images/slideshow-images/image-1-blur.jpg'),
+ require('../../../../assets/images/slideshow-images/image-2-blur.jpg'),
+ require('../../../../assets/images/slideshow-images/image-3-blur.jpg'),
+ require('../../../../assets/images/slideshow-images/image-4-blur.jpg'),
+ require('../../../../assets/images/slideshow-images/image-5-blur.png'),
+ require('../../../../assets/images/slideshow-images/image-6-blur.png'),
+ require('../../../../assets/images/slideshow-images/image-7-blur.png'),
+ ];
+
return (
{
- props.images.map((i, index) =>

)
+ slideshowImages.map((i, index) =>
)
}
)
diff --git a/src/renderer/components/svgs/bsm-icon.component.tsx b/src/renderer/components/svgs/bsm-icon.component.tsx
index ced7ac0e..4b92f7cb 100644
--- a/src/renderer/components/svgs/bsm-icon.component.tsx
+++ b/src/renderer/components/svgs/bsm-icon.component.tsx
@@ -19,11 +19,35 @@ import { CSSProperties, memo } from "react";
import EditIcon from "./icons/edit-icon.component";
import { ExportIcon } from "./icons/export-icon.component";
import PatreonIcon from "./icons/patreon-icon.component";
+import { SearchIcon } from "./icons/search-icon.component";
+import { CheckIcon } from "./icons/check-icon.component";
+import { ThreeDotsIcon } from "./icons/three-dots-icon.component";
+import { GitHubIcon } from "./icons/github-icon.component";
+import { CloseIcon } from "./icons/close-icon.component";
+import { BsMapDifficultyIcon } from "./icons/bs-map-difficulty-icon.component";
+import { TwitchIcon } from "./icons/twitch-icon.component";
+import EyeIcon from "./icons/eye-icon.component";
+import { PlayIcon } from "./icons/play-icon.component";
+import { ThumbUpFillIcon } from "./icons/thumb-up-fill-icon.component";
+import { TimerFillIcon } from "./icons/timer-fill.component";
+import { CheckCircleIcon } from "./icons/check-circle-icon.component";
+import { PauseIcon } from "./icons/pause-icon.component";
+import { BsvMapCharacteristic } from "shared/models/maps/beat-saver.model";
+import { LightshowIcon } from "./icons/lightshow-icon.component";
+import { LawlessIcon } from "./icons/lawless-icon.component";
+import { NoArrowIcon } from "./icons/no-arrow-icon.component";
+import { OneSaberIcon } from "./icons/one-saber-icon.component";
+import { NinetyDregreeIcon } from "./icons/ninety-dregree-icon.component";
+import { ThreeSixtyDegreeIcon } from "./icons/three-sixty-degree-icon.component";
+import { LinkIcon } from "./icons/link-icon.component";
+import { UnlinkIcon } from "./icons/unlink-icon.component";
+import { DownloadIcon } from "./icons/download-icon.component";
+import { FilterIcon } from "./icons/filter-icon.component";
-export type BsmIconType = (
- "settings"|"trash"|"favorite"|"folder"|"bsNote"|
- "terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|
- "copy"|"steam"|"edit"|"export"|"patreon"|
+export type BsmIconType = BsvMapCharacteristic | (
+ "settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|"twitch"|"eye"|"play"|"checkCircleIcon"|
+ "terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|"thumbUpFill"|"timerFill"|"pause"|
+ "copy"|"steam"|"edit"|"export"|"patreon"|"search"|"bsMapDifficulty"|"link"|"unlink"|"download"|"filter"|
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
);
@@ -50,6 +74,29 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
if(icon === "edit"){ return }
if(icon === "export"){ return }
if(icon === "patreon"){ return }
+ if(icon === "search"){ return }
+ if(icon === "check"){ return }
+ if(icon === "three-dots"){ return }
+ if(icon === "github"){ return }
+ if(icon === "close"){ return }
+ if(icon === "bsMapDifficulty" || icon === "Standard"){ return }
+ if(icon === "twitch"){ return }
+ if(icon === "eye"){ return }
+ if(icon === "play"){ return }
+ if(icon === "thumbUpFill"){ return }
+ if(icon === "timerFill"){ return }
+ if(icon === "checkCircleIcon"){ return }
+ if(icon === "pause"){ return }
+ if(icon === "Lawless"){ return }
+ if(icon === "NoArrows"){ return }
+ if(icon === "OneSaber"){ return }
+ if(icon === "Lightshow"){ return }
+ if(icon === "90Degree"){ return }
+ if(icon === "360Degree"){ return }
+ if(icon === "link"){ return }
+ if(icon === "unlink"){ return }
+ if(icon === "download"){ return }
+ if(icon === "filter"){ return