Variables
Const AnimeSearchDialog
AnimeSearchDialog: ComponentClass<object & StyledComponentProps<"content" | "grow" | "inputInput" | "inputRoot" | "search" | "searchIcon"> | object & StyledComponentProps<"content" | "grow" | "inputInput" | "inputRoot" | "search" | "searchIcon">, any> | FunctionComponent<object & StyledComponentProps<"content" | "grow" | "inputInput" | "inputRoot" | "search" | "searchIcon"> | object & StyledComponentProps<"content" | "grow" | "inputInput" | "inputRoot" | "search" | "searchIcon">> = withStyles(styles)(withMobileDialog<AnimeSearchDialogProps>()(class extends React.Component<AnimeSearchDialogProps, AnimeSearchDialogState> {public searchDebounced = AwesomeDebouncePromise(async (query: string) => {await this.search(query);}, 250);constructor(props: AnimeSearchDialogProps) {super(props);this.state = {loading: true,};}public async handleDialogEnter() {const {animePage} = this.props;(async () => {const currentAnimeUID = await animePage.getAnimeUID();this.setState({currentAnimeUID});})();const searchQuery = await animePage.getAnimeSearchQuery();if (!searchQuery) {this.setState({loading: false});return;}await this.search(searchQuery);}public async search(query: string) {const {animePage} = this.props;const state = animePage.state;if (!query) {this.setState({results: undefined,searchQuery: query,});return;}this.setState({loading: true, searchQuery: query});let results: GrobberMedium[] | undefined;const config = await state.config;const searchResults = await remoteGrobberClient.searchAnime(query);const resultUIDs = new Set();if (searchResults) {let consideration = searchResults.filter(res => {// make absolutely sure that there are no duplicate UIDs// it shouldn't happen anyway, but Grobber seems to have// some issues which causes duplicatesconst uid = res.item.uid;if (resultUIDs.has(uid))return false;elseresultUIDs.add(uid);return res.certainty >= config.minCertaintyForSearchResult;});if (consideration.length === 0) consideration = searchResults;results = consideration.map(res => res.item);}this.setState({loading: false, results});}public handleSelect(medium: GrobberMedium) {const {currentAnimeUID} = this.state;if (medium.uid === currentAnimeUID) return;this.setState({currentAnimeUID: medium.uid, currentAnime: medium});}public renderContent(): React.ReactNode {const {loading, results, currentAnimeUID, searchQuery} = this.state;const handleSelect = this.handleSelect.bind(this);if (loading) {return (<CircularProgress/>);} else if (results && results.length > 0) {// noinspection RequiredAttributesreturn (<AnimeSearchSelectionmedia={results}currentUID={currentAnimeUID}onSelect={handleSelect}/>);} else if (!searchQuery) {return (<Typography variant="overline">{_("anime__search__no_search_query")}</Typography>);} else {return (<Typography variant="overline">{_("anime__search__no_results")}</Typography>);}}public render(): React.ReactNode {const {classes, open, onClose, fullScreen} = this.props;const {searchQuery, currentAnime} = this.state;const handleDialogEnter = this.handleDialogEnter.bind(this);const handleClose = () => onClose && onClose();const handleSearchInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) =>this.searchDebounced(event.target.value);const searchInputClasses = {input: classes.inputInput,root: classes.inputRoot,};let searchInputField;// searchQuery is only undefined right at the beginningif (searchQuery !== undefined)searchInputField = (<div className={classes.search}><div className={classes.searchIcon}><SearchIcon/></div><InputBaseplaceholder={_("anime__search__search_placeholder")}defaultValue={searchQuery}onChange={handleSearchInputChange}classes={searchInputClasses}/></div>);let pickButton;if (currentAnime) {const handlePickClick = () => onClose && onClose(currentAnime);pickButton = (<ButtononClick={handlePickClick}variant="contained"color="primary">{_("anime__search__pick")}</Button>);}return (<DialogfullScreen={fullScreen}maxWidth="md"fullWidthopen={open}onEnter={handleDialogEnter}onBackdropClick={handleClose}scroll="paper"aria-labelledby="anime-search-result-dialog-title"style={{zIndex: 10000}}><Toolbar><DialogTitle id="anime-search-result-dialog-title">{_("anime__search__title")}</DialogTitle><div className={classes.grow}/>{searchInputField}</Toolbar><DialogContent className={classes.content}>{this.renderContent()}</DialogContent><DialogActions><Button onClick={handleClose} color="primary">{_("anime__search__abort")}</Button>{pickButton}</DialogActions></Dialog>);}},))
Const AnimeSearchSelection
AnimeSearchSelection: ComponentClass<object & StyledComponentProps<"listTile">, any> | FunctionComponent<object & StyledComponentProps<"listTile">> = withStyles(styles, {withTheme: true})(class extends React.Component<AnimeSelectionProps> {public onSelect(medium: GrobberMedium) {const {onSelect} = this.props;if (onSelect) onSelect(medium);}public render(): React.ReactNode {const {theme, media} = this.props;const renderAnimeTile = this.renderTile.bind(this);return (<GridList cellHeight="auto" cols={4} spacing={theme.spacing(2)}>{media.map(renderAnimeTile)}</GridList>);}private renderTile(medium: GrobberMedium) {const {classes, currentUID} = this.props;const handleSelect = () => this.onSelect(medium);return (<GridListTile key={medium.uid} classes={{tile: classes.listTile}}><AnimeSelectionItemmedium={medium}current={currentUID === medium.uid}onClick={handleSelect}/></GridListTile>);}},)
Const ContinueWatchingButton
ContinueWatchingButton: ComponentClass<object & StyledComponentProps<"buttonIconLeft">, any> | FunctionComponent<object & StyledComponentProps<"buttonIconLeft">> = withStyles(styles)(class extends React.Component<ContinueWatchingButtonProps, ContinueWatchingButtonState> {private episodesWatchedSub?: Subscription;constructor(props: ContinueWatchingButtonProps) {super(props);this.state = {buttonText: _("anime__continue_watching__loading"),disabled: true,tooltip: _("anime__continue_watching__loading"),};}public async componentDidMount() {const {animePage} = this.props;this.episodesWatchedSub = (await animePage.getEpisodesWatched$()).subscribe((epsWatched) => this.handleEpisodesWatchedChanged(epsWatched));}public componentWillUnmount(): void {if (this.episodesWatchedSub) this.episodesWatchedSub.unsubscribe();}public async showEpisode(episodeIndex: number) {const {animePage} = this.props;const success = await animePage.showEpisode(episodeIndex);if (!success)animePage.service.showErrorSnackbar(_("anime__show_episode__failed"));}public async handleEpisodesWatchedChanged(epsWatched?: number) {const {animePage} = this.props;const anime = await animePage.getAnime();if (!anime) {const msg = _("anime__continue_watching__anime_unknown");this.setState({disabled: true,tooltip: msg,});animePage.service.showWarningSnackbar(msg);return;}let buttonText;// either undefined or 0 i.e. the user hasn't started the Animeif (!epsWatched) {const msg = _("anime__continue_watching__unknown");this.setState({tooltip: msg,disabled: true,});epsWatched = 0;buttonText = _("anime__continue_watching__start");} else {buttonText = _("anime__continue_watching");}if (anime.episodes > epsWatched) {const href = await animePage.getEpisodeURL(epsWatched);if (href) {this.setState({buttonText,disabled: false,href,onClick: () => this.showEpisode(epsWatched as number),tooltip: _("anime__continue_watching__available", [epsWatched + 1]),});} else {// this isn't technically the truth but sometimes it's easier to liethis.setState({buttonText,disabled: true,tooltip: _("anime__continue_watching__unavailable", [epsWatched + 1]),});}} else {const totalEpisodes = await animePage.getEpisodeCount();if (epsWatched === totalEpisodes) {this.setState({buttonText,disabled: true,tooltip: _("anime__continue_watching__completed"),});} else {this.setState({buttonText,disabled: true,tooltip: _("anime__continue_watching__unavailable", [epsWatched + 1]),});}}}public render() {const {classes} = this.props;const {tooltip, buttonText, href, onClick, disabled} = this.state;const handleClick = (event: React.MouseEvent) => {event.preventDefault();if (onClick)onClick();};return (<Tooltip title={tooltip}><div><Buttonvariant="contained"color="primary"onClick={handleClick}fullWidth{...{href, disabled}}><PlayCircleIcon className={classes.buttonIconLeft}/>{buttonText}</Button></div></Tooltip>);}},)
Const EpisodeEmbed
EpisodeEmbed: ComponentClass<object & StyledComponentProps<"root" | "buttonIconRight" | "flexCenterColumn" | "playerBar">, any> | FunctionComponent<object & StyledComponentProps<"root" | "buttonIconRight" | "flexCenterColumn" | "playerBar">> = withStyles(styles)(class extends React.Component<EpisodeEmbedProps, EpisodeEmbedState> {public episodeBookmarkedSubscription?: rxjs.Subscription;constructor(props: EpisodeEmbedProps) {super(props);this.state = {debugMode: false,bookmarked: false,canSetProgress: false,playersAvailable: [],};}public componentWillUnmount() {if (this.episodeBookmarkedSubscription) this.episodeBookmarkedSubscription.unsubscribe();}public async componentDidMount() {const {episodePage} = this.props;this.episodeBookmarkedSubscription = episodePage.episodeBookmarked$.subscribe({next: (episodeBookmarked) => this.setState({bookmarked: episodeBookmarked}),});const config = await episodePage.state.config;this.setState({debugMode: config.debugMode});const canSetProgress = await episodePage.animePage.canSetEpisodesWatched();this.setState({canSetProgress});const [epIndex, episode] = await Promise.all([episodePage.getEpisodeIndex(),episodePage.getEpisode(),]);if (!episode) {this.setState({currentPlayer: PlayerType.NONE, failReason: "episode_unavailable"});return;}const epEmbeds = episode.embeds;const epStream = episode.stream;if (epEmbeds.length === 0 && !(epStream && epStream.links.length > 0)) {this.setState({currentPlayer: PlayerType.NONE, failReason: "no_streams"});return;}const updateState: Partial<EpisodeEmbedState> = {playersAvailable: [],};if (epEmbeds.length > 0) {const embedInfos = prepareEmbedInfos(epEmbeds, config.embedProviders);if (embedInfos.length > 0) {updateState.currentPlayer = PlayerType.EMBED;// @ts-ignoreupdateState.playersAvailable.push(PlayerType.EMBED);updateState.episodeEmbeds = embedInfos;}}if (epStream && epStream.links.length > 0) {const sources: PlayerSource[] = epStream.links.map(link => {return {url: link};});if (config.preferDolosPlayer)updateState.currentPlayer = PlayerType.DOLOS;// @ts-ignoreupdateState.playersAvailable.push(PlayerType.DOLOS);updateState.playerProps = {eventListener: {ended: () => episodePage.onEpisodeEnd()},options: {autoplay: config.autoplay,title: _("player__video_title_format", [episode.anime.title,epIndex !== undefined ? epIndex + 1 : "?",]),},poster: episode.poster,sources,};}if (updateState.currentPlayer === PlayerType.NONE)updateState.failReason = "no_streams";this.setState(updateState as EpisodeEmbedState);const loadSkipButtons = (async () => {if (epIndex === undefined) return;const prevEpPromise = epIndex > 0 ? episodePage.prevEpisodeButton() : Promise.resolve(null);const nextEpPromise = epIndex < episode.anime.episodes - 1? episodePage.nextEpisodeButton(): Promise.resolve(null);const skipButtons = await Promise.all([prevEpPromise, nextEpPromise]) as [SkipButton, SkipButton];this.setState({skipButtons});})();await Promise.all([loadSkipButtons]);}public render() {const {classes} = this.props;return (<div className={classes.root}>{this.renderPlayer()}<Paper className={classes.playerBar}><div>{this.renderSkipButtons()}{this.renderBookmarkButton()}</div><div style={{display: "flex"}}>{this.renderSwitchPlayerTypeButton()}{this.renderMenuButton()}{this.renderOpenDebugDialogButton()}</div></Paper></div>);}private getNextPlayerType(): PlayerType {switch (this.state.currentPlayer) {case PlayerType.DOLOS:return PlayerType.EMBED;case PlayerType.EMBED:return PlayerType.DOLOS;default:throw new Error(`Unhandled player type: ${this.state.currentPlayer} cannot switch!`);}}private switchPlayerType(): void {const nextPlayerType = this.getNextPlayerType();this.setState({currentPlayer: nextPlayerType});}private renderPlayer(): React.ReactElement<any> {const {classes} = this.props;const {currentPlayer,failReason,episodeEmbeds,playerProps,} = this.state;const view = () => {switch (currentPlayer) {case PlayerType.DOLOS:return (<Player {...playerProps as PlayerProps}/>);case PlayerType.EMBED:return (// @ts-ignore<EmbedPlayer embeds={episodeEmbeds}/>);case PlayerType.NONE:const msgName = failReason ? `episode__error__${failReason}` : "episode__error__general";return (<WithRatio ratio={16 / 9}><div className={classes.flexCenterColumn}><MoodBadIcon fontSize="large" color="primary"/><Typography variant="h4" color="textPrimary">{_(msgName)}</Typography></div></WithRatio>);default:return (<WithRatio ratio={16 / 9}><CircularProgress/></WithRatio>);}};return view();}private renderSkipButtons() {const {skipButtons} = this.state;const [skipPrev, skipNext] = skipButtons || [undefined, undefined];let handleSkipPrevClick;if (skipPrev && skipPrev.onClick)handleSkipPrevClick = (e: React.MouseEvent) => {e.preventDefault();// @ts-ignoreskipPrev.onClick(e);};let handleSkipNextClick;if (skipNext && skipNext.onClick)handleSkipNextClick = (e: React.MouseEvent) => {e.preventDefault();// @ts-ignoreskipNext.onClick(e);};return (<span><Tooltip title={_("episode__skip_previous")}><span>{/* TODO Material-UI doesn't accept the "href" prop */}<IconButtoncolor="primary"aria-label={_("episode__skip_previous")}disabled={!skipPrev}onClick={handleSkipPrevClick}{...{href: skipPrev && skipPrev.href}}><SkipPreviousIcon/></IconButton></span></Tooltip><Tooltip title={_("episode__skip_next")}><span>{/* TODO Material-UI doesn't accept the "href" prop */}<IconButtoncolor="primary"aria-label={_("episode__skip_next")}disabled={!skipNext}onClick={handleSkipNextClick}{...{href: skipNext && skipNext.href}}><SkipNextIcon/></IconButton></span></Tooltip></span>);}private async toggleBookmark() {const {episodePage} = this.props;const {bookmarked} = this.state;if (bookmarked) await episodePage.markEpisodeUnwatched();else await episodePage.markEpisodeWatched();}private renderBookmarkButton() {const {bookmarked, canSetProgress} = this.state;const handleToggle = () => this.toggleBookmark();return (<Toggletooltip={_("episode__bookmark_unseen")}tooltipToggled={_("episode__bookmark_seen")}icon={<BookmarkBorderIcon/>}iconToggled={<BookmarkIcon/>}toggled={bookmarked}canToggle={canSetProgress}onToggle={handleToggle}/>);}private renderSwitchPlayerTypeButton() {const {classes} = this.props;const {playersAvailable} = this.state;if (playersAvailable.length > 1) {const handleClick = this.switchPlayerType.bind(this);return (<Tooltip title={_("episode__switch_player_type")}><Button color="primary" onClick={handleClick}>{getPlayerTypeName(this.getNextPlayerType())}<SwitchVideoIcon className={classes.buttonIconRight}/></Button></Tooltip>);}return undefined;}private async handleSearchDialogClose(anime?: AnimeInfo) {// close the menu because it's served its purposethis.setState({menuAnchorElement: undefined});if (!anime) return;const {episodePage} = this.props;await episodePage.animePage.setAnimeUID(anime);}private renderMenuButton() {const {episodePage} = this.props;const {menuAnchorElement} = this.state;const handleSearchAnimeClick = async () => {await episodePage.animePage.openAnimeSearchDialog(this.handleSearchDialogClose.bind(this));};const onMenuClick = (event: React.MouseEvent<HTMLElement>) =>this.setState({menuAnchorElement: event.currentTarget});const onMenuClose = () => this.setState({menuAnchorElement: undefined});return (<div><IconButtonaria-label="More"aria-owns={open ? "episode-embed-menu" : undefined}aria-haspopup="true"color="primary"onClick={onMenuClick}><MoreVertIcon/></IconButton><Menuid="episode-embed-menu"anchorEl={menuAnchorElement}open={!!menuAnchorElement}onClose={onMenuClose}><MenuItem onClick={handleSearchAnimeClick}>{_("episode__menu__search_anime")}</MenuItem></Menu></div>);}private renderOpenDebugDialogButton() {const {debugMode} = this.state;if (!debugMode) return undefined;const {episodePage} = this.props;return (<OpenDebugDialogButton service={episodePage.service}/>);}},)
Const placeholderImage
placeholderImage: string = chrome.runtime.getURL("/img/broken_image.svg")
Const useItemStyles
useItemStyles: function = makeStyles({placeholder: {backgroundSize: "contain",filter: "opacity(.5)",},thumbnail: {paddingTop: `${100 * 40 / 27}%`,},})
Type declaration
-
- (props?: any): ClassNameMap<ClassKeyOfStyles<S>>
-
Parameters
Returns ClassNameMap<ClassKeyOfStyles<S>>
React components related to anime.