diff --git a/public/popup-old.html b/public/popup-old.html new file mode 100644 index 00000000..dd92927a --- /dev/null +++ b/public/popup-old.html @@ -0,0 +1,209 @@ + + + + + __MSG_openPopup__ + + + + + + + +
+ + + + + + + +
+ +

__MSG_noVideoID__

+ +

+ + +
+ +
+ +
+
+ + +
+ + + + +
+ + + + + + + +
+

+ __MSG_yourWork__ +

+
+ +
+

__MSG_Username__: + + +

+
+

+ + +
+ +
+ + +
+ + + +
+ + + + + + + +
+ + + + diff --git a/public/popup.css b/public/popup.css index a969738f..aa3fd108 100644 --- a/public/popup.css +++ b/public/popup.css @@ -582,6 +582,10 @@ margin: 0 !important; } +#sponsorBlockPopupBody .u-mZ.cleanPopupMargin { + margin-top: 10px !important; +} + #sponsorBlockPopupBody .hidden { display: none !important; } @@ -618,4 +622,9 @@ #issueReporterTabs > span.sbSelected > span::after { transform: scaleX(0.8); +} + +.sbPopupButton { + width: 16px; + fill: var(--sb-main-fg-color); } \ No newline at end of file diff --git a/public/popup.html b/public/popup.html index dd92927a..6264bbe4 100644 --- a/public/popup.html +++ b/public/popup.html @@ -1,209 +1,13 @@ - - - - __MSG_openPopup__ - - - - - + + - -
+ + + + - + - - - - -
- -

__MSG_noVideoID__

- -

- - -
- -
- -
-
- - -
- - - - -
- - - - - - - -
-

- __MSG_yourWork__ -

-
- -
-

__MSG_Username__: - - -

-
-

- - -
- -
- - -
- - - -
- - - - - - - -
- - - - + + \ No newline at end of file diff --git a/src/content.ts b/src/content.ts index c232aed9..90934d5c 100644 --- a/src/content.ts +++ b/src/content.ts @@ -223,7 +223,10 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo status: lastResponseStatus, sponsorTimes: sponsorTimes, time: getCurrentTime() ?? 0, - onMobileYouTube: isOnMobileYouTube() + onMobileYouTube: isOnMobileYouTube(), + videoID: getVideoID(), + loopedChapter: loopedChapter?.UUID, + channelWhitelisted }); if (!request.updating && popupInitialised && document.getElementById("sponsorBlockPopupContainer") != null) { @@ -1275,7 +1278,10 @@ async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) { status: lastResponseStatus, sponsorTimes: sponsorTimes, time: getCurrentTime() ?? 0, - onMobileYouTube: isOnMobileYouTube() + onMobileYouTube: isOnMobileYouTube(), + videoID: getVideoID(), + loopedChapter: loopedChapter?.UUID, + channelWhitelisted }); if (Config.config.isVip) { diff --git a/src/messageTypes.ts b/src/messageTypes.ts index e9920d7e..0e17fd4f 100644 --- a/src/messageTypes.ts +++ b/src/messageTypes.ts @@ -2,7 +2,7 @@ // Message and Response Types // -import { SegmentUUID, SponsorHideType, SponsorTime } from "./types"; +import { SegmentUUID, SponsorHideType, SponsorTime, VideoID } from "./types"; interface BaseMessage { from?: string; @@ -84,6 +84,9 @@ export interface IsInfoFoundMessageResponse { sponsorTimes: SponsorTime[]; time: number; onMobileYouTube: boolean; + videoID: VideoID; + loopedChapter: SegmentUUID | null; + channelWhitelisted: boolean; } interface GetVideoIdResponse { diff --git a/src/popup/PopupComponent.tsx b/src/popup/PopupComponent.tsx new file mode 100644 index 00000000..d33b7952 --- /dev/null +++ b/src/popup/PopupComponent.tsx @@ -0,0 +1,393 @@ +import * as React from "react"; +import { YourWorkComponent } from "./YourWorkComponent"; +// import { ToggleOptionComponent } from "./ToggleOptionComponent"; +// import { FormattingOptionsComponent } from "./FormattingOptionsComponent"; +import { isSafari } from "../../maze-utils/src/config"; +import { showDonationLink } from "../utils/configUtils"; +import Config from "../config"; +import { GetChannelIDResponse, IsInfoFoundMessageResponse, Message, MessageResponse, PopupMessage } from "../messageTypes"; +import { AnimationUtils } from "../../maze-utils/src/animationUtils"; +import { SegmentListComponent } from "./SegmentListComponent"; +import { ActionType, SegmentUUID, SponsorSourceType, SponsorTime } from "../types"; +import { SegmentSubmissionComponent } from "./SegmentSubmissionComponent"; + +export enum LoadingStatus { + Loading, + SegmentsFound, + NoSegmentsFound +} + +let loadRetryCount = 0; + +export const PopupComponent = () => { + const [status, setStatus] = React.useState(LoadingStatus.Loading); + const [extensionEnabled, setExtensionEnabled] = React.useState(!Config.config!.disableSkipping); + const [channelWhitelisted, setChannelWhitelisted] = React.useState(null); + const [showForceChannelCheckWarning, setShowForceChannelCheckWarning] = React.useState(false); + const [showNoticeButton, setShowNoticeButton] = React.useState(Config.config!.dontShowNotice); + + const [currentTime, setCurrentTime] = React.useState(0); + const [segments, setSegments] = React.useState([]); + const [loopedChapter, setLoopedChapter] = React.useState(null); + + const [videoID, setVideoID] = React.useState(null); + + React.useEffect(() => { + loadSegments({ + updating: false, + setStatus, + setChannelWhitelisted, + setVideoID, + setCurrentTime, + setSegments, + setLoopedChapter + }); + + setupComPort({ + setStatus, + setChannelWhitelisted, + setVideoID, + setCurrentTime, + setSegments, + setLoopedChapter + }); + + forwardClickEvents(sendMessage); + }, []); + + return ( +
+ { + window !== window.top && + + } + + { + Config.config!.testingServer && +
{ + chrome.runtime.sendMessage({ "message": "openConfig", "hash": "advanced" }); + }}> + {chrome.i18n.getMessage("betaServerWarning")} +
+ } + +
+ +

+ SponsorBlock +

+
+ + {/* Loading text */} + {status === LoadingStatus.Loading && ( +

+ {chrome.i18n.getMessage("noVideoID")} +

+ )} + {/* If the video was found in the database */} + {status !== LoadingStatus.Loading && ( +

+ {status === LoadingStatus.SegmentsFound ? chrome.i18n.getMessage("sponsorFound") : chrome.i18n.getMessage("sponsor404")} +

+ )} + + + + + {/* Toggle Box */} +
+ {/* github: mbledkowski/toggle-switch */} + {channelWhitelisted !== null && ( + + )} + + +
+ + { + showForceChannelCheckWarning && + { + chrome.runtime.sendMessage({ "message": "openConfig", "hash": "behavior" }); + }}> + {chrome.i18n.getMessage("forceChannelCheckPopup")} + + } + + { + !Config.config.cleanPopup && + + } + + + {/* Your Work box */} + { + !Config.config.cleanPopup && + + } + + {/* Footer */} + { + !Config.config.cleanPopup && + + } + + { + showNoticeButton && + + } +
+ ); +}; + +interface SegmentsLoadedProps { + setStatus: (status: LoadingStatus) => void; + setChannelWhitelisted: (whitelisted: boolean | null) => void; + setVideoID: (videoID: string | null) => void; + setCurrentTime: (time: number) => void; + setSegments: (segments: SponsorTime[]) => void; + setLoopedChapter: (loopedChapter: SegmentUUID | null) => void; +} + +interface LoadSegmentsProps extends SegmentsLoadedProps { + updating: boolean; +} + +async function loadSegments(props: LoadSegmentsProps): Promise { + const response = await sendMessage({ message: "isInfoFound", updating: props.updating }) as IsInfoFoundMessageResponse; + + if (response && response.videoID) { + segmentsLoaded(response, props); + } else { + // Handle error if it exists + chrome.runtime.lastError; + + props.setStatus(LoadingStatus.NoSegmentsFound); + + if (!props.updating) { + loadRetryCount++; + if (loadRetryCount < 6) { + setTimeout(() => loadSegments(props), 100 * loadRetryCount); + } + } + } +} + +function segmentsLoaded(response: IsInfoFoundMessageResponse, props: SegmentsLoadedProps): void { + props.setStatus(response.sponsorTimes?.length > 0 ? LoadingStatus.SegmentsFound : LoadingStatus.NoSegmentsFound); + props.setVideoID(response.videoID); + props.setCurrentTime(response.time); + props.setChannelWhitelisted(response.channelWhitelisted); + props.setSegments((response.sponsorTimes || []) + .filter((segment) => segment.source === SponsorSourceType.Server) + .sort((a, b) => b.segment[1] - a.segment[1]) + .sort((a, b) => a.segment[0] - b.segment[0]) + .sort((a, b) => a.actionType === ActionType.Full ? -1 : b.actionType === ActionType.Full ? 1 : 0)); + props.setLoopedChapter(response.loopedChapter); +} + +function sendMessage(request: Message): Promise { + return new Promise((resolve) => { + if (chrome.tabs) { + chrome.tabs.query({ + active: true, + currentWindow: true + }, (tabs) => chrome.tabs.sendMessage(tabs[0].id, request, resolve)); + } else { + chrome.runtime.sendMessage({ message: "tabs", data: request }, resolve); + } + }); +} + +interface ComPortProps extends SegmentsLoadedProps { +} + +function setupComPort(props: ComPortProps): void { + const port = chrome.runtime.connect({ name: "popup" }); + port.onDisconnect.addListener(() => setupComPort(props)); + port.onMessage.addListener((msg) => onMessage(props, msg)); +} + +function onMessage(props: ComPortProps, msg: PopupMessage) { + switch (msg.message) { + case "time": + props.setCurrentTime(msg.time); + break; + case "infoUpdated": + segmentsLoaded(msg, props); + break; + case "videoChanged": + props.setStatus(LoadingStatus.SegmentsFound); + props.setVideoID(msg.videoID); + props.setChannelWhitelisted(msg.whitelisted); + props.setSegments([]); + break; + } +} + +function forwardClickEvents(sendMessage: (request: Message) => Promise): void { + if (window !== window.top) { + document.addEventListener("keydown", (e) => { + const target = e.target as HTMLElement; + if (target.tagName === "INPUT" + || target.tagName === "TEXTAREA" + || e.key === "ArrowUp" + || e.key === "ArrowDown") { + return; + } + + if (e.key === " ") { + // No scrolling + e.preventDefault(); + } + + sendMessage({ + message: "keydown", + key: e.key, + keyCode: e.keyCode, + code: e.code, + which: e.which, + shiftKey: e.shiftKey, + ctrlKey: e.ctrlKey, + altKey: e.altKey, + metaKey: e.metaKey + }); + }); + } +} \ No newline at end of file diff --git a/src/popup/SegmentListComponent.tsx b/src/popup/SegmentListComponent.tsx new file mode 100644 index 00000000..87dfc44b --- /dev/null +++ b/src/popup/SegmentListComponent.tsx @@ -0,0 +1,445 @@ +import * as React from "react"; +import { ActionType, SegmentUUID, SponsorHideType, SponsorTime, VideoID } from "../types"; +import Config from "../config"; +import { waitFor } from "../../maze-utils/src"; +import { shortCategoryName } from "../utils/categoryUtils"; +import { getErrorMessage, getFormattedTime } from "../../maze-utils/src/formating"; +import { AnimationUtils } from "../../maze-utils/src/animationUtils"; +import { asyncRequestToServer } from "../utils/requests"; +import { Message, MessageResponse, VoteResponse } from "../messageTypes"; +import { LoadingStatus } from "./PopupComponent"; +import GenericNotice from "../render/GenericNotice"; +import { exportTimes } from "../utils/exporter"; + +interface SegmentListComponentProps { + videoID: VideoID; + currentTime: number; + status: LoadingStatus; + segments: SponsorTime[]; + loopedChapter: SegmentUUID | null; + + sendMessage: (request: Message) => Promise; +} + +enum SegmentListTab { + Segments, + Chapter +} + +export const SegmentListComponent = (props: SegmentListComponentProps) => { + const [tab, setTab] = React.useState(SegmentListTab.Segments); + const [isVip, setIsVip] = React.useState(Config.config?.isVip ?? false); + + React.useEffect(() => { + if (!Config.isReady()) { + waitFor(() => Config.isReady()).then(() => { + setIsVip(Config.config.isVip); + }); + } else { + setIsVip(Config.config.isVip); + } + }, []); + + React.useEffect(() => { + setTab(SegmentListTab.Segments); + }, [props.videoID]); + + const tabFilter = (segment: SponsorTime) => { + if (tab === SegmentListTab.Chapter) { + return segment.actionType === ActionType.Chapter; + } else { + return segment.actionType !== ActionType.Chapter; + } + }; + + return ( +
+
s.actionType === ActionType.Chapter) ? "" : "hidden"}> + { + setTab(SegmentListTab.Segments); + }}> + {chrome.i18n.getMessage("SegmentsCap")} + + { + setTab(SegmentListTab.Chapter); + }}> + {chrome.i18n.getMessage("Chapters")} + +
+
selectSegment({ + segment: null, + sendMessage: props.sendMessage + })}> + { + props.segments.map((segment) => ( + + )) + } +
+ + +
+ ); +}; + +function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped, tabFilter, sendMessage }: { + segment: SponsorTime; + videoID: VideoID; + currentTime: number; + isVip: boolean; + startingLooped: boolean; + + tabFilter: (segment: SponsorTime) => boolean; + sendMessage: (request: Message) => Promise; +}) { + const [voteMessage, setVoteMessage] = React.useState(null); + const [hidden, setHidden] = React.useState(segment.hidden || SponsorHideType.Visible); + const [isLooped, setIsLooped] = React.useState(startingLooped); + + let extraInfo = ""; + if (segment.hidden === SponsorHideType.Downvoted) { + // This one is downvoted + extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")"; + } else if (segment.hidden === SponsorHideType.MinimumDuration) { + // This one is too short + extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")"; + } else if (segment.hidden === SponsorHideType.Hidden) { + extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")"; + } + + return ( +
skipSegment({ + segment, + sendMessage + })} + onMouseEnter={() => { + selectSegment({ + segment, + sendMessage + }); + }} + className={"votingButtons " + (!tabFilter(segment) ? "hidden" : "")}> + = segment.segment[0] ? ( + currentTime < segment.segment[1] ? "segmentActive" : "segmentPassed" + ) : "" + )}> +
+ { + segment.actionType !== ActionType.Chapter && + + } + {(segment.description || shortCategoryName(segment.category)) + extraInfo} +
+ +
+ { + segment.actionType === ActionType.Full ? chrome.i18n.getMessage("full") : + (getFormattedTime(segment.segment[0], true) + + (segment.actionType !== ActionType.Poi + ? " " + chrome.i18n.getMessage("to") + " " + getFormattedTime(segment.segment[1], true) + : "")) + } +
+
+ +
+ { + vote({ + type: 1, + UUID: segment.UUID, + setVoteMessage: setVoteMessage, + sendMessage + }); + }}/> + { + vote({ + type: 0, + UUID: segment.UUID, + setVoteMessage: setVoteMessage, + sendMessage + }); + }}/> + { + const stopAnimation = AnimationUtils.applyLoadingAnimation(e.currentTarget, 0.3); + + if (segment.UUID.length > 60) { + copyToClipboard(segment.UUID, sendMessage); + } else { + const segmentIDData = await asyncRequestToServer("GET", "/api/segmentID", { + UUID: segment.UUID, + videoID: videoID + }); + + if (segmentIDData.ok && segmentIDData.responseText) { + copyToClipboard(segmentIDData.responseText, sendMessage); + } + } + + stopAnimation(); + }}/> + { + segment.actionType === ActionType.Chapter && + { + if (isLooped) { + loopChapter({ + segment: null, + element: e.currentTarget, + sendMessage + }); + } else { + loopChapter({ + segment, + element: e.currentTarget, + sendMessage + }); + } + + setIsLooped(!isLooped); + }}/> + } + { + (segment.actionType === ActionType.Skip || segment.actionType === ActionType.Mute + || segment.actionType === ActionType.Poi + && [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segment.hidden)) && + { + const stopAnimation = AnimationUtils.applyLoadingAnimation(e.currentTarget, 0.4); + stopAnimation(); + + if (segment.hidden === SponsorHideType.Hidden) { + segment.hidden = SponsorHideType.Visible; + setHidden(SponsorHideType.Visible); + } else { + segment.hidden = SponsorHideType.Hidden; + setHidden(SponsorHideType.Hidden); + } + + sendMessage({ + message: "hideSegment", + type: segment.hidden, + UUID: segment.UUID + }); + }}/> + } + { + segment.actionType !== ActionType.Full && + { + skipSegment({ + segment, + element: e.currentTarget, + sendMessage + }); + }}/> + } +
+ +
+
+ {voteMessage} +
+
+
+ ); +} + +async function vote(props: { + type: number; + UUID: SegmentUUID; + setVoteMessage: (message: string | null) => void; + sendMessage: (request: Message) => Promise; +}): Promise { + props.setVoteMessage(chrome.i18n.getMessage("Loading")); + + const response = await props.sendMessage({ + message: "submitVote", + type: props.type, + UUID: props.UUID + }) as VoteResponse; + + if (response != undefined) { + // See if it was a success or failure + if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) { + // Success (treat rate limits as a success) + props.setVoteMessage(chrome.i18n.getMessage("voted")); + } else if (response.successType == -1) { + props.setVoteMessage(getErrorMessage(response.statusCode, response.responseText)); + } + setTimeout(() => props.setVoteMessage(null), 1500); + } +} + +function skipSegment({ segment, element, sendMessage }: { + segment: SponsorTime; + element?: HTMLElement; + + sendMessage: (request: Message) => Promise; +}): void { + if (segment.actionType === ActionType.Chapter) { + sendMessage({ + message: "unskip", + UUID: segment.UUID + }); + } else { + sendMessage({ + message: "reskip", + UUID: segment.UUID + }); + } + + if (element) { + const stopAnimation = AnimationUtils.applyLoadingAnimation(element, 0.3); + stopAnimation(); + } +} + +function selectSegment({ segment, sendMessage }: { + segment: SponsorTime | null; + + sendMessage: (request: Message) => Promise; +}): void { + sendMessage({ + message: "selectSegment", + UUID: segment?.UUID + }); +} + +function loopChapter({ segment, element, sendMessage }: { + segment: SponsorTime; + element: HTMLElement; + + sendMessage: (request: Message) => Promise; +}): void { + sendMessage({ + message: "loopChapter", + UUID: segment?.UUID + }); + + if (element) { + const stopAnimation = AnimationUtils.applyLoadingAnimation(element, 0.3); + stopAnimation(); + } +} + +function copyToClipboard(text: string, sendMessage: (request: Message) => Promise): void { + if (window === window.top) { + window.navigator.clipboard.writeText(text); + } else { + sendMessage({ + message: "copyToClipboard", + text + }); + } +} + +interface ImportSegmentsProps { + status: LoadingStatus; + segments: SponsorTime[]; + + sendMessage: (request: Message) => Promise; +} + +function ImportSegments(props: ImportSegmentsProps) { + const [importMenuVisible, setImportMenuVisible] = React.useState(false); + const textArea = React.useRef(null); + + return ( +
+
+ + +
+ + + + + + +
+ ) +} \ No newline at end of file diff --git a/src/popup/SegmentSubmissionComponent.tsx b/src/popup/SegmentSubmissionComponent.tsx new file mode 100644 index 00000000..1be9a0c0 --- /dev/null +++ b/src/popup/SegmentSubmissionComponent.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +import { VideoID } from "../types"; +import Config from "../config"; +import { Message, MessageResponse } from "../messageTypes"; +import { LoadingStatus } from "./PopupComponent"; + +interface SegmentSubmissionComponentProps { + videoID: VideoID; + status: LoadingStatus; + + sendMessage: (request: Message) => Promise; +} + +export const SegmentSubmissionComponent = (props: SegmentSubmissionComponentProps) => { + const segments = Config.local.unsubmittedSegments[props.videoID]; + + const [showSubmitButton, setShowSubmitButton] = React.useState(segments && segments.length > 0); + const [showStartSegment, setShowStartSegment] = React.useState(!segments || segments[segments.length - 1].segment.length === 2); + + return ( +
+

+ {chrome.i18n.getMessage("recordTimesDescription")} +

+ + {chrome.i18n.getMessage("popupHint")} + +
+ + +
+ + {chrome.i18n.getMessage("submissionEditHint")} + +
+ ); +}; \ No newline at end of file diff --git a/src/popup/YourWorkComponent.tsx b/src/popup/YourWorkComponent.tsx new file mode 100644 index 00000000..97bacd24 --- /dev/null +++ b/src/popup/YourWorkComponent.tsx @@ -0,0 +1,207 @@ +import * as React from "react"; +import { getHash } from "../../maze-utils/src/hash"; +import { getErrorMessage } from "../../maze-utils/src/formating"; +import Config from "../config"; +import { asyncRequestToServer } from "../utils/requests"; +import PencilIcon from "../svg-icons/pencilIcon"; +import ClipboardIcon from "../svg-icons/clipboardIcon"; +import CheckIcon from "../svg-icons/checkIcon"; + +export const YourWorkComponent = () => { + const [isSettingUsername, setIsSettingUsername] = React.useState(false); + const [username, setUsername] = React.useState(""); + const [newUsername, setNewUsername] = React.useState(""); + const [usernameSubmissionStatus, setUsernameSubmissionStatus] = React.useState(""); + const [submissionCount, setSubmissionCount] = React.useState(""); + const [viewCount, setViewCount] = React.useState(0); + const [minutesSaved, setMinutesSaved] = React.useState(0); + const [showDonateMessage, setShowDonateMessage] = React.useState(false); + + React.useEffect(() => { + (async () => { + const values = ["userName", "viewCount", "minutesSaved", "vip", "permissions", "segmentCount"]; + const result = await asyncRequestToServer("GET", "/api/userInfo", { + publicUserID: await getHash(Config.config!.userID!), + values + }); + + if (result.ok) { + const userInfo = JSON.parse(result.responseText); + setUsername(userInfo.userName); + setSubmissionCount(Math.max(Config.config.sponsorTimesContributed ?? 0, userInfo.segmentCount).toLocaleString()); + setViewCount(userInfo.viewCount); + setMinutesSaved(userInfo.minutesSaved); + + Config.config!.isVip = userInfo.vip; + Config.config!.permissions = userInfo.permissions; + + setShowDonateMessage(Config.config.showDonationLink && Config.config.donateClicked <= 0 && Config.config.showPopupDonationCount < 5 + && viewCount < 50000 && !Config.config.isVip && Config.config.skipCount > 10); + } + })(); + }, []); + + return ( +
+

+ {chrome.i18n.getMessage("yourWork")} +

+
+ {/* Username */} +
+

+ {chrome.i18n.getMessage("Username")}: + {/* loading/errors */} + + {usernameSubmissionStatus} + +

+
+

{username}

+ + +
+
+ { + setNewUsername(e.target.value); + }}/> + +
+
+ +
+ + + + {showDonateMessage && { + setShowDonateMessage(false); + Config.config.showPopupDonationCount = 100; + }} />} + +
+ ); +}; + +function SubmissionCounts(props: { isSettingUsername: boolean; submissionCount: string }): JSX.Element { + return <> +
+

+ {chrome.i18n.getMessage("Submissions")}: +

+

{props.submissionCount}

+
+ +} + +function TimeSavedMessage({ viewCount, minutesSaved }: { viewCount: number; minutesSaved: number }): JSX.Element { + return ( + <> + { + viewCount > 0 && +

+ {chrome.i18n.getMessage("savedPeopleFrom")} + + {viewCount.toLocaleString()}{" "} + + {viewCount !== 1 ? chrome.i18n.getMessage("Segments") : chrome.i18n.getMessage("Segment")} +
+ + {"("}{" "} + + {getFormattedHours(minutesSaved)}{" "} + {minutesSaved !== 1 ? chrome.i18n.getMessage("minsLower") : chrome.i18n.getMessage("minLower")}{" "} + + {chrome.i18n.getMessage("youHaveSavedTimeEnd")}{" "} + {" )"} + +

+ } +

+ {chrome.i18n.getMessage("youHaveSkipped")} + + {Config.config.skipCount}{" "} + + {Config.config.skipCount > 1 ? chrome.i18n.getMessage("Segments") : chrome.i18n.getMessage("Segment")}{" "} + + {"("}{" "} + + {Config.config.minutesSaved}{" "} + {Config.config.minutesSaved !== 1 ? chrome.i18n.getMessage("minsLower") : chrome.i18n.getMessage("minLower")}{" "} + + {")"} + +

+ + ); +} + +function DonateMessage(props: { onClose: () => void }): JSX.Element { + return ( + + ); +} + +/** + * Converts time in minutes to 2d 5h 25.1 + * If less than 1 hour, just returns minutes + * + * @param {float} minutes + * @returns {string} + */ +function getFormattedHours(minutes) { + minutes = Math.round(minutes * 10) / 10; + const years = Math.floor(minutes / 525600); // Assumes 365.0 days in a year + const days = Math.floor(minutes / 1440) % 365; + const hours = Math.floor(minutes / 60) % 24; + return (years > 0 ? years + chrome.i18n.getMessage("yearAbbreviation") + " " : "") + (days > 0 ? days + chrome.i18n.getMessage("dayAbbreviation") + " " : "") + (hours > 0 ? hours + chrome.i18n.getMessage("hourAbbreviation") + " " : "") + (minutes % 60).toFixed(1); +} \ No newline at end of file diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx new file mode 100644 index 00000000..e26b54cf --- /dev/null +++ b/src/popup/popup.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; +import { createRoot } from "react-dom/client"; +import { PopupComponent } from "./PopupComponent"; +import { waitFor } from "../../maze-utils/src"; +import Config from "../config"; + + +document.addEventListener("DOMContentLoaded", async () => { + await waitFor(() => Config.isReady()); + + const root = createRoot(document.body); + root.render(); +}) \ No newline at end of file diff --git a/src/svg-icons/checkIcon.tsx b/src/svg-icons/checkIcon.tsx new file mode 100644 index 00000000..72840ded --- /dev/null +++ b/src/svg-icons/checkIcon.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; + +export interface CheckIconProps { + id?: string; + style?: React.CSSProperties; + className?: string; + onClick?: () => void; +} + +const CheckIcon = ({ + id = "", + className = "", + style = {}, + onClick +}: CheckIconProps): JSX.Element => ( + + + +); + +export default CheckIcon; \ No newline at end of file diff --git a/src/svg-icons/clipboardIcon.tsx b/src/svg-icons/clipboardIcon.tsx new file mode 100644 index 00000000..77f5002a --- /dev/null +++ b/src/svg-icons/clipboardIcon.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; + +export interface ClipboardIconProps { + id?: string; + style?: React.CSSProperties; + className?: string; + onClick?: () => void; +} + +const ClipboardIcon = ({ + id = "", + className = "", + style = {}, + onClick +}: ClipboardIconProps): JSX.Element => ( + + + + +); + +export default ClipboardIcon; \ No newline at end of file diff --git a/src/svg-icons/pencilIcon.tsx b/src/svg-icons/pencilIcon.tsx new file mode 100644 index 00000000..073635fa --- /dev/null +++ b/src/svg-icons/pencilIcon.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; + +export interface PencilIconProps { + id?: string; + style?: React.CSSProperties; + className?: string; + onClick?: () => void; +} + +const PencilIcon = ({ + id = "", + className = "", + style = {}, + onClick +}: PencilIconProps): JSX.Element => ( + + + +); + +export default PencilIcon; \ No newline at end of file diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index d17ec1b2..260bcacb 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -94,6 +94,7 @@ module.exports = env => { return { entry: { popup: path.join(__dirname, srcDir + 'popup.ts'), + popup2: path.join(__dirname, srcDir + 'popup/popup.tsx'), background: path.join(__dirname, srcDir + 'background.ts'), content: path.join(__dirname, srcDir + 'content.ts'), options: path.join(__dirname, srcDir + 'options.ts'),