Add a section in options for unsubmitted segments

This commit is contained in:
mini-bomba
2022-08-19 21:05:45 +02:00
committed by Ajay
parent 2b5a02e068
commit 9915d46ad4
7 changed files with 274 additions and 29 deletions

View File

@@ -1153,5 +1153,37 @@
}, },
"chaptersPage1": { "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" "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} unsubmitted segments on {1} 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"
} }
} }

View File

@@ -490,7 +490,11 @@
<div class="small-description">__MSG_copyDebugInformationOptions__</div> <div class="small-description">__MSG_copyDebugInformationOptions__</div>
</div> </div>
<div data-type="react-UnsubmittedVideosComponent">
</div>
<div data-type="toggle" data-sync="testingServer" data-confirm-message="testingServerWarning" data-no-safari="true"> <div data-type="toggle" data-sync="testingServer" data-confirm-message="testingServerWarning" data-no-safari="true">
<div class="switch-container"> <div class="switch-container">
<label class="switch"> <label class="switch">

View File

@@ -0,0 +1,71 @@
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">
<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,64 @@
import * as React from "react";
import Config from "../config"
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 + "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")
}
}
export default UnsubmittedVideoListItem;

View File

@@ -0,0 +1,51 @@
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);
// Setup state
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>
{chrome.i18n.getMessage("unsubmittedSegmentCounts").replace("{0}", segmentCount.toString()).replace("{1}", videoCount.toString())}
</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

@@ -10,6 +10,7 @@ window.SB = Config;
import Utils from "./utils"; import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser"; import CategoryChooser from "./render/CategoryChooser";
import UnsubmittedVideos from "./render/UnsubmittedVideos";
import KeybindComponent from "./components/options/KeybindComponent"; import KeybindComponent from "./components/options/KeybindComponent";
import { showDonationLink } from "./utils/configUtils"; import { showDonationLink } from "./utils/configUtils";
import { localizeHtmlPage } from "./utils/pageUtils"; import { localizeHtmlPage } from "./utils/pageUtils";
@@ -103,7 +104,7 @@ async function init() {
// Add click listener // Add click listener
checkbox.addEventListener("click", async () => { checkbox.addEventListener("click", async () => {
// Confirm if required // Confirm if required
if (confirmMessage && ((confirmOnTrue && checkbox.checked) || (!confirmOnTrue && !checkbox.checked)) if (confirmMessage && ((confirmOnTrue && checkbox.checked) || (!confirmOnTrue && !checkbox.checked))
&& !confirm(chrome.i18n.getMessage(confirmMessage))){ && !confirm(chrome.i18n.getMessage(confirmMessage))){
checkbox.checked = !checkbox.checked; checkbox.checked = !checkbox.checked;
return; return;
@@ -120,7 +121,7 @@ async function init() {
if (!checkbox.checked) { if (!checkbox.checked) {
// Enable the notice // Enable the notice
Config.config["dontShowNotice"] = false; Config.config["dontShowNotice"] = false;
const showNoticeSwitch = <HTMLInputElement> document.querySelector("[data-sync='dontShowNotice'] > div > label > input"); const showNoticeSwitch = <HTMLInputElement> document.querySelector("[data-sync='dontShowNotice'] > div > label > input");
showNoticeSwitch.checked = true; showNoticeSwitch.checked = true;
} }
@@ -162,7 +163,7 @@ async function init() {
} }
case "text-change": { case "text-change": {
const textChangeInput = <HTMLInputElement> optionsElements[i].querySelector(".option-text-box"); const textChangeInput = <HTMLInputElement> optionsElements[i].querySelector(".option-text-box");
const textChangeSetButton = <HTMLElement> optionsElements[i].querySelector(".text-change-set"); const textChangeSetButton = <HTMLElement> optionsElements[i].querySelector(".text-change-set");
textChangeInput.value = Config.config[option]; textChangeInput.value = Config.config[option];
@@ -292,6 +293,9 @@ async function init() {
case "react-CategoryChooserComponent": case "react-CategoryChooserComponent":
new CategoryChooser(optionsElements[i]); new CategoryChooser(optionsElements[i]);
break; break;
case "react-UnsubmittedVideosComponent":
new UnsubmittedVideos(optionsElements[i])
break;
} }
} }
@@ -338,8 +342,8 @@ function createStickyHeader() {
/** /**
* Handle special cases where an option shouldn't show * Handle special cases where an option shouldn't show
* *
* @param {String} element * @param {String} element
*/ */
async function shouldHideOption(element: Element): Promise<boolean> { async function shouldHideOption(element: Element): Promise<boolean> {
return (element.getAttribute("data-private-only") === "true" && !(await isIncognitoAllowed())) return (element.getAttribute("data-private-only") === "true" && !(await isIncognitoAllowed()))
@@ -348,8 +352,8 @@ async function shouldHideOption(element: Element): Promise<boolean> {
/** /**
* Called when the config is updated * Called when the config is updated
* *
* @param {String} element * @param {String} element
*/ */
function optionsConfigUpdateListener() { function optionsConfigUpdateListener() {
const optionsContainer = document.getElementById("options"); const optionsContainer = document.getElementById("options");
@@ -359,14 +363,18 @@ function optionsConfigUpdateListener() {
switch (optionsElements[i].getAttribute("data-type")) { switch (optionsElements[i].getAttribute("data-type")) {
case "display": case "display":
updateDisplayElement(<HTMLElement> optionsElements[i]) updateDisplayElement(<HTMLElement> optionsElements[i])
break;
case "react-UnsubmittedVideosComponent":
new UnsubmittedVideos(optionsElements[i])
break;
} }
} }
} }
/** /**
* Will set display elements to the proper text * Will set display elements to the proper text
* *
* @param element * @param element
*/ */
function updateDisplayElement(element: HTMLElement) { function updateDisplayElement(element: HTMLElement) {
const displayOption = element.getAttribute("data-sync") const displayOption = element.getAttribute("data-sync")
@@ -393,9 +401,9 @@ function updateDisplayElement(element: HTMLElement) {
/** /**
* Initializes the option to add Invidious instances * Initializes the option to add Invidious instances
* *
* @param element * @param element
* @param option * @param option
*/ */
function invidiousInstanceAddInit(element: HTMLElement, option: string) { function invidiousInstanceAddInit(element: HTMLElement, option: string) {
const textBox = <HTMLInputElement> element.querySelector(".option-text-box"); const textBox = <HTMLInputElement> element.querySelector(".option-text-box");
@@ -447,9 +455,9 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
/** /**
* Run when the invidious button is being initialized * Run when the invidious button is being initialized
* *
* @param checkbox * @param checkbox
* @param option * @param option
*/ */
function invidiousInit(checkbox: HTMLInputElement, option: string) { function invidiousInit(checkbox: HTMLInputElement, option: string) {
utils.containsInvidiousPermission().then((result) => { utils.containsInvidiousPermission().then((result) => {
@@ -463,9 +471,9 @@ function invidiousInit(checkbox: HTMLInputElement, option: string) {
/** /**
* Run whenever the invidious checkbox is clicked * Run whenever the invidious checkbox is clicked
* *
* @param checkbox * @param checkbox
* @param option * @param option
*/ */
async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Promise<void> { async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Promise<void> {
const enabled = await utils.applyInvidiousPermissions(checkbox.checked, option); const enabled = await utils.applyInvidiousPermissions(checkbox.checked, option);
@@ -474,8 +482,8 @@ async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Pro
/** /**
* Will trigger the textbox to appear to be able to change an option's text. * Will trigger the textbox to appear to be able to change an option's text.
* *
* @param element * @param element
*/ */
function activatePrivateTextChange(element: HTMLElement) { function activatePrivateTextChange(element: HTMLElement) {
const button = element.querySelector(".trigger-button"); const button = element.querySelector(".trigger-button");
@@ -492,7 +500,7 @@ function activatePrivateTextChange(element: HTMLElement) {
element.querySelector(".option-hidden-section").classList.remove("hidden"); element.querySelector(".option-hidden-section").classList.remove("hidden");
return; return;
} }
let result = Config.config[option]; let result = Config.config[option];
// See if anything extra must be done // See if anything extra must be done
switch (option) { switch (option) {
@@ -503,7 +511,7 @@ function activatePrivateTextChange(element: HTMLElement) {
} }
textBox.value = result; textBox.value = result;
const setButton = element.querySelector(".text-change-set"); const setButton = element.querySelector(".text-change-set");
setButton.addEventListener("click", async () => { setButton.addEventListener("click", async () => {
setTextOption(option, element, textBox.value); setTextOption(option, element, textBox.value);
@@ -532,7 +540,7 @@ function activatePrivateTextChange(element: HTMLElement) {
/** /**
* Function to run when a textbox change is submitted * Function to run when a textbox change is submitted
* *
* @param option data-sync value * @param option data-sync value
* @param element main container div * @param element main container div
* @param value new text * @param value new text
@@ -542,7 +550,7 @@ async function setTextOption(option: string, element: HTMLElement, value: string
const confirmMessage = element.getAttribute("data-confirm-message"); const confirmMessage = element.getAttribute("data-confirm-message");
if (confirmMessage === null || confirm(chrome.i18n.getMessage(confirmMessage))) { if (confirmMessage === null || confirm(chrome.i18n.getMessage(confirmMessage))) {
// See if anything extra must be done // See if anything extra must be done
switch (option) { switch (option) {
case "*": case "*":
@@ -554,13 +562,13 @@ async function setTextOption(option: string, element: HTMLElement, value: string
if (newConfig.supportInvidious) { if (newConfig.supportInvidious) {
const checkbox = <HTMLInputElement> document.querySelector("#support-invidious > div > label > input"); const checkbox = <HTMLInputElement> document.querySelector("#support-invidious > div > label > input");
checkbox.checked = true; checkbox.checked = true;
await invidiousOnClick(checkbox, "supportInvidious"); await invidiousOnClick(checkbox, "supportInvidious");
} }
window.location.reload(); window.location.reload();
} catch (e) { } catch (e) {
alert(chrome.i18n.getMessage("incorrectlyFormattedOptions")); alert(chrome.i18n.getMessage("incorrectlyFormattedOptions"));
} }
@@ -603,7 +611,7 @@ function uploadConfig(e) {
/** /**
* Validates the value used for the database server address. * Validates the value used for the database server address.
* Returns null and alerts the user if there is an issue. * Returns null and alerts the user if there is an issue.
* *
* @param input Input server address * @param input Input server address
*/ */
function validateServerAddress(input: string): string { function validateServerAddress(input: string): string {
@@ -637,7 +645,7 @@ function copyDebugOutputToClipboard() {
// Sanitise sensitive user config values // Sanitise sensitive user config values
delete output.config.userID; delete output.config.userID;
output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress) output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress)
? "Default server address" : "Custom server address"; ? "Default server address" : "Custom server address";
output.config.invidiousInstances = output.config.invidiousInstances.length; output.config.invidiousInstances = output.config.invidiousInstances.length;
output.config.whitelistedChannels = output.config.whitelistedChannels.length; output.config.whitelistedChannels = output.config.whitelistedChannels.length;

View File

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