storybook
tidusjar 2 years ago
parent 551da1a0aa
commit ee5aca215e

@ -3,11 +3,13 @@ using Moq.AutoMock;
using NUnit.Framework;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
using Ombi.Core.Authentication;
using Ombi.Core.Engine;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models.Requests;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Schedule.Jobs.Plex;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
@ -121,6 +123,53 @@ namespace Ombi.Schedule.Tests
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
}
[Test]
public async Task MovieRequestFromWatchList_DisabledUser()
{
_mocker.Setup<OmbiUserManager, Task<bool>>(x => x.IsInRoleAsync(It.IsAny<OmbiUser>(), OmbiRoles.Disabled)).ReturnsAsync(true);
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{
MediaContainer = new PlexWatchlist
{
Metadata = new List<Metadata>
{
new Metadata
{
type = "movie",
ratingKey = "abc"
}
}
}
});
_mocker.Setup<IPlexApi, Task<PlexWatchlistMetadataContainer>>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistMetadataContainer
{
MediaContainer = new PlexWatchlistMetadata
{
Metadata = new WatchlistMetadata[]
{
new WatchlistMetadata
{
Guid = new List<PlexGuids>
{
new PlexGuids
{
Id = "tmdb://123"
}
}
}
}
}
});
await _subject.Execute(_context.Object);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.Is<MovieRequestViewModel>(x => x.TheMovieDbId == 123)), Times.Never);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
_mocker.Verify<IMovieRequestEngine>(x => x.SetUser(It.Is<OmbiUser>(x => x.Id == "abc")), Times.Never);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
}
[Test]
public async Task MovieRequestFromWatchList_NoGuid()

@ -65,6 +65,12 @@ namespace Ombi.Schedule.Jobs.Plex
try
{
var isDisabled = await _ombiUserManager.IsInRoleAsync(user, OmbiRoles.Disabled);
if (isDisabled)
{
return;
}
_logger.LogDebug($"Starting Watchlist Import for {user.UserName} with token {user.MediaServerToken}");
var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
if (watchlist == null || !(watchlist.MediaContainer?.Metadata?.Any() ?? false))

@ -0,0 +1,15 @@
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
"framework": "@storybook/angular",
"core": {
"builder": "@storybook/builder-webpack5"
}
}

@ -0,0 +1,14 @@
import { setCompodocJson } from "@storybook/addon-docs/angular";
import docJson from "../documentation.json";
setCompodocJson(docJson);
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
docs: { inlineStories: true },
}

@ -0,0 +1,21 @@
{
"extends": "../src/tsconfig.app.json",
"compilerOptions": {
"types": [
"node"
],
"allowSyntheticDefaultImports": true
},
"exclude": [
"../src/test.ts",
"../src/**/*.spec.ts",
"../projects/**/*.spec.ts"
],
"include": [
"../src/**/*",
"../projects/**/*"
],
"files": [
"./typings.d.ts"
]
}

@ -0,0 +1,4 @@
declare module '*.md' {
const content: string;
export default content;
}

@ -0,0 +1,90 @@
{
"pipes": [],
"interfaces": [],
"injectables": [],
"guards": [],
"interceptors": [],
"classes": [],
"directives": [],
"components": [],
"modules": [],
"miscellaneous": {
"variables": [
{
"name": "bootstrap",
"ctype": "miscellaneous",
"subtype": "variable",
"file": "src/main.ts",
"deprecated": false,
"deprecationMessage": "",
"type": "",
"defaultValue": "() => platformBrowserDynamic().bootstrapModule(AppModule)"
},
{
"name": "module",
"ctype": "miscellaneous",
"subtype": "variable",
"file": "src/main.ts",
"deprecated": false,
"deprecationMessage": "",
"type": "any"
}
],
"functions": [],
"typealiases": [],
"enumerations": [],
"groupedVariables": {
"src/main.ts": [
{
"name": "bootstrap",
"ctype": "miscellaneous",
"subtype": "variable",
"file": "src/main.ts",
"deprecated": false,
"deprecationMessage": "",
"type": "",
"defaultValue": "() => platformBrowserDynamic().bootstrapModule(AppModule)"
},
{
"name": "module",
"ctype": "miscellaneous",
"subtype": "variable",
"file": "src/main.ts",
"deprecated": false,
"deprecationMessage": "",
"type": "any"
}
]
},
"groupedFunctions": {},
"groupedEnumerations": {},
"groupedTypeAliases": {}
},
"routes": [],
"coverage": {
"count": 0,
"status": "low",
"files": [
{
"filePath": "src/main.ts",
"type": "variable",
"linktype": "miscellaneous",
"linksubtype": "variable",
"name": "bootstrap",
"coveragePercent": 0,
"coverageCount": "0/1",
"status": "low"
},
{
"filePath": "src/main.ts",
"type": "variable",
"linktype": "miscellaneous",
"linksubtype": "variable",
"name": "module",
"coveragePercent": 0,
"coverageCount": "0/1",
"status": "low"
}
]
}
}

@ -5,7 +5,10 @@
"ng": "ng",
"start": "ng serve --port 3578 --configuration hmr",
"build": "node --max_old_space_size=6144 node_modules/@angular/cli/bin/ng build --prod",
"lint": "ng lint"
"lint": "ng lint",
"docs:json": "compodoc -p ./src/tsconfig.app.json -e json -d .",
"storybook": "yarn docs:json && start-storybook -p 6006",
"build-storybook": "yarn docs:json && build-storybook"
},
"private": true,
"dependencies": {
@ -67,9 +70,20 @@
"@angular/cli": "~13.2.0",
"@angular/compiler-cli": "^13.2.0",
"@angular/language-service": "^13.2.0",
"@babel/core": "^7.17.9",
"@compodoc/compodoc": "^1.1.19",
"@storybook/addon-actions": "^6.4.22",
"@storybook/addon-essentials": "^6.4.22",
"@storybook/addon-interactions": "^6.4.22",
"@storybook/addon-links": "^6.4.22",
"@storybook/angular": "^6.4.22",
"@storybook/builder-webpack5": "^6.4.22",
"@storybook/manager-webpack5": "^6.4.22",
"@storybook/testing-library": "^0.0.9",
"@types/jasmine": "~3.6.7",
"@types/jasminewd2": "~2.0.8",
"@types/node": "^16.10.9",
"babel-loader": "^8.2.4",
"codelyzer": "^6.0.1",
"typescript": "~4.5.5"
},

@ -14,6 +14,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { BrowserModule } from "@angular/platform-browser";
import { ButtonModule } from "primeng/button";
import { CUSTOMIZATION_INITIALIZER } from "./state/customization/customization-initializer";
import { ComponentsModule } from "./components/components.module";
import { ConfirmDialogModule } from "primeng/confirmdialog";
import { CookieComponent } from "./auth/cookie.component";
import { CookieService } from "ng2-cookies";
@ -150,6 +151,7 @@ export function JwtTokenGetter() {
MatCheckboxModule,
MatProgressSpinnerModule,
MDBBootstrapModule.forRoot(),
ComponentsModule,
JwtModule.forRoot({
config: {
tokenGetter: JwtTokenGetter,

@ -0,0 +1,51 @@
<p-skeleton id="cardLoading{{result.id}}" width="100%" height="315px"></p-skeleton>
<div id="result{{result.id}}" class="ombi-card dark-card c" [style.display]="hide ? 'none' : 'block'">
<div class="card-top-info">
<div class="top-left" id="type{{result.id}}">
{{ 'Common.' + RequestType[result.type] | translate }}
</div>
<div class="{{getStatusClass()}} top-right" id="status{{result.id}}">
<span class="indicator"></span><span class="indicator-text" id="availabilityStatus{{result.id}}">{{getAvailabilityStatus()}}</span>
</div>
</div>
<img [routerLink]="generateDetailsLink()" id="cardImage" src="{{result.posterPath}}" class="image"
alt="{{result.title}}">
<div [ngClass]="result.posterPath.includes('images/') ? 'middle-show' : 'middle'">
<a class="poster-overlay" [routerLink]="generateDetailsLink()">
<div class="summary">
<div class="title" id="title{{result.id}}">{{result.title}}</div>
<div class="small-text ellipsis" id="overview{{result.id}}">{{result.overview}}</div>
</div>
</a>
</div>
<div [ngClass]="result.posterPath.includes('images/') ? 'button-request-container-show' : 'button-request-container'" class="row" *ngIf="!result.available && !result.approved && !result.requested">
<div *ngIf="is4kEnabled && requestable && result.type === RequestType.movie;then show4K else regular"></div>
<ng-template #show4K>
<div class="button-request poster-overlay">
<button [matMenuTriggerFor]="menu" id="requestButton{{result.id}}{{result.type}}{{discoverType}}" mat-raised-button class="btn-ombi full-width poster-request-btn">
<i *ngIf="!loading" class="fa-lg fas fa-cloud-download-alt"></i>
<i *ngIf="loading" class="fas fa-spinner fa-pulse fa-2x fa-fw" aria-hidden="true"></i>
{{'Common.Request' | translate }}
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item class="btn-ombi full-width poster-request-btn" (click)="request(false)">{{'Common.Request' | translate }}</button>
<button mat-menu-item class="btn-ombi full-width poster-request-btn" (click)="request(true)">{{'Common.Request4K' | translate }}</button>
</mat-menu>
</div>
</ng-template>
<ng-template #regular>
<div class="button-request poster-overlay">
<button id="requestButton{{result.id}}{{result.type}}{{discoverType}}" *ngIf="requestable" mat-raised-button class="btn-ombi full-width poster-request-btn" (click)="request($event, false)">
<i *ngIf="!loading" class="fa-lg fas fa-cloud-download-alt"></i>
<i *ngIf="loading" class="fas fa-spinner fa-pulse fa-2x fa-fw" aria-hidden="true"></i>
{{'Common.Request' | translate }}
</button>
</div>
</ng-template>
</div>
</div>

@ -0,0 +1,299 @@
@import "~styles/variables.scss";
#cardImage {
border-radius: 5px;
object-fit:cover;
}
.dark-card {
border-radius: 8px;
}
// Changed height to 100% to make all cards the same height
.card-spacing {
margin-top: 10%;
height: 100%;
}
.rating {
position: absolute;
font-weight: bold;
}
.expand {
text-align: center;
}
@media (min-width: 1025px) {
// Changed height to 100% to make all cards the same height
.grow {
transition: all .2s ease-in-out;
height: 100%;
}
.grow:hover {
transform: scale(1.1);
}
}
::ng-deep mat-dialog-container.mat-dialog-container {
// background-color: $ombi-primary;
// color: white;
border-radius: 2%
}
/* Title adjust for the Discover page */
.mat-card-content h6 {
overflow: hidden;
white-space: nowrap;
font-weight: 400;
font-size: 1.1rem;
}
/* Summary adjust for Discover page */
.small,
small {
font-size: 0.8rem;
}
#cardImage {
height: 100%;
object-fit: cover;
display: block;
}
.box {
position: relative;
max-width: 600px;
}
.ombi-card {
padding: 5px;
height:100%;
max-width: 200px;
max-height: 295px;
}
::ng-deep .p-carousel-indicators {
display: none !important;
}
.image {
border-radius: 10px;
opacity: 1;
display: block;
width: 100%;
height: auto;
transition: .5s ease;
backface-visibility: hidden;
}
.middle {
transition: .5s ease;
opacity: 0;
position: absolute;
top: 67%;
width: 90%;
left: 50%;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
}
.middle-show {
transition: .5s ease;
opacity: 1;
position: absolute;
top: 67%;
width: 90%;
left: 50%;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
}
.c {
position: relative;
}
.c:hover .image {
opacity: 0.3;
}
.c:hover .middle {
opacity: 1;
}
.small-text {
font-size: 11px;
}
.title {
font-size: 18px;
}
.full-width {
width: 100%;
}
.ellipsis {
display: -webkit-box;
-webkit-line-clamp: 6;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.card-top-info{
position: absolute;
top: 0px;
width: 100%;
background-color: rgba(15,23,31,0.7);
display:flex;
justify-content: space-between;
padding-top:0.6em;
padding-bottom:0.3em;
z-index:2;
letter-spacing: 0.2px;
}
.top-left {
font-size: 14px;
font-weight:200;
padding-left: 0.5em;
font-size: 14px;
}
/* common */
.top-right{
display:flex;
font-weight:200;
}
.top-right span.indicator, span.indicator-text {
display: none;
background-color: transparent;
color: #fff;
text-shadow: 0 1px 1px rgba(0,0,0,.2);
text-align: right;
font-size: 14px;
}
.top-right span.indicator{
padding-right: 0px;
}
.top-right span.indicator-text{
padding-right: 1em;
}
.top-right span.indicator:before{
content: '';
width: 10px;
height: 10px;
-moz-border-radius: 7.5px;
-webkit-border-radius: 7.5px;
border-radius: 7.5px;
margin-right:5px;
}
.top-right span.indicator, span.indicator-text{
display:block;
}
.top-right span.indicator:before{
display: inline-block;
}
.top-right.available span.indicator:before{
background-color: #1DE9B6;
}
.top-right.approved span.indicator:before{
background-color: #ffd740;
}
.top-right.denied span.indicator:before{
background-color: #660202;
}
.top-right.partly-available span.indicator:before{
background-color: #ffd740;
}
.top-right.requested span.indicator:before{
background-color: #ff5722;
}
::ng-deep a.poster-overlay{
color:#fff;
}
a.poster-overlay:hover{
text-decoration: none;
}
@media screen and (max-width: 400px){
.ellipsis{
display:none;
}
.top-right span.indicator-text{
display:none;
}
.top-right span.indicator{
padding-right:1em;
}
}
.ombi-card #cardImage:hover{
cursor: pointer;
}
.ombi-card .button-request-container{
position: relative;
width: 100%;
margin-left: 0;
margin-right: 0;
margin-top: -36px;
margin-bottom: 0px;
opacity:0;
transition: .5s ease;
}
.ombi-card .button-request-container-show{
position: relative;
width: 100%;
margin-left: 0;
margin-right: 0;
margin-top: -36px;
margin-bottom: 0px;
opacity:1;
transition: .5s ease;
}
::ng-deep .ombi-card .button-request-container .button-request{
padding-right:0px;
padding-left:0px;
width:100%;
}
::ng-deep .ombi-card .button-request-container-show .button-request{
padding-right:0px;
padding-left:0px;
width:100%;
}
.c:hover .button-request-container {
opacity:1;
}
.btn-ombi{
background-color:#293a4c;
}
::ng-deep .mat-menu-panel {
min-width: 190px !important;
}

@ -0,0 +1,87 @@
import { Component, Input } from "@angular/core";
import { DiscoverType } from "../../discover/components/carousel-list/carousel-list.component";
import { EventEmitter } from '@angular/core';
import { IDiscoverCardResult } from "../../discover/interfaces";
import { ISearchTvResultV2 } from "../../interfaces/ISearchTvResultV2";
import { Output } from "@angular/core";
import { RequestType } from "../../interfaces";
import { TranslateService } from "@ngx-translate/core";
@Component({
selector: "ombi-card",
templateUrl: "./card.component.html",
styleUrls: ["./card.component.scss"],
})
export default class CardComponent {
@Input() public discoverType: DiscoverType;
@Input() public result: IDiscoverCardResult;
@Input() public isAdmin: boolean;
@Input() public is4kEnabled: boolean = false;
@Output() public requested: EventEmitter<boolean> = new EventEmitter();
public RequestType = RequestType;
public hide: boolean;
public allow4KButton: boolean = false;
public requestable: boolean;
// This data is needed to open the dialog
private tvSearchResult: ISearchTvResultV2;
constructor(private translate: TranslateService) { }
public generateDetailsLink(): string {
switch (this.result.type) {
case RequestType.movie:
return `/details/movie/${this.result.id}`;
case RequestType.tvShow:
return `/details/tv/${this.result.id}`;
case RequestType.album: //Actually artist
return `/details/artist/${this.result.id}`;
}
}
public getStatusClass(): string {
if (this.result.available) {
return "available";
}
if (this.tvSearchResult?.partlyAvailable) {
return "partly-available";
}
if (this.result.approved) {
return "approved";
}
if (this.result.denied) {
return "denied";
}
if (this.result.requested) {
return "requested";
}
return "";
}
public getAvailabilityStatus(): string {
if (this.result.available) {
return this.translate.instant("Common.Available");
}
if (this.tvSearchResult?.partlyAvailable) {
return this.translate.instant("Common.PartlyAvailable");
}
if (this.result.approved) {
return this.translate.instant("Common.Approved");
}
if (this.result.denied) {
return this.translate.instant("Common.Denied");
}
if (this.result.requested) {
return this.translate.instant("Common.Pending");
}
return "";
}
public request(is4k: boolean) {
this.requested.emit(is4k);
}
}

@ -0,0 +1,36 @@
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { Meta, Story, moduleMetadata } from '@storybook/angular';
import { AppModule } from '../../app.module';
import Card from './card.component';
import { StorybookTranslateModule } from '../../../stories/storybook-translate-module';
// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
export default {
title: 'Ombi/Card',
component: Card,
// More on argTypes: https://storybook.js.org/docs/angular/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
decorators: [
moduleMetadata({
imports: [AppModule],
}),
],
} as Meta;
// More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
const Template: Story<Card> = (args: Card) => ({
props: args,
});
export const Primary = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
Primary.args = {
result: {
title: 'Title',
}
};

@ -0,0 +1,30 @@
import * as fromComponents from './';
import { CarouselModule } from 'primeng/carousel';
import { CommonModule } from '@angular/common';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import {MatButtonToggleModule} from '@angular/material/button-toggle';
import { NgModule } from "@angular/core";
import { PipeModule } from "../pipes/pipe.module";
import { RouterModule } from "@angular/router";
import { SharedModule } from "../shared/shared.module";
import { SkeletonModule } from 'primeng/skeleton';
import { TranslateModule } from '@ngx-translate/core';
@NgModule({
imports: [
SharedModule,
SkeletonModule,
TranslateModule,
RouterModule,
CommonModule,
],
declarations: [
...fromComponents.components
],
exports: [
RouterModule,
],
})
export class ComponentsModule { }

@ -0,0 +1,5 @@
import { CardComponent } from "./card/card.component";
export const components: any[] = [
CardComponent,
];

@ -1,198 +0,0 @@
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane active" id="MoviesTab">
<ng-template #FilterRef>
<div class="btn-group" role="group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
{{ 'Search.Suggestions' | translate }}
<i class="fa-list fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li><a (click)="popularMovies()" [translate]="'Search.Movies.PopularMovies'"></a></li>
<li><a (click)="upcomingMovies()" [translate]="'Search.Movies.UpcomingMovies'"></a></li>
<li><a (click)="topRatedMovies()" [translate]="'Search.Movies.TopRatedMovies'"></a></li>
<li><a (click)="nowPlayingMovies()" [translate]="'Search.Movies.NowPlayingMovies'"></a></li>
</ul>
<button class="btn btn-sm btn-primary-outline" (click)="refineOpen()">
{{ 'Search.Refine' | translate }}
<i class="fas" [ngClass]="{'fa-chevron-down': !refineSearchEnabled, 'fa-chevron-up': refineSearchEnabled}"></i>
</button>
</div>
</ng-template>
<div class="input-group search-bar-background">
<input id="search" type="text" placeholder="{{ 'Search.SearchBarPlaceholder' | translate}}"
class="form-control form-control-custom form-control-search form-control-withbuttons"
(keyup)="search($event)">
<div class="input-group-addon right-radius">
<div class="search-button-container-inline">
<ng-template [ngTemplateOutlet]="FilterRef"></ng-template>
</div>
<i class="fas fa-search"></i>
</div>
</div>
<div class="row search-button-container">
<ng-template [ngTemplateOutlet]="FilterRef"></ng-template>
</div>
<!-- Refine search options -->
<div class="row top-spacing form-group vcenter" *ngIf="refineSearchEnabled">
<div class="col-md-1">
<div class="form-group">
<label class="control-label">Year</label>
<input [(ngModel)]="searchYear" class="form-control form-control-custom refine-option">
</div>
</div>
<!-- <label for="name" class="col-xs-2 col-md-1">Language:</label> -->
<div class="col-md-2">
<div class="form-group">
<label for="select" class="control-label">Language</label>
<div id="profiles">
<select [(ngModel)]="selectedLanguage" class="form-control form-control-custom refine-option" id="select">
<option *ngFor="let lang of langauges" value="{{lang.code}}">{{lang.nativeName}}</option>
</select>
</div>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="actorSearch" name="actorSearch" [(ngModel)]="actorSearch">
<label for="actorSearch" tooltipPosition="top" pTooltip="Search for movies by actor">Actor Search</label>
</div>
</div>
</div>
<div class="col-md-7">
<button class="btn pull-right btn-success-outline" (click)="applyRefinedSearch()">Apply</button>
</div>
</div>
<remaining-requests [movie]="true" [quotaRefreshEvents]="movieRequested.asObservable()" #remainingFilms></remaining-requests>
<!-- Movie content -->
<div id="movieList">
<div *ngIf="searchApplied && movieResults?.length <= 0" class='no-search-results'>
<i class='fas fa-film no-search-results-icon'></i>
<div class='no-search-results-text' [translate]="'Search.NoResults'"></div>
</div>
<div *ngFor="let result of movieResults">
<div class="row">
<div class="myBg backdrop" [style.background-image]="result.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-2 small-padding">
<img *ngIf="result.posterPath" class="img-responsive poster movie-poster" src="{{result.posterPath}}"
alt="poster">
</div>
<div class="col-sm-8 small-padding">
<div>
<a href="https://www.themoviedb.org/movie/{{result.id}}/" target="_blank">
<h4>{{result.title}} ({{result.releaseDate | amLocal | amDateFormat: 'YYYY'}})</h4>
</a>
<span class="tags">
<span *ngIf="result.releaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{
'Search.TheatricalRelease' | translate: {date: result.releaseDate | amLocal |
amDateFormat: 'LL'} }}</span>
<span *ngIf="result.digitalReleaseDate" class="label label-info" id="releaseDateLabel"
target="_blank">{{ 'Search.DigitalDate' | translate: {date: result.digitalReleaseDate |
amLocal | amUserLocale | amDateFormat: 'LL'} }}</span>
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homePageLabel" target="_blank"><span
class="label label-info" [translate]="'Search.Movies.HomePage'"></span></a>
<a *ngIf="result.trailer" href="{{result.trailer}}" id="trailerLabel" target="_blank"><span
class="label label-info" [translate]="'Search.Movies.Trailer'"></span></a>
<span *ngIf="result.quality" id="qualityLabel" class="label label-success">{{result.quality}}p</span>
<ng-template [ngIf]="result.available"><span class="label label-success" id="availableLabel"
[translate]="'Common.Available'"></span></ng-template>
<ng-template [ngIf]="result.approved && !result.available"><span class="label label-info"
id="processingRequestLabel" [translate]="'Common.ProcessingRequest'"></span></ng-template>
<ng-template [ngIf]="result.requested && !result.approved && !result.available"><span class="label label-warning"
id="pendingApprovalLabel" [translate]="'Common.PendingApproval'"></span></ng-template>
<ng-template [ngIf]="!result.requested && !result.available && !result.approved"><span
class="label label-danger" id="notRequestedLabel" [translate]="'Common.NotRequested'"></span></ng-template>
</span>
<br />
</div>
<p style="font-size: 0.9rem !important">{{result.overview}}</p>
</div>
<div class="col-sm-2 small-padding">
<div *ngIf="result.available">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fas fa-check"></i>
{{ 'Common.Available' | translate }}</button>
</div>
<div *ngIf="!result.available">
<div *ngIf="result.requested || result.approved; then requestedBtn else notRequestedBtn"></div>
<ng-template #requestedBtn>
<button style="text-align: right" class="btn btn-primary-outline disabled" [disabled]><i
class="fas fa-check"></i> {{ 'Common.Requested' | translate }}</button>
</ng-template>
<ng-template #notRequestedBtn>
<button id="{{result.id}}" style="text-align: right" class="btn btn-primary-outline"
(click)="request(result)">
<i *ngIf="result.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i> <i
*ngIf="!result.requestProcessing && !result.processed" class="fas fa-plus"></i>
<i *ngIf="result.processed && !result.requestProcessing" class="fas fa-check"></i> {{
'Common.Request' | translate }}</button>
</ng-template>
</div>
<div *ngIf="result.requested">
<a *ngIf="result.showSubscribe && !result.subscribed" style="text-align: right" class="btn btn btn-success-outline"
(click)="subscribe(result)" pTooltip="Subscribe for notifications when this movie becomes available">
<i class="fas fa-rss"></i> Subscribe</a>
<a *ngIf="result.showSubscribe && result.subscribed" style="text-align: right;" class="btn btn btn-warning-outline"
(click)="unSubscribe(result)" pTooltip="Unsubscribe notifications when this movie becomes available">
<i class="fas fa-rss"></i> Unsubscribe</a>
</div>
<button style="text-align: right" class="btn btn-sm btn-info-outline" (click)="similarMovies(result.id)">
<i class="far fa-eye"></i> {{ 'Search.Similar' | translate }}</button>
<br />
<div *ngIf="result.available">
<a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}"
target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnPlex' | translate}}</a>
<a *ngIf="result.embyUrl" style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline"
href="{{result.embyUrl}}" target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnEmby' |
translate}}</a>
<a *ngIf="result.jellyfinUrl" style="text-align: right" id="jellyfinbtn" class="btn btn-sm btn-success-outline"
href="{{result.jellyfinUrl}}" target="_blank"><i class="far fa-eye"></i> {{'Search.ViewOnJellyfin' |
translate}}</a>
</div>
<div class="dropdown" *ngIf="result.available && issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<i class="fas fa-plus"></i> {{'Requests.ReportIssue' | translate}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, result)">{{cat.value}}</a></li>
</ul>
</div>
</div>
</div>
<br />
<br />
</div>
</div>
</div>
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequestTitle"
[issueCategory]="issueCategorySelected" [id]="issueRequestId" [providerId]="issueProviderId"></issue-report>

@ -1,275 +0,0 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { Component, Input, OnInit, Inject } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { TranslateService } from "@ngx-translate/core";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { AuthService } from "../auth/auth.service";
import { IIssueCategory, ILanguageRefine, IRequestEngineResult, ISearchMovieResult } from "../interfaces";
import { NotificationService, RequestService, SearchService, SettingsService } from "../services";
import * as languageData from "../../other/iso-lang.json";
@Component({
selector: "movie-search",
templateUrl: "./moviesearch.component.html",
styleUrls: ["./search.component.scss"],
})
export class MovieSearchComponent implements OnInit {
public searchText: string;
public searchChanged: Subject<string> = new Subject<string>();
public movieRequested: Subject<void> = new Subject<void>();
public movieResults: ISearchMovieResult[];
public result: IRequestEngineResult;
public searchApplied = false;
public refineSearchEnabled = false;
public searchYear?: number;
public actorSearch: boolean;
public selectedLanguage: string;
public langauges: ILanguageRefine[];
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issuesBarVisible = false;
public issueRequestTitle: string;
public issueRequestId: number;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
public defaultPoster: string;
private href: string;
constructor(
private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService,
private readonly translate: TranslateService, private sanitizer: DomSanitizer,
@Inject(APP_BASE_HREF) href:string, private settingsService: SettingsService) {
this.href= href;
this.langauges = <ILanguageRefine[]><any>languageData;
this.searchChanged.pipe(
debounceTime(600), // Wait Xms after the last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
this.runSearch();
});
this.defaultPoster = "../../../images/default_movie_poster.png";
const base = this.href;
if (base) {
this.defaultPoster = "../../.." + base + "/images/default_movie_poster.png";
}
}
public ngOnInit() {
this.searchText = "";
this.movieResults = [];
this.result = {
message: "",
result: false,
errorMessage: "",
};
this.settingsService.getDefaultLanguage().subscribe(x => this.selectedLanguage = x);
this.popularMovies();
}
public search(text: any) {
this.searchChanged.next(text.target.value);
}
public request(searchResult: ISearchMovieResult) {
searchResult.requested = true;
searchResult.requestProcessing = true;
searchResult.showSubscribe = false;
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMovie")) {
searchResult.approved = true;
}
try {
const language = this.selectedLanguage && this.selectedLanguage.length > 0 ? this.selectedLanguage : "en";
this.requestService.requestMovie({ theMovieDbId: searchResult.id, languageCode: language })
.subscribe(x => {
this.result = x;
if (this.result.result) {
this.movieRequested.next();
this.translate.get("Search.RequestAdded", { title: searchResult.title }).subscribe(x => {
this.notificationService.success(x);
searchResult.processed = true;
});
} else {
if (this.result.errorMessage && this.result.message) {
this.notificationService.warning("Request Added", `${this.result.message} - ${this.result.errorMessage}`);
} else {
this.notificationService.warning("Request Added", this.result.message ? this.result.message : this.result.errorMessage);
}
searchResult.requested = false;
searchResult.approved = false;
searchResult.processed = false;
searchResult.requestProcessing = false;
}
});
} catch (e) {
searchResult.processed = false;
searchResult.requestProcessing = false;
this.notificationService.error(e);
}
}
public popularMovies() {
this.clearResults();
this.searchService.popularMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
public nowPlayingMovies() {
this.clearResults();
this.searchService.nowPlayingMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
public topRatedMovies() {
this.clearResults();
this.searchService.topRatedMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
public upcomingMovies() {
this.clearResults();
this.searchService.upcomingMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
public reportIssue(catId: IIssueCategory, req: ISearchMovieResult) {
this.issueRequestId = req.id;
const releaseDate = new Date(req.releaseDate);
this.issueRequestTitle = req.title + ` (${releaseDate.getFullYear()})`;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
}
public similarMovies(theMovieDbId: number) {
this.clearResults();
const lang = this.selectedLanguage && this.selectedLanguage.length > 0 ? this.selectedLanguage : "";
this.searchService.similarMovies(theMovieDbId, lang)
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
public subscribe(r: ISearchMovieResult) {
r.subscribed = true;
this.requestService.subscribeToMovie(r.requestId)
.subscribe(x => {
this.notificationService.success(`Subscribed To Movie ${r.title}!`);
});
}
public unSubscribe(r: ISearchMovieResult) {
r.subscribed = false;
this.requestService.unSubscribeToMovie(r.requestId)
.subscribe(x => {
this.notificationService.success("Unsubscribed Movie!");
});
}
public refineOpen() {
this.refineSearchEnabled = !this.refineSearchEnabled;
if (!this.refineSearchEnabled) {
this.searchYear = undefined;
}
}
public applyRefinedSearch() {
this.runSearch();
}
private getExtraInfo() {
this.movieResults.forEach((val, index) => {
if (val.posterPath === null) {
val.posterPath = this.defaultPoster;
} else {
val.posterPath = "https://image.tmdb.org/t/p/w300/" + val.posterPath;
}
val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + "https://image.tmdb.org/t/p/w1280" + val.backdropPath + ")");
if (this.applyRefinedSearch) {
this.searchService.getMovieInformationWithRefined(val.id, this.selectedLanguage)
.subscribe(m => {
this.updateItem(val, m);
});
} else {
this.searchService.getMovieInformation(val.id)
.subscribe(m => {
this.updateItem(val, m);
});
}
});
}
private updateItem(key: ISearchMovieResult, updated: ISearchMovieResult) {
const index = this.movieResults.indexOf(key, 0);
if (index > -1) {
const copy = { ...this.movieResults[index] };
this.movieResults[index] = updated;
this.movieResults[index].background = copy.background;
this.movieResults[index].posterPath = copy.posterPath;
}
}
private clearResults() {
this.movieResults = [];
this.searchApplied = false;
}
private runSearch() {
if (this.searchText === "") {
this.clearResults();
return;
}
if (this.refineOpen) {
if (!this.actorSearch) {
this.searchService.searchMovieWithRefined(this.searchText, this.searchYear, this.selectedLanguage)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
} else {
this.searchService.searchMovieByActor(this.searchText, this.selectedLanguage)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
}
} else {
this.searchService.searchMovie(this.searchText)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
}
}
}

@ -1,149 +0,0 @@
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane active" id="MoviesTab">
<div class="input-group">
<input id="search" type="text" class="form-control form-control-custom form-control-search form-control-withbuttons" (keyup)="search($event)">
<div class="input-group-addon right-radius">
<div class="btn-group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Suggestions
<i class="fas fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li>
<a (click)="popularMovies()">Popular Movies</a>
</li>
<li>
<a (click)="upcomingMovies()">Upcoming Movies</a>
</li>
<li>
<a (click)="topRatedMovies()">Top Rated Movies</a>
</li>
<li>
<a (click)="nowPlayingMovies()">Now Playing Movies</a>
</li>
</ul>
</div>
<i id="movieSearchButton" class="fas fa-search"></i>
</div>
</div>
<br />
<br />
<!-- Movie content -->
<div id="movieList">
<div *ngIf="searchApplied && movieResults?.length <= 0" class='no-search-results'>
<i class='fas fa-film no-search-results-icon'></i>
<div class='no-search-results-text'>Sorry, we didn't find any results!</div>
</div>
<!--NEW-->
<div *ngFor="let grid of movieResultGrid; let i = index">
<div class="row">
<div *ngFor="let r of grid.movies">
<div class="col-md-3">
<img *ngIf="r.posterPath" class="img-responsive poster" src="https://image.tmdb.org/t/p/w150/{{r.posterPath}}"
alt="poster">
</div>
</div>
</div>
</div>
<!--END NEW-->
<br/>
<br/>
<br/>
<hr/>
<div *ngFor="let result of movieResults">
<div class="row">
<div id="imgDiv" class="col-sm-2">
<img *ngIf="result.posterPath" class="img-responsive poster" src="https://image.tmdb.org/t/p/w150/{{result.posterPath}}"
alt="poster">
</div>
<div class="col-sm-8">
<div>
<a href="https://www.themoviedb.org/movie/{{result.id}}/" target="_blank">
<h4>{{result.title}} ({{result.releaseDate | amLocal | amDateFormat: 'YYYY'}})</h4>
</a>
<span *ngIf="result.releaseDate" class="label label-info" target="_blank">Release Date: {{result.releaseDate | amLocal | amUserLocale | amDateFormat: 'L'}}</span>
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homepageLabel" target="_blank">
<span class="label label-info">HomePage</span>
</a>
<a *ngIf="result.trailer" href="{{result.trailer}}" id="trailerLabel" target="_blank">
<span class="label label-info">Trailer</span>
</a>
<span *ngIf="result.quality" class="label label-success" id="qualityLabel">{{result.quality}}p</span>
<ng-template [ngIf]="result.available">
<span class="label label-success" id="availableLabel">Available</span>
</ng-template>
<ng-template [ngIf]="result.approved && !result.available">
<span class="label label-info" id="processingRequestLabel">Processing Request</span>
</ng-template>
<ng-template [ngIf]="result.requested && !result.approved && !result.available">
<span class="label label-warning" id="pendingApprovalLabel">Pending Approval</span>
</ng-template>
<ng-template [ngIf]="!result.requested && !result.available && !result.approved">
<span class="label label-danger" id="notRequetsedLabel">Not Requested</span>
</ng-template>
<br/>
<br/>
</div>
<p style="font-size: 0.9rem !important">{{result.overview}}</p>
</div>
<div class="col-sm-2">
<div *ngIf="result.available">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fas fa-check"></i> Available</button>
<div *ngIf="result.plexUrl">
<br/>
<br/>
<a style="text-align: right" class="btn btn-sm btn-primary-outline" href="{{result.plexUrl}}" target="_blank">
<i class="far fa-eye"></i> View In Plex</a>
</div>
</div>
<div *ngIf="!result.available">
<div *ngIf="result.requested || result.approved; then requestedBtn else notRequestedBtn"></div>
<ng-template #requestedBtn>
<button style="text-align: right" class="btn btn-primary-outline disabled" [disabled]>
<i class="fas fa-check"></i> Requested</button>
</ng-template>
<ng-template #notRequestedBtn>
<button id="{{result.id}}" style="text-align: right" class="btn btn-primary-outline" (click)="request(result)">
<i *ngIf="result.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i>
<i *ngIf="!result.requestProcessing && !result.processed" class="fas fa-plus"></i>
<i *ngIf="result.processed && !result.requestProcessing" class="fas fa-check"></i>Request</button>
</ng-template>
</div>
<br/>
<div *ngIf="result.available">
<a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}" target="_blank">
<i class="far fa-eye"></i> View On Plex</a>
</div>
</div>
</div>
<hr/>
</div>
</div>
</div>

@ -1,164 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { AuthService } from "../auth/auth.service";
import { IRequestEngineResult, ISearchMovieResult, ISearchMovieResultContainer } from "../interfaces";
import { NotificationService, RequestService, SearchService } from "../services";
@Component({
selector: "movie-search-grid",
templateUrl: "./moviesearchgrid.component.html",
})
export class MovieSearchGridComponent implements OnInit {
public searchText: string;
public searchChanged: Subject<string> = new Subject<string>();
public movieResults: ISearchMovieResult[];
public movieResultGrid: ISearchMovieResultContainer[] = [];
public result: IRequestEngineResult;
public searchApplied = false;
constructor(
private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService) {
this.searchChanged.pipe(
debounceTime(600), // Wait Xms afterthe last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
this.clearResults();
return;
}
this.searchService.searchMovie(this.searchText)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some exta info including IMDBId
// This way the search is fast at displaying results.
this.getExtaInfo();
});
});
}
public ngOnInit() {
this.searchText = "";
this.movieResults = [];
this.result = {
message: "",
result: false,
errorMessage: "",
};
}
public search(text: any) {
this.searchChanged.next(text.target.value);
}
public request(searchResult: ISearchMovieResult) {
searchResult.requested = true;
searchResult.requestProcessing = true;
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMovie")) {
searchResult.approved = true;
}
try {
this.requestService.requestMovie({ theMovieDbId: searchResult.id, languageCode: "en" })
.subscribe(x => {
this.result = x;
if (this.result.result) {
this.notificationService.success(
`Request for ${searchResult.title} has been added successfully`);
searchResult.processed = true;
} else {
if (this.result.errorMessage && this.result.message) {
this.notificationService.warning("Request Added", `${this.result.message} - ${this.result.errorMessage}`);
} else {
this.notificationService.warning("Request Added", this.result.message ? this.result.message : this.result.errorMessage);
}
searchResult.requested = false;
searchResult.approved = false;
searchResult.processed = false;
searchResult.requestProcessing = false;
}
});
} catch (e) {
searchResult.processed = false;
searchResult.requestProcessing = false;
this.notificationService.error(e);
}
}
public popularMovies() {
this.clearResults();
this.searchService.popularMovies()
.subscribe(x => {
this.movieResults = x;
this.processGrid(x);
this.getExtaInfo();
});
}
public nowPlayingMovies() {
this.clearResults();
this.searchService.nowPlayingMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtaInfo();
});
}
public topRatedMovies() {
this.clearResults();
this.searchService.topRatedMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtaInfo();
});
}
public upcomingMovies() {
this.clearResults();
this.searchService.upcomingMovies()
.subscribe(x => {
this.movieResults = x;
this.getExtaInfo();
});
}
private getExtaInfo() {
this.movieResults.forEach((val) => {
this.searchService.getMovieInformation(val.id)
.subscribe(m => this.updateItem(val, m));
});
}
private updateItem(key: ISearchMovieResult, updated: ISearchMovieResult) {
const index = this.movieResults.indexOf(key, 0);
if (index > -1) {
this.movieResults[index] = updated;
}
}
private clearResults() {
this.movieResults = [];
this.searchApplied = false;
}
private processGrid(movies: ISearchMovieResult[]) {
let container = <ISearchMovieResultContainer> { movies: [] };
movies.forEach((movie, i) => {
i++;
if ((i % 4) === 0) {
container.movies.push(movie);
this.movieResultGrid.push(container);
container = <ISearchMovieResultContainer> { movies: [] };
} else {
container.movies.push(movie);
}
});
this.movieResultGrid.push(container);
}
}

@ -1,116 +0,0 @@
<div class="row">
<!--Backdrop-->
<div class="album-bg backdrop" [style.background-image]="result.background"></div>
<div class="album-tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<!--Album Art-->
<div class="col-sm-12 small-padding">
<img *ngIf="result.disk" class="img-responsive poster album-cover" src="{{result.disk}}" alt="poster">
</div>
<!--Artist Title-->
<div class="col-sm-12 small-padding">
<div>
<h4>
<a href="" target="_blank">
{{result.title | truncate: 36}}
</a>
</h4>
<h5>
<a href="" (click)="selectArtist($event, result.foreignArtistId)">
{{result.artistName}}
</a>
</h5>
</div>
<!--Tags-->
<div>
<span class="tags">
<!-- <span *ngIf="result.releaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.TheatricalRelease' | translate: {date: result.releaseDate | date: 'mediumDate'} }}</span>
<span *ngIf="result.digitalReleaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.DigitalDate' | translate: {date: result.digitalReleaseDate | date: 'mediumDate'} }}</span>
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homePageLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.HomePage'"></span></a>
<a *ngIf="result.trailer" href="{{result.trailer}}" id="trailerLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.Trailer'"></span></a> -->
<ng-template [ngIf]="!result.requested && !result.fullyAvailable && !result.approved">
<span class="label label-danger" id="notRequestedLabel" [translate]="'Common.NotRequested'"></span>
</ng-template>
<ng-template [ngIf]="result.fullyAvailable">
<span class="label label-success" id="availableLabel" [translate]="'Common.Available'"></span>
</ng-template>
<ng-template [ngIf]="result.partiallyAvailable">
<span class="label label-info" id="availableLabel" [translate]="'Common.PartiallyAvailable'"></span>
</ng-template>
<ng-template [ngIf]="result.monitored && !result.fullyAvailable">
<span class="label label-info" id="processingRequestLabel" [translate]="'Common.Monitored'"></span>
</ng-template>
<ng-template [ngIf]="result.requested && !result.approved && !result.partiallyAvailable">
<span class="label label-warning" id="pendingApprovalLabel" [translate]="'Common.PendingApproval'"></span>
</ng-template>
<ng-template [ngIf]="result.approved && !result.fullyAvailable"><span class="label label-info" id="processingRequestLabel" [translate]="'Common.ProcessingRequest'"></span></ng-template>
<ng-template [ngIf]="result.releaseDate">
<span class="label label-info" id="availableLabel">Release Date: {{result.releaseDate | amLocal | amUserLocale | amDateFormat: 'L'}}</span>
</ng-template>
<ng-template [ngIf]="result.rating">
<span class="label label-info" id="availableLabel">{{result.rating}}/10</span>
</ng-template>
</span>
</div>
</div>
<!--Buttons-->
<div class="col-sm-12 small-padding">
<!-- <div class="row" *ngIf="result.requested">
<div class="col-md-2 col-md-push-10">
<a *ngIf="result.showSubscribe && !result.subscribed" style="color:white" (click)="subscribe(result)" pTooltip="Subscribe for notifications"> <i class="fas fa-rss"></i></a>
<a *ngIf="result.showSubscribe && result.subscribed" style="color:red" (click)="unSubscribe(result)" pTooltip="Unsubscribe notification"> <i class="fas fa-rss"></i></a>
</div>
</div> -->
<div *ngIf="result.fullyAvailable">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fas fa-check"></i> {{ 'Common.Available' | translate }}</button>
</div>
<div *ngIf="!result.fullyAvailable">
<div *ngIf="result.requested || result.approved || result.monitored; then requestedBtn else notRequestedBtn"></div>
<ng-template #requestedBtn>
<button style="text-align: right" class="btn btn-primary-outline disabled" [disabled]>
<i class="fas fa-check"></i> {{ 'Common.Requested' | translate }}</button>
</ng-template>
<ng-template #notRequestedBtn>
<button style="text-align: right" class="btn btn-primary-outline" (click)="request(result)">
<i *ngIf="result.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i>
<i *ngIf="!result.requestProcessing && !result.processed" class="fas fa-plus"></i>
<i *ngIf="result.processed && !result.requestProcessing" class="fas fa-check"></i> {{ 'Common.Request'
| translate }}</button>
</ng-template>
</div>
<div class="dropdown" *ngIf="(result.partiallyAvailable || result.fullyAvailable) && issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<i class="fas fa-plus"></i> Report Issue
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, result)">{{cat.value}}</a></li>
</ul>
</div>
</div>
</div>
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequestTitle"
[issueCategory]="issueCategorySelected" [id]="issueRequestId" [providerId]="issueProviderId"></issue-report>

@ -1,91 +0,0 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import { Subject } from "rxjs";
import { AuthService } from "../../auth/auth.service";
import { IIssueCategory, IRequestEngineResult } from "../../interfaces";
import { ISearchAlbumResult } from "../../interfaces/ISearchMusicResult";
import { NotificationService, RequestService } from "../../services";
@Component({
selector: "album-search",
templateUrl: "./albumsearch.component.html",
})
export class AlbumSearchComponent {
@Input() public result: ISearchAlbumResult;
public engineResult: IRequestEngineResult;
@Input() public defaultPoster: string;
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
@Input() public musicRequested: Subject<void>;
public issuesBarVisible = false;
public issueRequestTitle: string;
public issueRequestId: number;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
@Output() public setSearch = new EventEmitter<string>();
constructor(
private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService,
private readonly translate: TranslateService) {
}
public selectArtist(event: Event, artistId: string) {
event.preventDefault();
this.setSearch.emit(artistId);
}
public reportIssue(catId: IIssueCategory, req: ISearchAlbumResult) {
this.issueRequestId = req.id;
this.issueRequestTitle = req.title + `(${req.releaseDate.getFullYear})`;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
}
public request(searchResult: ISearchAlbumResult) {
searchResult.requested = true;
searchResult.requestProcessing = true;
searchResult.showSubscribe = false;
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMusic")) {
searchResult.approved = true;
}
try {
this.requestService.requestAlbum({ foreignAlbumId: searchResult.foreignAlbumId })
.subscribe(x => {
this.engineResult = x;
if (this.engineResult.result) {
this.musicRequested.next();
this.translate.get("Search.RequestAdded", { title: searchResult.title }).subscribe(x => {
this.notificationService.success(x);
searchResult.processed = true;
});
} else {
if (this.engineResult.errorMessage && this.engineResult.message) {
this.notificationService.warning("Request Added", `${this.engineResult.message} - ${this.engineResult.errorMessage}`);
} else {
this.notificationService.warning("Request Added", this.engineResult.message ? this.engineResult.message : this.engineResult.errorMessage);
}
searchResult.requested = false;
searchResult.approved = false;
searchResult.processed = false;
searchResult.requestProcessing = false;
}
});
} catch (e) {
searchResult.processed = false;
searchResult.requestProcessing = false;
this.notificationService.error(e);
}
}
}

@ -1,65 +0,0 @@
<div class="row">
<div class="myBg backdrop" [style.background-image]="result.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-3 small-padding">
<img *ngIf="result.poster" class="img-responsive poster artist-cover" src="{{result.poster}}" alt="poster">
</div>
<div class="col-sm-7 small-padding">
<div>
<a href="" target="_blank">
<h4>{{result.artistName}}</h4>
</a>
<span class="tags">
<!-- <span *ngIf="result.releaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.TheatricalRelease' | translate: {date: result.releaseDate | date: 'mediumDate'} }}</span>
<span *ngIf="result.digitalReleaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.DigitalDate' | translate: {date: result.digitalReleaseDate | date: 'mediumDate'} }}</span>
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homePageLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.HomePage'"></span></a>
<a *ngIf="result.trailer" href="{{result.trailer}}" id="trailerLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.Trailer'"></span></a> -->
<ng-template [ngIf]="result.artistType">
<span class="label label-info" id="artistType">{{result.artistType}}</span>
</ng-template>
<ng-template [ngIf]="result.disambiguation">
<span class="label label-info" id="disambiguation">{{result.disambiguation}}</span>
</ng-template>
<ng-template [ngIf]="result.monitored">
<span class="label label-info" id="disambiguation">Monitored</span>
</ng-template>
</span>
<br/>
</div>
<p style="font-size: 0.9rem !important">{{result.overview | truncate: 350 }}</p>
<div class="row">
<a id="infoTags" *ngFor="let link of result.links" href="{{link.url}}" target="_blank" class="label label-primary">
{{link.name}}
</a>
</div>
</div>
<div class="col-sm-2 small-padding">
<div class="row" *ngIf="result.requested">
<div class="col-md-2 col-md-push-10">
<!-- <a *ngIf="result.showSubscribe && !result.subscribed" style="color:white" (click)="subscribe(result)" pTooltip="Subscribe for notifications"> <i class="fas fa-rss"></i></a>
<a *ngIf="result.showSubscribe && result.subscribed" style="color:red" (click)="unSubscribe(result)" pTooltip="Unsubscribe notification"> <i class="fas fa-rss"></i></a> -->
</div>
</div>
<button style="text-align: right" class="btn btn-info-outline" [disabled]="searchingAlbums" (click)="viewAllAlbums()">
<i class="far fa-eye"></i> View Albums</button>
</div>
<!-- <button style="text-align: right" class="btn btn-sm btn-info-outline" (click)="similarMovies(result.id)"> <i class="far fa-eye"></i> {{ 'Search.Similar' | translate }}</button> -->
<br/>
</div>

@ -1,27 +0,0 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ISearchAlbumResult, ISearchArtistResult } from "../../interfaces/ISearchMusicResult";
import { SearchService } from "../../services";
@Component({
selector: "artist-search",
templateUrl: "./artistsearch.component.html",
})
export class ArtistSearchComponent {
@Input() public result: ISearchArtistResult;
@Input() public defaultPoster: string;
public searchingAlbums: boolean;
@Output() public viewAlbumsResult = new EventEmitter<ISearchAlbumResult[]>();
constructor(private searchService: SearchService) {
}
public viewAllAlbums() {
this.searchingAlbums = true;
this.searchService.getAlbumsForArtist(this.result.forignArtistId).subscribe(x => {
this.viewAlbumsResult.emit(x);
});
}
}

@ -1,52 +0,0 @@
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane active" id="MoviesTab">
<div class="input-group">
<input id="search" type="text" placeholder="{{ 'Search.SearchBarPlaceholder' | translate }}" class="form-control form-control-custom form-control-search form-control-withbuttons" (keyup)="search($event)">
<div class="input-group-addon right-radius">
<i class="fas fa-search"></i>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="Album" name="Mode" [checked]="searchAlbum" (click)="searchMode(true)">
<label for="Album">Album Search</label>
<input type="radio" id="Artist" name="Mode" [checked]="!searchAlbum" (click)="searchMode(false)">
<label for="Artist">Artist Search</label>
</div>
</div>
<br />
<br />
<div id="movieList">
<div *ngIf="searchApplied && artistResult?.length <= 0 && !searchAlbum" class='no-search-results'>
<i class='fas fa-music no-search-results-icon'></i>
<div class='no-search-results-text' [translate]="'Search.NoResults'"></div>
</div>
<div *ngIf="searchApplied && albumResult?.length <= 0 && searchAlbum" class='no-search-results'>
<i class='fas fa-music no-search-results-icon'></i>
<div class='no-search-results-text' [translate]="'Search.NoResults'"></div>
</div>
<remaining-requests [music]="true" [quotaRefreshEvents]="musicRequested.asObservable()" #remainingAlbums></remaining-requests>
<div *ngFor="let result of artistResult">
<artist-search [result]="result" [defaultPoster]="defaultPoster" (viewAlbumsResult)="viewAlbumsForArtist($event)"></artist-search>
<br/>
<br/>
</div>
<div class="col-md-12">
<div *ngFor="let result of albumResult" class="col-md-4">
<album-search [musicRequested]="musicRequested" [result]="result" [defaultPoster]="defaultPoster" (setSearch)="setArtistSearch($event)"></album-search>
<br/>
<br/>
</div>
</div>
</div>
</div>
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequestTitle"
[issueCategory]="issueCategorySelected" [id]="issueRequestId" [providerId]="issueProviderId"></issue-report>

@ -1,149 +0,0 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { Component, Input, OnInit, Inject } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { IIssueCategory, IRequestEngineResult } from "../../interfaces";
import { ISearchAlbumResult, ISearchArtistResult } from "../../interfaces/ISearchMusicResult";
import { SearchService } from "../../services";
@Component({
selector: "music-search",
templateUrl: "./musicsearch.component.html",
})
export class MusicSearchComponent implements OnInit {
public searchText: string;
public searchChanged: Subject<string> = new Subject<string>();
public artistResult: ISearchArtistResult[];
public albumResult: ISearchAlbumResult[];
public result: IRequestEngineResult;
public searchApplied = false;
public searchAlbum: boolean = true;
public musicRequested: Subject<void> = new Subject<void>();
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issuesBarVisible = false;
public issueRequestTitle: string;
public issueRequestId: number;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
public defaultPoster: string;
private href: string;
constructor(
private searchService: SearchService, private sanitizer: DomSanitizer,
@Inject(APP_BASE_HREF) href:string) {
this.href = href;
this.searchChanged.pipe(
debounceTime(600), // Wait Xms after the last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
if(this.searchAlbum) {
this.clearAlbumResults();
} else {
this.clearArtistResults();
}
return;
}
if(this.searchAlbum) {
if(!this.searchText) {
this.searchText = "iowa"; // REMOVE
}
this.searchService.searchAlbum(this.searchText)
.subscribe(x => {
this.albumResult = x;
this.searchApplied = true;
this.setAlbumBackground();
});
} else {
this.searchService.searchArtist(this.searchText)
.subscribe(x => {
this.artistResult = x;
this.searchApplied = true;
this.setArtistBackground();
});
}
});
this.defaultPoster = "../../../images/default-music-placeholder.png";
const base = this.href;
if (base) {
this.defaultPoster = "../../.." + base + "/images/default-music-placeholder.png";
}
}
public ngOnInit() {
this.searchText = "";
this.artistResult = [];
this.result = {
message: "",
result: false,
errorMessage: "",
};
}
public search(text: any) {
this.searchChanged.next(text.target.value);
}
public searchMode(val: boolean) {
this.searchAlbum = val;
if(val) {
// Album
this.clearArtistResults();
} else {
this.clearAlbumResults();
}
}
public setArtistSearch(artistId: string) {
this.searchAlbum = false;
this.clearAlbumResults();
this.searchChanged.next(`lidarr:${artistId}`);
}
public viewAlbumsForArtist(albums: ISearchAlbumResult[]) {
this.clearArtistResults();
this.searchAlbum = true;
this.albumResult = albums;
this.setAlbumBackground();
}
private clearArtistResults() {
this.artistResult = [];
this.searchApplied = false;
}
private clearAlbumResults() {
this.albumResult = [];
this.searchApplied = false;
}
private setArtistBackground() {
this.artistResult.forEach((val, index) => {
if (val.poster === null) {
val.poster = this.defaultPoster;
}
val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + val.banner + ")");
});
}
private setAlbumBackground() {
this.albumResult.forEach((val, index) => {
if (val.disk === null) {
if(val.cover === null) {
val.disk = this.defaultPoster;
} else {
val.disk = val.cover;
}
}
val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + val.cover + ")");
});
}
}

@ -1,35 +0,0 @@
<h1 [translate]="'Search.Title'"></h1>
<h4 [translate]="'Search.Paragraph'"></h4>
<br />
<!-- Nav tabs -->
<ul id="nav-tabs" class="nav nav-tabs nav-justified" role="tablist">
<li role="presentation" class="active">
<a id="movieTabButton" href="#MoviesTab" aria-controls="home" role="tab" data-toggle="tab" (click)="selectMovieTab()"><i class="fas fa-film"></i> {{ 'Search.MoviesTab' | translate }}</a>
</li>
<li role="presentation">
<a id="tvTabButton" href="#TvShowTab" aria-controls="profile" role="tab" data-toggle="tab" (click)="selectTvTab()"><i class="fas fa-tv"></i> {{ 'Search.TvTab' | translate }}</a>
</li>
<li role="presentation" *ngIf="musicEnabled">
<a id="tvTabButton" href="#MusicTab" aria-controls="profile" role="tab" data-toggle="tab" (click)="selectMusicTab()"><i class="fas fa-music"></i> {{ 'Search.MusicTab' | translate }}</a>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div [hidden]="!showMovie">
<movie-search [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"></movie-search>
</div>
<div [hidden]="!showTv">
<tv-search [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"></tv-search>
</div>
<div [hidden]="!showMusic">
<music-search></music-search>
</div>
</div>

@ -1,34 +0,0 @@
@media (max-width: 978px) {
.top-spacing {
padding-top: 5%
}
.form-control-search {
padding-right: 165px;
}
}
@media (min-width: 979px) {
.top-spacing {
padding-top: 2%
}
.form-control-search {
width: 90%;
}
}
.tab-content {
margin-top: 1.5em;
}
.search-bar-background {
background-color: #333333;
}
.vcenter {
display: flex;
align-items: center;
}
.refine-option {
box-shadow: inset 0 1px 5px rgba(0,0,0,1.0);
}

@ -1,47 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { IIssueCategory } from "../interfaces";
import { IssuesService, SettingsService } from "../services";
@Component({
templateUrl: "./search.component.html",
})
export class SearchComponent implements OnInit {
public showTv: boolean;
public showMovie: boolean;
public showMusic: boolean;
public issueCategories: IIssueCategory[];
public issuesEnabled = false;
public musicEnabled: boolean;
constructor(private issuesService: IssuesService,
private settingsService: SettingsService) {
}
public ngOnInit() {
this.settingsService.lidarrEnabled().subscribe(x => this.musicEnabled = x);
this.showMovie = true;
this.showTv = false;
this.showMusic = false;
this.issuesService.getCategories().subscribe(x => this.issueCategories = x);
this.settingsService.getIssueSettings().subscribe(x => this.issuesEnabled = x.enabled);
}
public selectMovieTab() {
this.showMovie = true;
this.showTv = false;
this.showMusic = false;
}
public selectTvTab() {
this.showMovie = false;
this.showTv = true;
this.showMusic = false;
}
public selectMusicTab() {
this.showMovie = false;
this.showTv = false;
this.showMusic = true;
}
}

@ -1,60 +0,0 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { RouterModule, Routes } from "@angular/router";
import { MovieSearchComponent } from "./moviesearch.component";
import { MovieSearchGridComponent } from "./moviesearchgrid.component";
import { AlbumSearchComponent } from "./music/albumsearch.component";
import { ArtistSearchComponent } from "./music/artistsearch.component";
import { MusicSearchComponent } from "./music/musicsearch.component";
import { SearchComponent } from "./search.component";
import { SeriesInformationComponent } from "./seriesinformation.component";
import { TvSearchComponent } from "./tvsearch.component";
import { CardsFreeModule } from "angular-bootstrap-md";
import { RequestService } from "../services";
import { SearchService } from "../services";
import { AuthGuard } from "../auth/auth.guard";
import { RemainingRequestsComponent } from "../requests/remainingrequests.component";
import { SharedModule } from "../shared/shared.module";
const routes: Routes = [
{ path: "", component: SearchComponent, canActivate: [AuthGuard] },
{ path: "show/:id", component: SeriesInformationComponent, canActivate: [AuthGuard] },
];
@NgModule({
imports: [
CommonModule,
FormsModule,
RouterModule.forChild(routes),
TreeTableModule,
SharedModule,
SidebarModule,
TooltipModule,
CardsFreeModule,
],
declarations: [
SearchComponent,
MovieSearchComponent,
TvSearchComponent,
SeriesInformationComponent,
MovieSearchGridComponent,
RemainingRequestsComponent,
MusicSearchComponent,
ArtistSearchComponent,
AlbumSearchComponent,
],
exports: [
RouterModule,
],
providers: [
SearchService,
RequestService,
],
})
export class SearchModule { }

@ -1,71 +0,0 @@
<div *ngIf="series" class="content-space">
<button class="btn btn-sm btn-success pull-right" (click)="submitRequests()" title="Go to top">{{ 'Search.TvShows.SubmitRequest' | translate }}</button>
<ngb-tabset>
<div *ngFor="let season of series.seasonRequests">
<ngb-tab [id]="season.seasonNumber" [title]="season.seasonNumber">
<ng-template ngbTabContent>
<h2 [translate]="'Requests.SeasonNumberHeading'" [translateParams]="{seasonNumber: season.seasonNumber}">Season: {{season.seasonNumber}}</h2>
<button (click)="addAllEpisodes(season)" class="btn btn-sm btn-primary-outline" [translate]="'Search.TvShows.SelectAllInSeason'" [translateParams]="{seasonNumber: season.seasonNumber}">Select All in Season {{season.seasonNumber}}</button>
<table class="table table-striped table-hover table-responsive table-condensed">
<thead>
<tr>
<th>
<a>
#
</a>
</th>
<th>
<a>
Title
</a>
</th>
<th>
<a>
Air Date
</a>
</th>
<th>
<a>
Status
</a>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let ep of season.episodes">
<td>
{{ep.episodeNumber}}
</td>
<td>
{{ep.title}}
</td>
<td *ngIf="ep.airDateDisplay != 'Unknown'">
{{ep.airDate | amLocal | amUserLocale | amDateFormat: 'L' }}
</td>
<td *ngIf="ep.airDateDisplay == 'Unknown'">
{{ep.airDateDisplay }}
</td>
<td>
<ng-template [ngIf]="ep.available"><span class="label label-success" id="availableLabel">Available</span></ng-template>
<ng-template [ngIf]="ep.approved && !ep.available "><span class="label label-info" id="processingRequestLabel">Processing Request</span></ng-template>
<ng-template [ngIf]="ep.selected"><span class="label label-info" id="selectedLabel">Selected</span></ng-template>
<ng-template [ngIf]="ep.requested && !ep.approved && !ep.available && !ep.selected"><span class="label label-warning" id="pendingApprovalLabel">Pending Approval</span></ng-template>
<ng-template [ngIf]="!ep.requested && !ep.available && !ep.approved"><span class="label label-danger" id="notRequetsedLabel">Not Requested</span></ng-template>
</td>
<td>
<button *ngIf="!ep.selected" (click)="addRequest(ep)" [disabled]="ep.available || ep.requested || ep.approved" class="btn btn-sm btn-primary-outline">Select</button>
<button *ngIf="ep.selected" (click)="removeRequest(ep)" class="btn btn-sm btn-primary-outline">Unselect</button>
</td>
</tr>
</tbody>
</table>
</ng-template>
</ngb-tab>
</div>
</ngb-tabset>
</div>

@ -1,25 +0,0 @@
#requestFloatingBtn {
position: fixed; /* Fixed/sticky position */
bottom: 20px; /* Place the button at the bottom of the page */
right: 30px; /* Place the button 30px from the right */
z-index: 99; /* Make sure it does not overlap */
cursor: pointer; /* Add a mouse pointer on hover */
padding: 15px; /* Some padding */
border-radius: 10px; /* Rounded corners */
}
#requestFloatingBtn:hover {
background-color: #555; /* Add a dark-grey background on hover */
}
#bannerimage {
width: 758px;
height: 140px;
background-color: black;
background-position: center;
padding-bottom:30px;
}
.content-space {
padding-top: 10px;
}

@ -1,98 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { NotificationService } from "../services";
import { RequestService } from "../services";
import { SearchService } from "../services";
import { INewSeasonRequests, IRequestEngineResult, ISeasonsViewModel, ITvRequestViewModel } from "../interfaces";
import { IEpisodesRequests } from "../interfaces";
import { ISearchTvResult } from "../interfaces";
import { Subject } from "rxjs";
@Component({
selector: "seriesinformation",
templateUrl: "./seriesinformation.component.html",
styleUrls: ["./seriesinformation.component.scss"],
})
export class SeriesInformationComponent implements OnInit {
public result: IRequestEngineResult;
public series: ISearchTvResult;
public requestedEpisodes: IEpisodesRequests[] = [];
@Input() public tvRequested: Subject<void>;
@Input() private seriesId: number;
constructor(private searchService: SearchService, private requestService: RequestService, private notificationService: NotificationService) { }
public ngOnInit() {
this.searchService.getShowInformation(this.seriesId)
.subscribe(x => {
this.series = x;
});
}
public submitRequests() {
// Make sure something has been selected
const selected = this.series.seasonRequests.some((season) => {
return season.episodes.some((ep) => {
return ep.selected;
});
});
if (!selected) {
this.notificationService.error("You need to select some episodes!");
return;
}
this.series.requested = true;
const viewModel = <ITvRequestViewModel> { firstSeason: this.series.firstSeason, latestSeason: this.series.latestSeason, requestAll: this.series.requestAll, tvDbId: this.series.id};
viewModel.seasons = [];
this.series.seasonRequests.forEach((season) => {
const seasonsViewModel = <ISeasonsViewModel> {seasonNumber: season.seasonNumber, episodes: []};
season.episodes.forEach(ep => {
if (!this.series.latestSeason || !this.series.requestAll || !this.series.firstSeason) {
if (ep.selected) {
seasonsViewModel.episodes.push({episodeNumber: ep.episodeNumber});
}
}
});
viewModel.seasons.push(seasonsViewModel);
});
this.requestService.requestTv(viewModel)
.subscribe(x => {
this.tvRequested.next();
this.result = x as IRequestEngineResult;
if (this.result.result) {
this.notificationService.success(
`Request for ${this.series.title} has been added successfully`);
this.series.seasonRequests.forEach((season) => {
season.episodes.forEach((ep) => {
ep.selected = false;
});
});
} else {
this.notificationService.warning("Request Added", this.result.errorMessage ? this.result.errorMessage : this.result.message);
}
});
}
public addRequest(episode: IEpisodesRequests) {
episode.requested = true;
episode.selected = true;
}
public removeRequest(episode: IEpisodesRequests) {
episode.requested = false;
episode.selected = false;
}
public addAllEpisodes(season: INewSeasonRequests) {
season.episodes.forEach((ep) => this.addRequest(ep));
}
}

@ -1,185 +0,0 @@
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane" id="TvShowTab">
<ng-template #FilterRef>
<div class="btn-group" role="group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
{{ 'Search.Suggestions' | translate }}
<i class="fas fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li>
<a (click)="popularShows()">{{ 'Search.TvShows.Popular' | translate }} </a>
</li>
<li>
<a (click)="trendingShows()">{{ 'Search.TvShows.Trending' | translate }}</a>
</li>
<li>
<a (click)="mostWatchedShows()">{{ 'Search.TvShows.MostWatched' | translate }}</a>
</li>
<li>
<a (click)="anticipatedShows()">{{ 'Search.TvShows.MostAnticipated' | translate }}</a>
</li>
</ul>
</div>
</ng-template>
<div class="input-group">
<input id="search" type="text" placeholder="{{ 'Search.SearchBarPlaceholder' | translate }}"
class="form-control form-control-custom form-control-search form-control-withbuttons"
(keyup)="search($event)">
<div class="input-group-addon right-radius">
<div class="search-button-container-inline">
<ng-template [ngTemplateOutlet]="FilterRef"></ng-template>
</div>
<i id="tvSearchButton" class="fas fa-search"></i>
</div>
</div>
<div class="row search-button-container">
<ng-template [ngTemplateOutlet]="FilterRef"></ng-template>
</div>
<remaining-requests [tv]="true" [quotaRefreshEvents]="tvRequested.asObservable()" #remainingTvShows></remaining-requests>
<!-- Movie content -->
<div id="actorMovieList">
</div>
<!-- TV content -->
<div id="tvList">
<div *ngIf="searchApplied && tvResults?.length <= 0" class='no-search-results'>
<i class='fas fa-film no-search-results-icon'></i>
<div class='no-search-results-text'>{{ 'Search.NoResults' | translate }}</div>
</div>
<div *ngIf="tvResults" >
<div *ngFor="let node of tvResults">
<!--This is the section that holds the parent level search results set-->
<div *ngIf="node">
<div class="row">
<div class="myBg backdrop" [style.background-image]="node?.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-2 small-padding">
<img *ngIf="node.banner" class="img-responsive poster tv-poster" width="150" [src]="node.banner" alt="poster">
</div>
<div class="col-sm-8 small-padding">
<div>
<a *ngIf="node.imdbId" href="{{node.imdbId}}" target="_blank">
<h4>{{node.title}} ({{node.firstAired | amLocal | amDateFormat: 'YYYY'}})</h4>
</a>
<span class="tags">
<a *ngIf="node.homepage" id="homepageLabel" href="{{node.homepage}}" target="_blank">
<span class="label label-info">{{ 'Search.Movies.HomePage' | translate }}</span>
</a>
<a *ngIf="node.trailer" id="trailerLabel" href="{{node.trailer}}" target="_blank">
<span class="label label-info">{{ 'Search.Movies.Trailer' | translate }}</span>
</a>
<span *ngIf="node.status" class="label label-primary" id="statusLabel" target="_blank">{{node.status}}</span>
<span *ngIf="node.firstAired" class="label label-info" target="_blank" id="airDateLabel">{{ 'Search.TvShows.AirDate' | translate }} {{node.firstAired | amLocal | amUserLocale | amDateFormat: 'L'}}</span>
<span *ngIf="node.network" class="label label-info" id="networkLabel" target="_blank">{{node.network}}</span>
<span *ngIf="node.available" class="label label-success" id="availableLabel">{{ 'Common.Available' | translate }}</span>
<span *ngIf="node.partlyAvailable" class="label label-warning" id="partiallyAvailableLabel">{{ 'Common.PartlyAvailable' | translate }}</span>
</span>
<br />
<br />
</div>
<p class="tv-overview">{{node.overview}}</p>
</div>
<div class="col-sm-2 small-padding">
<div *ngIf="!node.fullyAvailable" class="dropdown">
<button class="btn btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fas fa-plus"></i> {{ 'Common.Request' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li>
<a href="#" (click)="allSeasons(node, $event)">{{ 'Search.TvShows.AllSeasons' | translate }}</a>
</li>
<li>
<a href="#" (click)="firstSeason(node, $event)">{{ 'Search.TvShows.FirstSeason' | translate }}</a>
</li>
<li>
<a href="#" (click)="latestSeason(node, $event)">{{ 'Search.TvShows.LatestSeason' | translate }}</a>
</li>
<li>
<a href="#" (click)="openClosestTab(node, $event)">{{ 'Search.TvShows.Select' | translate }}</a>
</li>
</ul>
</div>
<div *ngIf="node.fullyAvailable">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fas fa-check"></i> {{ 'Common.Available' | translate }}
</button>
</div>
<br />
<div *ngIf="node.plexUrl && node.available">
<a style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.plexUrl}}" target="_blank">
<i class="far fa-eye"></i> {{ 'Search.ViewOnPlex' | translate }}
</a>
</div>
<div *ngIf="node.embyUrl && node.available">
<a style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline" href="{{node.embyUrl}}" target="_blank">
<i class="far fa-eye"></i> {{ 'Search.ViewOnEmby' | translate }}
</a>
</div>
<div *ngIf="node.jellyfinUrl && node.available">
<a style="text-align: right" id="jellyfinbtn" class="btn btn-sm btn-success-outline" href="{{node.jellyfinUrl}}" target="_blank">
<i class="far fa-eye"></i> {{ 'Search.ViewOnJellyfin' | translate }}
</a>
</div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<i class="fas fa-plus"></i> {{ 'Requests.ReportIssue' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories">
<a [routerLink]="" (click)="reportIssue(cat, node)">{{cat.value}}</a>
</li>
</ul>
</div>
<div *ngIf="!node.available">
<br />
<br />
</div>
</div>
</div>
</div>
<!--This is the section that holds the child seasons if they want to specify specific episodes-->
<div *ngIf="node.open">
<seriesinformation [seriesId]="node.id" [tvRequested]="tvRequested"></seriesinformation>
</div>
<br/>
<br/>
</div>
</div>
</div>
</div>
<issue-report [movie]="false" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequestTitle"
[issueCategory]="issueCategorySelected" [id]="issueRequestId" [providerId]="issueProviderId"></issue-report>

@ -1,238 +0,0 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { Component, Input, OnInit, Inject } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { AuthService } from "../auth/auth.service";
import { IIssueCategory, IRequestEngineResult, ISearchTvResult, ISeasonsViewModel, ITvRequestViewModel } from "../interfaces";
import { ImageService, NotificationService, RequestService, SearchService } from "../services";
@Component({
selector: "tv-search",
templateUrl: "./tvsearch.component.html",
styleUrls: ["./../requests/tvrequests.component.scss"],
})
export class TvSearchComponent implements OnInit {
public searchText: string;
public searchChanged = new Subject<string>();
public tvResults: ISearchTvResult[];
public tvRequested: Subject<void> = new Subject<void>();
public result: IRequestEngineResult;
public searchApplied = false;
public defaultPoster: string;
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issuesBarVisible = false;
public issueRequestTitle: string;
public issueRequestId: number;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
private href: string;
constructor(
private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService,
private imageService: ImageService, private sanitizer: DomSanitizer,
@Inject(APP_BASE_HREF) href:string) {
this.href = href;
this.searchChanged.pipe(
debounceTime(600), // Wait Xms after the last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
this.clearResults();
return;
}
this.searchService.searchTv(this.searchText)
.subscribe(x => {
this.tvResults = x;
this.searchApplied = true;
this.getExtraInfo();
});
});
this.defaultPoster = "../../../images/default_tv_poster.png";
const base = this.href;
if (base) {
this.defaultPoster = "../../.." + base + "/images/default_tv_poster.png";
}
}
public openClosestTab(node: ISearchTvResult,el: any) {
el.preventDefault();
node.open = !node.open;
}
public ngOnInit() {
this.searchText = "";
this.tvResults = [];
this.result = {
message: "",
result: false,
errorMessage: "",
};
this.popularShows();
}
public search(text: any) {
this.searchChanged.next(text.target.value);
}
public popularShows() {
this.clearResults();
this.searchService.popularTv()
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
});
}
public trendingShows() {
this.clearResults();
this.searchService.trendingTv()
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
});
}
public mostWatchedShows() {
this.clearResults();
this.searchService.mostWatchedTv()
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
});
}
public anticipatedShows() {
this.clearResults();
this.searchService.anticipatedTv()
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
});
}
public getExtraInfo() {
this.tvResults.forEach((val, index) => {
this.imageService.getTvBanner(val.id).subscribe(x => {
if (x) {
val.background = this.sanitizer.
bypassSecurityTrustStyle
("url(" + x + ")");
}
});
this.searchService.getShowInformation(val.id)
.subscribe(x => {
if (x) {
this.setDefaults(x);
this.updateItem(val, x);
} else {
const index = this.tvResults.indexOf(val, 0);
if (index > -1) {
this.tvResults.splice(index, 1);
}
}
});
});
}
public request(searchResult: ISearchTvResult) {
searchResult.requested = true;
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMovie")) {
searchResult.approved = true;
}
const viewModel = <ITvRequestViewModel> { firstSeason: searchResult.firstSeason, latestSeason: searchResult.latestSeason, requestAll: searchResult.requestAll, tvDbId: searchResult.id };
viewModel.seasons = [];
searchResult.seasonRequests.forEach((season) => {
const seasonsViewModel = <ISeasonsViewModel> { seasonNumber: season.seasonNumber, episodes: [] };
season.episodes.forEach(ep => {
if (!searchResult.latestSeason || !searchResult.requestAll || !searchResult.firstSeason) {
if (ep.requested) {
seasonsViewModel.episodes.push({ episodeNumber: ep.episodeNumber });
}
}
});
viewModel.seasons.push(seasonsViewModel);
});
this.requestService.requestTv(viewModel)
.subscribe(x => {
this.tvRequested.next();
this.result = x;
if (this.result.result) {
this.notificationService.success(
`Request for ${searchResult.title} has been added successfully`);
} else {
if (this.result.errorMessage && this.result.message) {
this.notificationService.warning("Request Added", `${this.result.message} - ${this.result.errorMessage}`);
} else {
this.notificationService.warning("Request Added", this.result.message ? this.result.message : this.result.errorMessage);
}
}
});
}
public allSeasons(searchResult: ISearchTvResult, event: any) {
event.preventDefault();
searchResult.requestAll = true;
this.request(searchResult);
}
public firstSeason(searchResult: ISearchTvResult, event: any) {
event.preventDefault();
searchResult.firstSeason = true;
this.request(searchResult);
}
public latestSeason(searchResult: ISearchTvResult, event: any) {
event.preventDefault();
searchResult.latestSeason = true;
this.request(searchResult);
}
public reportIssue(catId: IIssueCategory, req: ISearchTvResult) {
this.issueRequestId = req.id;
const firstAiredDate = new Date(req.firstAired);
this.issueRequestTitle = req.title + ` (${firstAiredDate.getFullYear()})`;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
}
private updateItem(key: ISearchTvResult, updated: ISearchTvResult) {
const index = this.tvResults.indexOf(key, 0);
if (index > -1) {
// Update certain properties, otherwise we will loose some data
this.tvResults[index].title = updated.title;
this.tvResults[index].banner = updated.banner;
this.tvResults[index].imdbId = updated.imdbId;
this.tvResults[index].seasonRequests = updated.seasonRequests;
this.tvResults[index].seriesId = updated.seriesId;
this.tvResults[index].fullyAvailable = updated.fullyAvailable;
this.tvResults[index].background = updated.banner;
}
}
private setDefaults(x: ISearchTvResult) {
if (x.banner === null) {
x.banner = this.defaultPoster;
}
if (x.imdbId === null) {
x.imdbId = "https://www.tvmaze.com/shows/" + x.seriesId;
} else {
x.imdbId = "http://www.imdb.com/title/" + x.imdbId + "/";
}
}
private clearResults() {
this.tvResults = [];
this.searchApplied = false;
}
}

@ -0,0 +1,42 @@
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { Story, Meta } from '@storybook/angular/types-6-0';
import Button from './button.component';
// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
export default {
title: 'Example/Button',
component: Button,
// More on argTypes: https://storybook.js.org/docs/angular/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
} as Meta;
// More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
const Template: Story<Button> = (args: Button) => ({
props: args,
});
export const Primary = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
Primary.args = {
primary: true,
label: 'Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
label: 'Button',
};
export const Large = Template.bind({});
Large.args = {
size: 'large',
label: 'Button',
};
export const Small = Template.bind({});
Small.args = {
size: 'small',
label: 'Button',
};

@ -0,0 +1,35 @@
import { moduleMetadata } from '@storybook/angular';
import { CommonModule } from '@angular/common';
import type { Story, Meta } from '@storybook/angular';
import Button from './button.component';
import Header from './header.component';
export default {
title: 'Example/Header',
component: Header,
decorators: [
moduleMetadata({
declarations: [Button],
imports: [CommonModule],
}),
],
parameters: {
// More on Story layout: https://storybook.js.org/docs/angular/configure/story-layout
layout: 'fullscreen',
},
} as Meta;
const Template: Story<Header> = (args: Header) => ({
props: args,
});
export const LoggedIn = Template.bind({});
LoggedIn.args = {
user: {
name: 'Jane Doe',
},
};
export const LoggedOut = Template.bind({});
LoggedOut.args = {};

@ -0,0 +1,211 @@
import { Meta } from '@storybook/addon-docs';
import Code from './assets/code-brackets.svg';
import Colors from './assets/colors.svg';
import Comments from './assets/comments.svg';
import Direction from './assets/direction.svg';
import Flow from './assets/flow.svg';
import Plugin from './assets/plugin.svg';
import Repo from './assets/repo.svg';
import StackAlt from './assets/stackalt.svg';
<Meta title="Example/Introduction" />
<style>{`
.subheading {
--mediumdark: '#999999';
font-weight: 900;
font-size: 13px;
color: #999;
letter-spacing: 6px;
line-height: 24px;
text-transform: uppercase;
margin-bottom: 12px;
margin-top: 40px;
}
.link-list {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
row-gap: 10px;
}
@media (min-width: 620px) {
.link-list {
row-gap: 20px;
column-gap: 20px;
grid-template-columns: 1fr 1fr;
}
}
@media all and (-ms-high-contrast:none) {
.link-list {
display: -ms-grid;
-ms-grid-columns: 1fr 1fr;
-ms-grid-rows: 1fr 1fr;
}
}
.link-item {
display: block;
padding: 20px 30px 20px 15px;
border: 1px solid #00000010;
border-radius: 5px;
transition: background 150ms ease-out, border 150ms ease-out, transform 150ms ease-out;
color: #333333;
display: flex;
align-items: flex-start;
}
.link-item:hover {
border-color: #1EA7FD50;
transform: translate3d(0, -3px, 0);
box-shadow: rgba(0, 0, 0, 0.08) 0 3px 10px 0;
}
.link-item:active {
border-color: #1EA7FD;
transform: translate3d(0, 0, 0);
}
.link-item strong {
font-weight: 700;
display: block;
margin-bottom: 2px;
}
.link-item img {
height: 40px;
width: 40px;
margin-right: 15px;
flex: none;
}
.link-item span {
font-size: 14px;
line-height: 20px;
}
.tip {
display: inline-block;
border-radius: 1em;
font-size: 11px;
line-height: 12px;
font-weight: 700;
background: #E7FDD8;
color: #66BF3C;
padding: 4px 12px;
margin-right: 10px;
vertical-align: top;
}
.tip-wrapper {
font-size: 13px;
line-height: 20px;
margin-top: 40px;
margin-bottom: 40px;
}
.tip-wrapper code {
font-size: 12px;
display: inline-block;
}
`}</style>
# Welcome to Storybook
Storybook helps you build UI components in isolation from your app's business logic, data, and context.
That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA.
Browse example stories now by navigating to them in the sidebar.
View their code in the `src/stories` directory to learn how they work.
We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages.
<div className="subheading">Configure</div>
<div className="link-list">
<a
className="link-item"
href="https://storybook.js.org/docs/react/addons/addon-types"
target="_blank"
>
<img src={Plugin} alt="plugin" />
<span>
<strong>Presets for popular tools</strong>
Easy setup for TypeScript, SCSS and more.
</span>
</a>
<a
className="link-item"
href="https://storybook.js.org/docs/react/configure/webpack"
target="_blank"
>
<img src={StackAlt} alt="Build" />
<span>
<strong>Build configuration</strong>
How to customize webpack and Babel
</span>
</a>
<a
className="link-item"
href="https://storybook.js.org/docs/react/configure/styling-and-css"
target="_blank"
>
<img src={Colors} alt="colors" />
<span>
<strong>Styling</strong>
How to load and configure CSS libraries
</span>
</a>
<a
className="link-item"
href="https://storybook.js.org/docs/react/get-started/setup#configure-storybook-for-your-stack"
target="_blank"
>
<img src={Flow} alt="flow" />
<span>
<strong>Data</strong>
Providers and mocking for data libraries
</span>
</a>
</div>
<div className="subheading">Learn</div>
<div className="link-list">
<a className="link-item" href="https://storybook.js.org/docs" target="_blank">
<img src={Repo} alt="repo" />
<span>
<strong>Storybook documentation</strong>
Configure, customize, and extend
</span>
</a>
<a className="link-item" href="https://storybook.js.org/tutorials/" target="_blank">
<img src={Direction} alt="direction" />
<span>
<strong>In-depth guides</strong>
Best practices from leading teams
</span>
</a>
<a className="link-item" href="https://github.com/storybookjs/storybook" target="_blank">
<img src={Code} alt="code" />
<span>
<strong>GitHub project</strong>
View the source and add issues
</span>
</a>
<a className="link-item" href="https://discord.gg/storybook" target="_blank">
<img src={Comments} alt="comments" />
<span>
<strong>Discord chat</strong>
Chat with maintainers and the community
</span>
</a>
</div>
<div className="tip-wrapper">
<span className="tip">Tip</span>Edit the Markdown in{' '}
<code>src/stories/Introduction.stories.mdx</code>
</div>

@ -0,0 +1,36 @@
import { moduleMetadata, Story, Meta } from '@storybook/angular';
import { within, userEvent } from '@storybook/testing-library';
import { CommonModule } from '@angular/common';
import Button from './button.component';
import Header from './header.component';
import Page from './page.component';
export default {
title: 'Example/Page',
component: Page,
parameters: {
// More on Story layout: https://storybook.js.org/docs/angular/configure/story-layout
layout: 'fullscreen',
},
decorators: [
moduleMetadata({
declarations: [Button, Header],
imports: [CommonModule],
}),
],
} as Meta;
const Template: Story<Page> = (args: Page) => ({
props: args,
});
export const LoggedOut = Template.bind({});
// More on interaction testing: https://storybook.js.org/docs/angular/writing-tests/interaction-testing
export const LoggedIn = Template.bind({});
LoggedIn.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = await canvas.getByRole('button', { name: /Log in/i });
await userEvent.click(loginButton);
};

@ -0,0 +1,2 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface User {}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/code-brackets</title><g id="illustration/code-brackets" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#87E6E5" d="M11.4139325,12 C11.7605938,12 12,12.5059743 12,13.3779712 L12,17.4951758 L6.43502246,23.3839989 C5.85499251,23.9978337 5.85499251,25.0021663 6.43502246,25.6160011 L12,31.5048242 L12,35.6220288 C12,36.4939606 11.7605228,37 11.4139325,37 C11.2725831,37 11.1134406,36.9158987 10.9453839,36.7379973 L0.435022463,25.6160011 C-0.145007488,25.0021663 -0.145007488,23.9978337 0.435022463,23.3839989 L10.9453839,12.2620027 C11.1134051,12.0841663 11.2725831,12 11.4139325,12 Z M36.5860675,12 C36.7274169,12 36.8865594,12.0841013 37.0546161,12.2620027 L47.5649775,23.3839989 C48.1450075,23.9978337 48.1450075,25.0021663 47.5649775,25.6160011 L37.0546161,36.7379973 C36.8865949,36.9158337 36.7274169,37 36.5860675,37 C36.2394062,37 36,36.4940257 36,35.6220288 L36,31.5048242 L41.5649775,25.6160011 C42.1450075,25.0021663 42.1450075,23.9978337 41.5649775,23.3839989 L36,17.4951758 L36,13.3779712 C36,12.5060394 36.2394772,12 36.5860675,12 Z"/><rect id="Rectangle-7-Copy-5" width="35.57" height="4" x="5.009" y="22.662" fill="#A0DB77" rx="2" transform="translate(22.793959, 24.662305) rotate(-75.000000) translate(-22.793959, -24.662305)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/comments</title><g id="illustration/comments" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Path" fill="#96D07C" d="M2.52730803,17.9196415 C2.44329744,17.9745167 2.36370847,18.000488 2.29303375,18.000488 C2.1197031,18.000488 2,17.8443588 2,17.5752855 L2,4 C2,1.790861 3.790861,3.23296945e-13 6,3.23296945e-13 L33.9995117,3.23296945e-13 C36.2086507,3.23296945e-13 37.9995117,1.790861 37.9995117,4 L37.9995117,9.999512 C37.9995117,12.208651 36.2086507,13.999512 33.9995117,13.999512 L8,13.999512 C7.83499225,13.999512 7.6723181,13.9895206 7.51254954,13.9701099 L2.52730803,17.9196415 Z"/><path id="Path" fill="#73E1E0" d="M7.51066,44.9703679 L2.52730803,47.9186655 C2.44329744,47.9735407 2.36370847,47.999512 2.29303375,47.999512 C2.1197031,47.999512 2,47.8433828 2,47.5743095 L2,35 C2,32.790861 3.790861,31 6,31 L26,31 C28.209139,31 30,32.790861 30,35 L30,41 C30,43.209139 28.209139,45 26,45 L8,45 C7.8343417,45 7.67103544,44.9899297 7.51066,44.9703679 Z"/><path id="Path" fill="#FFD476" d="M46,19.5 L46,33.0747975 C46,33.3438708 45.8802969,33.5 45.7069663,33.5 C45.6362915,33.5 45.5567026,33.4740287 45.472692,33.4191535 L40.4887103,29.4704446 C40.3285371,29.489956 40.1654415,29.5 40,29.5 L18,29.5 C15.790861,29.5 14,27.709139 14,25.5 L14,19.5 C14,17.290861 15.790861,15.5 18,15.5 L42,15.5 C44.209139,15.5 46,17.290861 46,19.5 Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/direction</title><g id="illustration/direction" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFD476" d="M23.4917015,33.6030641 L2.93840258,31.4321033 C2.38917316,31.3740904 1.99096346,30.8818233 2.04897631,30.3325939 C2.0747515,30.0885705 2.18934861,29.8625419 2.37095722,29.6975265 L34.2609105,0.721285325 C34.6696614,0.349881049 35.3021022,0.38015648 35.6735064,0.788907393 C35.9232621,1.06377731 36.0001133,1.45442096 35.8730901,1.80341447 L24.5364357,32.9506164 C24.3793473,33.3822133 23.9484565,33.6513092 23.4917015,33.6030641 L23.4917015,33.6030641 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" d="M24.3163597,33.2881029 C24.0306575,33.0138462 23.9337246,32.5968232 24.069176,32.2246735 L35.091923,1.9399251 C35.2266075,1.56988243 35.5659249,1.31333613 35.9586669,1.28460955 C36.5094802,1.24432106 36.9886628,1.65818318 37.0289513,2.20899647 L40.2437557,46.1609256 C40.2644355,46.4436546 40.1641446,46.7218752 39.9678293,46.9263833 C39.5853672,47.3248067 38.9523344,47.3377458 38.5539111,46.9552837 L24.3163597,33.2881029 L24.3163597,33.2881029 Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/flow</title><g id="illustration/flow" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" fill-rule="nonzero" d="M30,29 C32.7614237,29 35,26.7614237 35,24 C35,14.6111593 27.3888407,7 18,7 C8.61115925,7 1,14.6111593 1,24 C1,33.3888407 8.61115925,41 18,41 C19.3333404,41 20.6447683,40.8466238 21.9154603,40.5471706 C19.5096374,39.3319645 17.5510566,37.8612875 16.0456579,36.1314815 C14.1063138,33.9030427 12.769443,31.0725999 12.0293806,27.6556449 C11.360469,26.565281 11,25.3082308 11,24 C11,20.1340068 14.1340068,17 18,17 C21.8659932,17 25,20.1340068 25,24 C25,26.125 27.7040312,29 30,29 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" fill-rule="nonzero" d="M42,29 C44.7614237,29 47,26.7614237 47,24 C47,14.6111593 39.3888407,7 30,7 C20.6111593,7 13,14.6111593 13,24 C13,33.3888407 20.6111593,41 30,41 C31.3333404,41 32.6447683,40.8466238 33.9154603,40.5471706 C31.5096374,39.3319645 29.4051056,37.9781963 28.0456579,36.1314815 C26.0625,33.4375 23,27.1875 23,24 C23,20.1340068 26.1340068,17 30,17 C33.8659932,17 37,20.1340068 37,24 C37.02301,26.3435241 39.7040312,29 42,29 Z" transform="translate(30.000000, 24.000000) scale(-1, -1) translate(-30.000000, -24.000000)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/plugin</title><g id="illustration/plugin" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" d="M26,15.3994248 C26,15.4091303 26,15.4188459 26,15.4285714 L26,21.4694881 C25.8463595,21.4969567 25.6941676,21.51275 25.5873784,21.51275 C25.4974117,21.51275 25.4230979,21.4768034 25.377756,21.4206259 L25.2660784,21.2822603 L25.1317423,21.1657666 C24.2436317,20.3956144 23.100098,19.9633214 21.895551,19.9633214 C19.2039137,19.9633214 17,22.1075558 17,24.7804643 C17,27.4533728 19.2039137,29.5976071 21.895551,29.5976071 C23.1972122,29.5976071 24.3149423,29.2878193 25.1231445,28.3613697 C25.4542273,27.9818463 25.568273,27.9073214 25.5873784,27.9073214 C25.681532,27.9073214 25.8352452,27.9239643 26,27.9524591 L26,32.5714286 C26,32.5811541 26,32.5908697 26,32.6005752 L26,33 C26,35.209139 24.209139,37 22,37 L4,37 C1.790861,37 0,35.209139 0,33 L0,15 C0,12.790861 1.790861,11 4,11 L22,11 C24.209139,11 26,12.790861 26,15 L26,15.3994248 Z"/><path id="Path" fill="#87E6E5" d="M27.9998779,32.5714286 C27.9998779,33.3604068 28.6572726,34 29.4682101,34 L46.5315458,34 C47.3424832,34 47.9998779,33.3604068 47.9998779,32.5714286 L47.9998779,15.4285714 C47.9998779,14.6395932 47.3424832,14 46.5315458,14 L29.4682101,14 C28.6572726,14 27.9998779,14.6395932 27.9998779,15.4285714 L27.9998779,21.8355216 C27.9334367,22.2650514 27.8567585,22.6454496 27.746391,22.8084643 C27.4245309,23.2838571 26.2402709,23.51275 25.5873784,23.51275 C24.8705773,23.51275 24.2322714,23.1857725 23.8214379,22.6767605 C23.3096996,22.2329909 22.6349941,21.9633214 21.895551,21.9633214 C20.2963823,21.9633214 19,23.2245992 19,24.7804643 C19,26.3363293 20.2963823,27.5976071 21.895551,27.5976071 C22.5398535,27.5976071 23.2399343,27.477727 23.6160247,27.0466112 C24.1396029,26.4464286 24.7367044,25.9073214 25.5873784,25.9073214 C26.2402709,25.9073214 27.5912951,26.1766031 27.8226692,26.6116071 C27.8819199,26.7230038 27.9403239,26.921677 27.9998779,27.1556219 L27.9998779,32.5714286 Z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/repo</title><g id="illustration/repo" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Rectangle-62-Copy" fill="#B7F0EF" d="M27.2217723,9.04506931 L41.2217723,6.2682098 C43.3886973,5.83840648 45.4937616,7.2466219 45.9235649,9.41354696 C45.9743993,9.66983721 46,9.93049166 46,10.1917747 L46,32.581381 C46,34.4904961 44.650862,36.1335143 42.7782277,36.5049459 L28.7782277,39.2818054 C26.6113027,39.7116087 24.5062384,38.3033933 24.0764351,36.1364682 C24.0256007,35.880178 24,35.6195235 24,35.3582405 L24,12.9686342 C24,11.0595191 25.349138,9.4165009 27.2217723,9.04506931 Z" opacity=".7"/><path id="Combined-Shape" fill="#87E6E5" d="M6.77822775,6.2682098 L20.7782277,9.04506931 C22.650862,9.4165009 24,11.0595191 24,12.9686342 L24,35.3582405 C24,37.5673795 22.209139,39.3582405 20,39.3582405 C19.738717,39.3582405 19.4780625,39.3326398 19.2217723,39.2818054 L5.22177225,36.5049459 C3.34913798,36.1335143 2,34.4904961 2,32.581381 L2,10.1917747 C2,7.98263571 3.790861,6.19177471 6,6.19177471 C6.26128305,6.19177471 6.5219375,6.21737537 6.77822775,6.2682098 Z"/><path id="Rectangle-63-Copy-2" fill="#61C1FD" d="M22,10 C23.1666667,10.2291667 24.0179036,10.625 24.5537109,11.1875 C25.0895182,11.75 25.5716146,12.875 26,14.5625 C26,29.3020833 26,37.5208333 26,39.21875 C26,40.9166667 26.4241536,42.9583333 27.2724609,45.34375 L24.5537109,41.875 L22.9824219,45.34375 C22.327474,43.1979167 22,41.2291667 22,39.4375 C22,37.6458333 22,27.8333333 22,10 Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/stackalt</title><g id="illustration/stackalt" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFAE00" d="M23.8628277,0 L23.8628277,48 L3.32291648,36.2491883 L3.32155653,11.9499781 L23.8628277,0 Z M23.8670509,0 L44.408322,11.9499781 L44.4069621,36.2491883 L23.8670509,48 L23.8670509,0 Z" opacity=".196"/><path id="Rectangle-46-Copy-3" fill="#66BF3C" d="M15.8232279,19.1155258 L24.7368455,21.4714881 C29.6053842,22.7582937 33.4077423,26.5606518 34.694548,31.4291905 L37.0505103,40.3428082 C37.6150232,42.4786032 36.3412474,44.6676353 34.2054524,45.2321482 C33.5569474,45.4035549 32.87575,45.4091235 32.2245294,45.2483418 L23.3459013,43.0562718 C18.2976962,41.809906 14.3561301,37.8683399 13.1097642,32.8201348 L10.9176943,23.9415066 C10.3881737,21.7967682 11.6975664,19.6288529 13.8423049,19.0993322 C14.4935255,18.9385505 15.1747229,18.9441191 15.8232279,19.1155258 Z" opacity=".5" transform="translate(23.999997, 32.166058) rotate(-45.000000) translate(-23.999997, -32.166058)"/><path id="Rectangle-46-Copy-2" fill="#FFAE00" d="M15.8232279,11.2216893 L24.7368455,13.5776516 C29.6053842,14.8644572 33.4077423,18.6668153 34.694548,23.5353541 L37.0505103,32.4489717 C37.6150232,34.5847667 36.3412474,36.7737988 34.2054524,37.3383117 C33.5569474,37.5097184 32.87575,37.515287 32.2245294,37.3545053 L23.3459013,35.1624353 C18.2976962,33.9160695 14.3561301,29.9745034 13.1097642,24.9262983 L10.9176943,16.0476701 C10.3881737,13.9029317 11.6975664,11.7350164 13.8423049,11.2054957 C14.4935255,11.044714 15.1747229,11.0502826 15.8232279,11.2216893 Z" opacity=".5" transform="translate(23.999997, 24.272222) rotate(-45.000000) translate(-23.999997, -24.272222)"/><path id="Rectangle-46-Copy" fill="#FC521F" d="M15.8232279,3.32785281 L24.7368455,5.68381509 C29.6053842,6.97062075 33.4077423,10.7729788 34.694548,15.6415176 L37.0505103,24.5551352 C37.6150232,26.6909302 36.3412474,28.8799623 34.2054524,29.4444752 C33.5569474,29.6158819 32.87575,29.6214505 32.2245294,29.4606688 L23.3459013,27.2685988 C18.2976962,26.022233 14.3561301,22.0806669 13.1097642,17.0324618 L10.9176943,8.15383364 C10.3881737,6.00909519 11.6975664,3.84117987 13.8423049,3.31165925 C14.4935255,3.15087753 15.1747229,3.15644615 15.8232279,3.32785281 Z" opacity=".5" transform="translate(23.999997, 16.378385) rotate(-45.000000) translate(-23.999997, -16.378385)"/></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1,53 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'storybook-button',
template: ` <button
type="button"
(click)="onClick.emit($event)"
[ngClass]="classes"
[ngStyle]="{ 'background-color': backgroundColor }"
>
{{ label }}
</button>`,
styleUrls: ['./button.css'],
})
export default class ButtonComponent {
/**
* Is this the principal call to action on the page?
*/
@Input()
primary = false;
/**
* What background color to use
*/
@Input()
backgroundColor?: string;
/**
* How large should the button be?
*/
@Input()
size: 'small' | 'medium' | 'large' = 'medium';
/**
* Button contents
*
* @required
*/
@Input()
label = 'Button';
/**
* Optional click handler
*/
@Output()
onClick = new EventEmitter<Event>();
public get classes(): string[] {
const mode = this.primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return ['storybook-button', `storybook-button--${this.size}`, mode];
}
}

@ -0,0 +1,30 @@
.storybook-button {
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: 700;
border: 0;
border-radius: 3em;
cursor: pointer;
display: inline-block;
line-height: 1;
}
.storybook-button--primary {
color: white;
background-color: #1ea7fd;
}
.storybook-button--secondary {
color: #333;
background-color: transparent;
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
}
.storybook-button--small {
font-size: 12px;
padding: 10px 16px;
}
.storybook-button--medium {
font-size: 14px;
padding: 11px 20px;
}
.storybook-button--large {
font-size: 16px;
padding: 12px 24px;
}

@ -0,0 +1,44 @@
// // also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
// import { Meta, Story } from '@storybook/angular/types-6-0';
// import Card from '../app/components/card/card.component';
// // More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
// export default {
// title: 'Ombi/Card',
// component: Card,
// // More on argTypes: https://storybook.js.org/docs/angular/api/argtypes
// argTypes: {
// backgroundColor: { control: 'color' },
// },
// } as Meta;
// // More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
// const Template: Story<Card> = (args: Card) => ({
// props: args,
// });
// export const Primary = Template.bind({});
// // More on args: https://storybook.js.org/docs/angular/writing-stories/args
// Primary.args = {
// primary: true,
// label: 'Buttonaaaa',
// };
// export const Secondary = Template.bind({});
// Secondary.args = {
// label: 'Buttonaaaa',
// };
// export const Large = Template.bind({});
// Large.args = {
// size: 'large',
// label: 'Buttonaaa',
// };
// export const Small = Template.bind({});
// Small.args = {
// size: 'small',
// label: 'Buttonaaa',
// };

@ -0,0 +1,75 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { User } from './User';
@Component({
selector: 'storybook-header',
template: `<header>
<div class="wrapper">
<div>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
fill="#FFF"
/>
<path
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
fill="#555AB9"
/>
<path
d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z"
fill="#91BAF8"
/>
</g>
</svg>
<h1>Acme</h1>
</div>
<div>
<div *ngIf="user">
<span class="welcome">
Welcome, <b>{{ user.name }}</b
>!
</span>
<storybook-button
*ngIf="user"
size="small"
(onClick)="onLogout.emit($event)"
label="Log out"
></storybook-button>
</div>
<div *ngIf="!user">
<storybook-button
*ngIf="!user"
size="small"
class="margin-left"
(onClick)="onLogin.emit($event)"
label="Log in"
></storybook-button>
<storybook-button
*ngIf="!user"
primary
size="small"
primary="true"
class="margin-left"
(onClick)="onCreateAccount.emit($event)"
label="Sign up"
></storybook-button>
</div>
</div>
</div>
</header>`,
styleUrls: ['./header.css'],
})
export default class HeaderComponent {
@Input()
user: User | null = null;
@Output()
onLogin = new EventEmitter<Event>();
@Output()
onLogout = new EventEmitter<Event>();
@Output()
onCreateAccount = new EventEmitter<Event>();
}

@ -0,0 +1,32 @@
.wrapper {
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 15px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
svg {
display: inline-block;
vertical-align: top;
}
h1 {
font-weight: 900;
font-size: 20px;
line-height: 1;
margin: 6px 0 6px 10px;
display: inline-block;
vertical-align: top;
}
button + button {
margin-left: 10px;
}
.welcome {
color: #333;
font-size: 14px;
margin-right: 10px;
}

@ -0,0 +1,77 @@
import { Component } from '@angular/core';
import { User } from './User';
@Component({
selector: 'storybook-page',
template: `<article>
<storybook-header
[user]="user"
(onLogout)="doLogout()"
(onLogin)="doLogin()"
(onCreateAccount)="doCreateAccount()"
></storybook-header>
<section>
<h2>Pages in Storybook</h2>
<p>
We recommend building UIs with a
<a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer">
<strong>component-driven</strong>
</a>
process starting with atomic components and ending with pages.
</p>
<p>
Render pages with mock data. This makes it easy to build and review page states without
needing to navigate to them in your app. Here are some handy patterns for managing page data
in Storybook:
</p>
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose such data from the
"args" of child component stories
</li>
<li>
Assemble data in the page component from your services. You can mock these services out
using Storybook.
</li>
</ul>
<p>
Get a guided tutorial on component-driven development at
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
Storybook tutorials
</a>
. Read more in the
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer"> docs </a>
.
</p>
<div class="tip-wrapper">
<span class="tip">Tip</span> Adjust the width of the canvas with the
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
id="a"
fill="#999"
/>
</g>
</svg>
Viewports addon in the toolbar
</div>
</section>
</article>`,
styleUrls: ['./page.css'],
})
export default class PageComponent {
user: User | null = null;
doLogout() {
this.user = null;
}
doLogin() {
this.user = { name: 'Jane Doe' };
}
doCreateAccount() {
this.user = { name: 'Jane Doe' };
}
}

@ -0,0 +1,69 @@
section {
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 24px;
padding: 48px 20px;
margin: 0 auto;
max-width: 600px;
color: #333;
}
section h2 {
font-weight: 900;
font-size: 32px;
line-height: 1;
margin: 0 0 4px;
display: inline-block;
vertical-align: top;
}
section p {
margin: 1em 0;
}
section a {
text-decoration: none;
color: #1ea7fd;
}
section ul {
padding-left: 30px;
margin: 1em 0;
}
section li {
margin-bottom: 8px;
}
section .tip {
display: inline-block;
border-radius: 1em;
font-size: 11px;
line-height: 12px;
font-weight: 700;
background: #e7fdd8;
color: #66bf3c;
padding: 4px 12px;
margin-right: 10px;
vertical-align: top;
}
section .tip-wrapper {
font-size: 13px;
line-height: 20px;
margin-top: 40px;
margin-bottom: 40px;
}
section .tip-wrapper svg {
display: inline-block;
height: 12px;
width: 12px;
margin-right: 4px;
vertical-align: top;
margin-top: 3px;
}
section .tip-wrapper svg path {
fill: #1ea7fd;
}

@ -0,0 +1,29 @@
import { TranslateLoader, TranslateModule, TranslateService } from "@ngx-translate/core";
import { HttpClient } from "@angular/common/http";
import { HttpLoaderFactory } from "../app/app.module";
import { NgModule } from "@angular/core";
import { PlatformLocation } from "@angular/common";
/**
A utility module adding I18N support for Storybook stories
**/
@NgModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient, PlatformLocation],
},
}),
],
})
export class StorybookTranslateModule {
constructor(translateService: TranslateService) {
console.log("Configuring the translation service: ", translateService);
console.log("Translations: ", translateService.translations);
translateService.setDefaultLang("en-US");
translateService.use("en-US");
}
}

@ -1,5 +1,5 @@
// Globals
declare var __webpack_public_path__: any;
// declare var __webpack_public_path__: any;
// declare module "*.json" {
// const value: any;
// export default value;

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save