Merge pull request #1446 from mini-bomba/clearUnsubmittedSegments

Add a section in options for unsubmitted segments
This commit is contained in:
Ajay Ramachandran
2022-09-02 00:17:57 -04:00
committed by GitHub
9 changed files with 390 additions and 59 deletions

View File

@@ -1153,5 +1153,60 @@
},
"chaptersPage1": {
"message": "SponsorBlock crowd-sourced chapters feature is only available to people who purchase a license, or for people who are granted access for free due their past contributions"
},
"unsubmittedSegmentCounts": {
"message": "You currently have {0} on {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
},
"unsubmittedSegmentCountsZero": {
"message": "You currently have no unsubmitted segments",
"description": "Replaces 'unsubmittedSegmentCounts' string when there are no unsubmitted segments"
},
"unsubmittedSegmentsSingular": {
"message": "unsubmitted segment",
"description": "Example: You currently have 1 *unsubmitted segment* on 1 video"
},
"unsubmittedSegmentsPlural": {
"message": "unsubmitted segments",
"description": "Example: You currently have 12 *unsubmitted segments* on 5 videos"
},
"videosSingular": {
"message": "video",
"description": "Example: You currently have 3 unsubmitted segments on 1 *video*"
},
"videosPlural": {
"message": "videos",
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "Clear all segments",
"description": "Label for a button in settings"
},
"clearUnsubmittedSegmentsConfirm": {
"message": "Are you sure you want to clear all your unsubmitted segments?",
"description": "Confirmation message for the Clear unsubmitted segments button"
},
"showUnsubmittedSegments": {
"message": "Show segments",
"description": "Show/hide button for the unsubmitted segments list"
},
"hideUnsubmittedSegments": {
"message": "Hide segments",
"description": "Show/hide button for the unsubmitted segments list"
},
"videoID": {
"message": "Video ID",
"description": "Header of the unsubmitted segments list"
},
"segmentCount": {
"message": "Segment Count",
"description": "Header of the unsubmitted segments list"
},
"actions": {
"message": "Actions",
"description": "Header of the unsubmitted segments list"
},
"exportSegmentsAsURL": {
"message": "Share as URL"
}
}

View File

@@ -363,6 +363,8 @@
</div>
</div>
<div data-type="react-UnsubmittedVideosComponent"></div>
<div data-type="private-text-change" data-sync="*" data-confirm-message="exportOptionsWarning">
<h2>__MSG_exportOptions__</h2>

View File

@@ -0,0 +1,72 @@
import * as React from "react";
import Config from "../../config";
import UnsubmittedVideoListItem from "./UnsubmittedVideoListItem";
export interface UnsubmittedVideoListProps {
}
export interface UnsubmittedVideoListState {
}
class UnsubmittedVideoListComponent extends React.Component<UnsubmittedVideoListProps, UnsubmittedVideoListState> {
constructor(props: UnsubmittedVideoListProps) {
super(props);
// Setup state
this.state = {
};
}
render(): React.ReactElement {
// Render nothing if there are no unsubmitted segments
if (Object.keys(Config.config.unsubmittedSegments).length == 0)
return <></>;
return (
<table id="unsubmittedVideosList"
className="categoryChooserTable"
style={{marginTop: "10px"}} >
<tbody>
{/* Headers */}
<tr id="UnsubmittedVideosListHeader"
className="categoryTableElement categoryTableHeader">
<th id="UnsubmittedVideoID">
{chrome.i18n.getMessage("videoID")}
</th>
<th id="UnsubmittedSegmentCount">
{chrome.i18n.getMessage("segmentCount")}
</th>
<th id="UnsubmittedVideoActions">
{chrome.i18n.getMessage("actions")}
</th>
</tr>
{this.getUnsubmittedVideos()}
</tbody>
</table>
);
}
getUnsubmittedVideos(): JSX.Element[] {
const elements: JSX.Element[] = [];
for (const videoID of Object.keys(Config.config.unsubmittedSegments)) {
elements.push(
<UnsubmittedVideoListItem videoID={videoID} key={videoID}>
</UnsubmittedVideoListItem>
);
}
return elements;
}
}
export default UnsubmittedVideoListComponent;

View File

@@ -0,0 +1,95 @@
import * as React from "react";
import Config from "../../config";
import { exportTimes, exportTimesAsHashParam } from "../../utils/exporter";
export interface UnsubmittedVideosListItemProps {
videoID: string;
}
export interface UnsubmittedVideosListItemState {
}
class UnsubmittedVideoListItem extends React.Component<UnsubmittedVideosListItemProps, UnsubmittedVideosListItemState> {
constructor(props: UnsubmittedVideosListItemProps) {
super(props);
// Setup state
this.state = {
};
}
render(): React.ReactElement {
const segmentCount = Config.config.unsubmittedSegments[this.props.videoID]?.length ?? 0;
return (
<>
<tr id={this.props.videoID + "UnsubmittedSegmentsRow"}
className="categoryTableElement">
<td id={this.props.videoID + "UnsubmittedVideoID"}
className="categoryTableLabel">
<a href={`https://youtu.be/${this.props.videoID}`}
target="_blank" rel="noreferrer">
{this.props.videoID}
</a>
</td>
<td id={this.props.videoID + "UnsubmittedSegmentCount"}>
{segmentCount}
</td>
<td id={this.props.videoID + "UnsubmittedVideoActions"}>
<div id={this.props.videoID + "ExportSegmentsAction"}
className="option-button inline low-profile"
onClick={this.exportSegments.bind(this)}>
{chrome.i18n.getMessage("exportSegments")}
</div>
{" "}
<div id={this.props.videoID + "ExportSegmentsAsURLAction"}
className="option-button inline low-profile"
onClick={this.exportSegmentsAsURL.bind(this)}>
{chrome.i18n.getMessage("exportSegmentsAsURL")}
</div>
{" "}
<div id={this.props.videoID + "ClearSegmentsAction"}
className="option-button inline low-profile"
onClick={this.clearSegments.bind(this)}>
{chrome.i18n.getMessage("clearTimes")}
</div>
</td>
</tr>
</>
);
}
clearSegments(): void {
if (confirm(chrome.i18n.getMessage("clearThis"))) {
delete Config.config.unsubmittedSegments[this.props.videoID];
Config.forceSyncUpdate("unsubmittedSegments");
}
}
exportSegments(): void {
this.copyToClipboard(exportTimes(Config.config.unsubmittedSegments[this.props.videoID]));
}
exportSegmentsAsURL(): void {
this.copyToClipboard(`https://youtube.com/watch?v=${this.props.videoID}${exportTimesAsHashParam(Config.config.unsubmittedSegments[this.props.videoID])}`)
}
copyToClipboard(text: string): void {
navigator.clipboard.writeText(text)
.then(() => {
alert(chrome.i18n.getMessage("CopiedExclamation"));
})
.catch(() => {
alert(chrome.i18n.getMessage("copyDebugInformationFailed"));
});
}
}
export default UnsubmittedVideoListItem;

View File

@@ -0,0 +1,55 @@
import * as React from "react";
import Config from "../../config";
import UnsubmittedVideoListComponent from "./UnsubmittedVideoListComponent";
export interface UnsubmittedVideosProps {
}
export interface UnsubmittedVideosState {
tableVisible: boolean,
}
class UnsubmittedVideosComponent extends React.Component<UnsubmittedVideosProps, UnsubmittedVideosState> {
constructor(props: UnsubmittedVideosProps) {
super(props);
this.state = {
tableVisible: false,
};
}
render(): React.ReactElement {
const videoCount = Object.keys(Config.config.unsubmittedSegments).length;
const segmentCount = Object.values(Config.config.unsubmittedSegments).reduce((acc: number, vid: Array<unknown>) => acc + vid.length, 0);
return <>
<div style={{marginBottom: "10px"}}>
{segmentCount == 0 ?
chrome.i18n.getMessage("unsubmittedSegmentCountsZero") :
chrome.i18n.getMessage("unsubmittedSegmentCounts")
.replace("{0}", `${segmentCount} ${chrome.i18n.getMessage("unsubmittedSegments" + (segmentCount == 1 ? "Singular" : "Plural"))}`)
.replace("{1}", `${videoCount} ${chrome.i18n.getMessage("videos" + (videoCount == 1 ? "Singular" : "Plural"))}`)
}
</div>
{videoCount > 0 && <div className="option-button inline" onClick={() => this.setState({tableVisible: !this.state.tableVisible})}>
{chrome.i18n.getMessage(this.state.tableVisible ? "hideUnsubmittedSegments" : "showUnsubmittedSegments")}
</div>}
{" "}
<div className="option-button inline" onClick={this.clearAllSegments}>
{chrome.i18n.getMessage("clearUnsubmittedSegments")}
</div>
{this.state.tableVisible && <UnsubmittedVideoListComponent/>}
</>;
}
clearAllSegments(): void {
if (confirm(chrome.i18n.getMessage("clearUnsubmittedSegmentsConfirm")))
Config.config.unsubmittedSegments = {};
}
}
export default UnsubmittedVideosComponent;

View File

@@ -1,11 +1,26 @@
import Config from "./config";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime, HashedValue } from "./types";
import { ContentContainer, Keybind } from "./types";
import {
ActionType,
Category,
CategorySkipOption,
ChannelIDInfo,
ChannelIDStatus,
ContentContainer,
HashedValue,
Keybind,
ScheduledTime,
SegmentUUID,
SkipToTimeParams,
SponsorHideType,
SponsorSourceType,
SponsorTime,
StorageChangesObject,
ToggleSkippable,
VideoID,
VideoInfo,
} from "./types";
import Utils from "./utils";
const utils = new Utils();
import PreviewBar, {PreviewBarSegment} from "./js-components/previewBar";
import PreviewBar, { PreviewBarSegment } from "./js-components/previewBar";
import SkipNotice from "./render/SkipNotice";
import SkipNoticeComponent from "./components/SkipNoticeComponent";
import SubmissionNotice from "./render/SubmissionNotice";
@@ -22,6 +37,8 @@ import { importTimes } from "./utils/exporter";
import { ChapterVote } from "./render/ChapterVote";
import { openWarningDialog } from "./utils/warnings";
const utils = new Utils();
// Hack to get the CSS loaded on permission-based sites (Invidious)
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
@@ -2343,6 +2360,7 @@ function checkForPreloadedSegment() {
UUID: GenericUtils.generateUserID() as SegmentUUID,
category: segment.category ? segment.category : Config.config.defaultCategory,
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
description: segment.description ?? "",
source: SponsorSourceType.Local
});

View File

@@ -10,6 +10,7 @@ window.SB = Config;
import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser";
import UnsubmittedVideos from "./render/UnsubmittedVideos";
import KeybindComponent from "./components/options/KeybindComponent";
import { showDonationLink } from "./utils/configUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
@@ -292,6 +293,9 @@ async function init() {
case "react-CategoryChooserComponent":
new CategoryChooser(optionsElements[i]);
break;
case "react-UnsubmittedVideosComponent":
new UnsubmittedVideos(optionsElements[i])
break;
}
}
@@ -359,6 +363,10 @@ function optionsConfigUpdateListener() {
switch (optionsElements[i].getAttribute("data-type")) {
case "display":
updateDisplayElement(<HTMLElement> optionsElements[i])
break;
case "react-UnsubmittedVideosComponent":
new UnsubmittedVideos(optionsElements[i])
break;
}
}
}

View File

@@ -0,0 +1,15 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import UnsubmittedVideosComponent from "../components/options/UnsubmittedVideosComponent";
class UnsubmittedVideos {
constructor(element: Element) {
ReactDOM.render(
<UnsubmittedVideosComponent/>,
element
);
}
}
export default UnsubmittedVideos;

View File

@@ -74,3 +74,14 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
return result;
}
export function exportTimesAsHashParam(segments: SponsorTime[]): string {
const hashparamSegments = segments.map(segment => ({
actionType: segment.actionType,
category: segment.category,
segment: segment.segment,
...(segment.description ? {description: segment.description} : {}) // don't include the description param if empty
}));
return `#segments=${JSON.stringify(hashparamSegments)}`;
}