* slideshow start changed to slideshowService call

* builtin slide switching added to the team forms
* parts of team form made conditional
* fixed keyboard slide switching when not in slideshow
* slideshow slide management totally reworked into linked list
* Date.getDay() -> Date.getDate() as it was originally meant to be
This commit is contained in:
Dávid Danyi 2018-09-17 16:03:23 +02:00
parent 6af8ccbf7a
commit 41ad9d9a28
10 changed files with 286 additions and 86 deletions

View File

@ -2,7 +2,7 @@
<h1 class="ui dividing header">Dashboard</h1> <h1 class="ui dividing header">Dashboard</h1>
<div class="ui four cards"> <div class="ui four cards">
<a class="ui raised yellow card" [routerLink]="['/kanban']" *ngIf="hasTeamSelected"> <a class="ui raised yellow card" (click)="startSlideShow()" *ngIf="hasTeamSelected">
<div class="content"> <div class="content">
<div class="header">Start slideshow</div> <div class="header">Start slideshow</div>
<div class="meta"> <div class="meta">

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import {SettingsService} from '../../shared/service/settings.service'; import {SettingsService} from '../../shared/service/settings.service';
import {SlideShowService} from '../../display/slide-show.service';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@ -10,7 +11,8 @@ import {SettingsService} from '../../shared/service/settings.service';
export class DashboardComponent implements OnInit { export class DashboardComponent implements OnInit {
constructor(private titleService: Title, constructor(private titleService: Title,
private settingService: SettingsService) { } private settingService: SettingsService,
private slideShowService: SlideShowService) { }
ngOnInit() { ngOnInit() {
this.titleService.setTitle('Dashboard : MTAStv'); this.titleService.setTitle('Dashboard : MTAStv');
@ -20,4 +22,7 @@ export class DashboardComponent implements OnInit {
return this.settingService.team.id !== null; return this.settingService.team.id !== null;
} }
public startSlideShow() {
this.slideShowService.startWithFirstSlide();
}
} }

View File

@ -1,29 +1,59 @@
<div class="ui main container"> <div class="ui main container">
<h1 class="ui dividing header">Team editor</h1> <h1 class="ui dividing header">Team editor</h1>
<form class="ui form" #teamEditorForm (ngSubmit)="saveTeam(f)" #f="ngForm"> <form class="ui form" #teamEditorForm (ngSubmit)="saveTeam(f)" #f="ngForm">
<div class="two fields"> <div class="two inline fields">
<div class="six wide field" [class.error]="checkError(teamName)"> <div class="six wide field" [class.error]="checkError(teamName)">
<label for="team_name">Team name</label> <label for="team_name">Team name</label>
<input id="team_name" type="text" name="team_name" <input id="team_name" type="text" name="team_name"
required [(ngModel)]="team.name" #teamName="ngModel"> required [(ngModel)]="team.name" #teamName="ngModel">
</div> </div>
<div class="six wide field" [class.error]="checkError(filterId)">
<label for="filter_id">Jira filter id</label>
<input id="filter_id" type="number" name="filter_id"
required minlength="4" min="1" [(ngModel)]="team.filterId" #filterId="ngModel">
</div>
</div>
<div class="six wide field"> <div class="six wide field">
<label for="team_name"> </label> <label> </label>
<div class="ui checkbox"> <div class="ui checkbox">
<input type="checkbox" id="team_is_active" name="team_is_active" <input type="checkbox" id="team_is_active" name="team_is_active"
[(ngModel)]="team.isActive"> [(ngModel)]="team.isActive">
<label for="team_is_active">Active</label> <label for="team_is_active">Active</label>
</div> </div>
</div> </div>
</div>
<h4 class="ui dividing header">Built-in slides</h4>
<div class="three inline fields">
<div class="three wide field">
<label for="team_name"> </label>
<div class="ui checkbox">
<input type="checkbox" id="kanban_enabled" name="kanban_enabled"
[(ngModel)]="team.kanbanEnabled">
<label for="kanban_enabled">Kanban board</label>
</div>
</div>
<div class="three wide field">
<label for="team_name"> </label>
<div class="ui checkbox">
<input type="checkbox" id="commit_tracker_enabled" name="commit_tracker_enabled"
[(ngModel)]="team.commitTrackerEnabled">
<label for="commit_tracker_enabled">Commit-tracker</label>
</div>
</div>
<div class="three wide field">
<label for="team_name"> </label>
<div class="ui checkbox">
<input type="checkbox" id="watched_enabled" name="watched_enabled"
[(ngModel)]="team.watchedEnabled">
<label for="watched_enabled">Watched</label>
</div>
</div>
</div>
<ng-container *ngIf="team.kanbanEnabled">
<h3 class="ui dividing header">Kanban configuration</h3> <h3 class="ui dividing header">Kanban configuration</h3>
<h4 class="ui dividing header">Daily standup timer</h4> <div class="six wide field" [class.error]="checkError(filterId)">
<label for="filter_id">Jira filter id</label>
<input id="filter_id" type="number" name="filter_id"
required minlength="4" min="1" [required]="team.kanbanEnabled"
[(ngModel)]="team.filterId" #filterId="ngModel">
</div>
<h5 class="ui dividing header">Daily standup timer</h5>
<div class="three inline fields"> <div class="three inline fields">
<div class="two wide field"> <div class="two wide field">
<div class="ui checkbox"> <div class="ui checkbox">
@ -36,7 +66,7 @@
<label for="daily_start">Starts</label> <label for="daily_start">Starts</label>
<div class="ui left icon input"> <div class="ui left icon input">
<input type="time" id="daily_start" name="daily_start" <input type="time" id="daily_start" name="daily_start"
min="9:00" max="15:00" [required]="team.dailyLockEnabled" min="9:00" max="15:00" [required]="team.dailyLockEnabled && team.kanbanEnabled"
[(ngModel)]="team.dailyStartTime" #startTime="ngModel"> [(ngModel)]="team.dailyStartTime" #startTime="ngModel">
<i class="time icon"></i> <i class="time icon"></i>
</div> </div>
@ -45,7 +75,7 @@
<label for="daily_end">Ends</label> <label for="daily_end">Ends</label>
<div class="ui left icon input"> <div class="ui left icon input">
<input type="time" id="daily_end" name="daily_end" <input type="time" id="daily_end" name="daily_end"
min="9:00" max="15:00" [required]="team.dailyLockEnabled" min="9:00" max="15:00" [required]="team.dailyLockEnabled && team.kanbanEnabled"
[(ngModel)]="team.dailyEndTime" #endTime="ngModel"> [(ngModel)]="team.dailyEndTime" #endTime="ngModel">
<i class="time icon"></i> <i class="time icon"></i>
</div> </div>
@ -148,6 +178,7 @@
class="ui medium {{label.color}} label">{{label.name}}<i class="large delete icon" class="ui medium {{label.color}} label">{{label.name}}<i class="large delete icon"
(click)="removeLabel(label)"></i></span> (click)="removeLabel(label)"></i></span>
</div> </div>
</ng-container>
<h3 class="ui dividing header">Team members</h3> <h3 class="ui dividing header">Team members</h3>
<div class="three inline fields"> <div class="three inline fields">

View File

@ -15,6 +15,7 @@ export class AppComponent implements OnInit {
@HostListener('document:keyup', ['$event.key']) @HostListener('document:keyup', ['$event.key'])
private keyPressed(key: string) { private keyPressed(key: string) {
if (this.timerService.autoSwitch) {
switch (key) { switch (key) {
case ' ': case ' ':
this.timerService.togglePause(); this.timerService.togglePause();
@ -29,6 +30,7 @@ export class AppComponent implements OnInit {
break; break;
} }
} }
}
public get paused(): boolean { public get paused(): boolean {
return this.timerService.paused; return this.timerService.paused;

View File

@ -3,74 +3,84 @@ import {Slide, SlideVisibility} from '../shared/slide';
import { SlideService } from '../shared/service/slide.service'; import { SlideService } from '../shared/service/slide.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AnimationDirection, SettingsService } from '../shared/service/settings.service'; import { AnimationDirection, SettingsService } from '../shared/service/settings.service';
import { TwoWayLinkedList } from '../shared/two-way-linked-list';
import { SlideWrapper, WrappedType } from '../shared/slide-wrapper';
@Injectable() @Injectable()
export class SlideShowService { export class SlideShowService {
private oddEven = false; private oddEven = false;
private currentSlideIndex = -1; private cachedSlides: TwoWayLinkedList<SlideWrapper> = new TwoWayLinkedList<SlideWrapper>();
private slides: Array<Slide> = [];
constructor(private slideService: SlideService, constructor(private slideService: SlideService,
private settingsService: SettingsService, private settings: SettingsService,
private router: Router) { private router: Router) {
this.reloadSlides(); this.reloadSlides();
} }
public prevSlide() { public startWithFirstSlide() {
this.settingsService.animationDirection = AnimationDirection.LEFT; this.settings.animationDirection = AnimationDirection.RIGHT;
console.log('prev-in', this.slides.length, this.currentSlideIndex); this.reloadSlides(() => this.switchToWrappedSlide(this.cachedSlides.first));
if (this.currentSlideIndex > this.slides.length) { }
this.currentSlideIndex = this.slides.length;
this.router.navigate(['/watchers']); public prevSlide() {
} else if (this.currentSlideIndex === this.slides.length) { this.settings.animationDirection = AnimationDirection.LEFT;
this.currentSlideIndex--; if (this.cachedSlides.isFirst()) {
this.router.navigate(['/commit-tracker']); this.reloadSlides(() => this.switchToWrappedSlide(this.cachedSlides.last));
} else if (this.currentSlideIndex < 0) { } else {
this.currentSlideIndex = this.slides.length + 1; const prevSlide = this.cachedSlides.prev();
this.reloadSlides(); this.switchToWrappedSlide(prevSlide);
this.router.navigate(['/kanban']);
} else {
this.oddEven = !this.oddEven;
this.router.navigate([
this.oddEven ? '/slideshow-odd' : '/slideshow-even',
this.slides[this.currentSlideIndex].id
]);
this.currentSlideIndex--;
} }
console.log('prev-out', this.slides.length, this.currentSlideIndex);
} }
public nextSlide() { public nextSlide() {
this.settingsService.animationDirection = AnimationDirection.RIGHT; this.settings.animationDirection = AnimationDirection.RIGHT;
console.log('next-in', this.slides.length, this.currentSlideIndex); if (this.cachedSlides.isLast()) {
if (this.currentSlideIndex < 0) { this.reloadSlides(() => this.switchToWrappedSlide(this.cachedSlides.first));
this.currentSlideIndex++;
this.router.navigate(['/kanban']);
} else if (this.currentSlideIndex === this.slides.length) {
this.currentSlideIndex++;
this.router.navigate(['/commit-tracker']);
} else if (this.currentSlideIndex > this.slides.length) {
this.currentSlideIndex = -1;
this.reloadSlides();
this.router.navigate(['/watchers']);
} else { } else {
const nextSlide = this.cachedSlides.next();
this.switchToWrappedSlide(nextSlide);
}
}
private switchToWrappedSlide(wrappedSlide: SlideWrapper) {
if (WrappedType.BUILTIN === wrappedSlide.type) {
this.router.navigate([wrappedSlide.slideRoute]);
} else if (WrappedType.USER === wrappedSlide.type) {
this.oddEven = !this.oddEven; this.oddEven = !this.oddEven;
this.router.navigate([ this.router.navigate([
this.oddEven ? '/slideshow-odd' : '/slideshow-even', this.oddEven ? '/slideshow-odd' : '/slideshow-even',
this.slides[this.currentSlideIndex].id wrappedSlide.slideData.id
]); ]);
this.currentSlideIndex++; } else {
throw Error(`Unknown slide type: ${wrappedSlide.type}`);
} }
console.log('next-out', this.slides.length, this.currentSlideIndex);
} }
private reloadSlides() { private reloadSlides(onReloadFinish: () => void = null) {
const team = this.settingsService.team;
this.slideService.list().subscribe( this.slideService.list().subscribe(
slides => this.slides = slides.filter( slides => {
slide => slide.isVisible && (slide.visibility === SlideVisibility.Public || slide.teams.some(s => s.id === team.id)) this.cachedSlides.clear();
) slides.filter(
(slide: Slide) => slide.isVisible && (slide.visibility === SlideVisibility.Public || slide.teams.some(
s => s.id === this.settings.team.id
))
).map(slide => this.cachedSlides.push(new SlideWrapper(WrappedType.USER, slide)));
if (this.settings.team.kanbanEnabled) {
this.cachedSlides.push(new SlideWrapper(WrappedType.BUILTIN, '/kanban'));
}
if (this.settings.team.commitTrackerEnabled) {
this.cachedSlides.push(new SlideWrapper(WrappedType.BUILTIN, '/commit-tracker'));
}
if (this.settings.team.watchedEnabled) {
this.cachedSlides.push(new SlideWrapper(WrappedType.BUILTIN, '/watchers'));
}
if (null !== onReloadFinish) {
onReloadFinish();
}
}
); );
} }
} }

View File

@ -5,6 +5,9 @@ import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { Slide } from '../slide'; import { Slide } from '../slide';
import { TeamService } from './team.service';
import { SettingsService } from './settings.service';
import { flatMap } from 'rxjs/operators';
@Injectable() @Injectable()
export class SlideService implements Resolve<Array<Slide>>{ export class SlideService implements Resolve<Array<Slide>>{
@ -13,7 +16,9 @@ export class SlideService implements Resolve<Array<Slide>>{
private apiEndPointPosition = environment.apiUrl + '/api/slide-position'; private apiEndPointPosition = environment.apiUrl + '/api/slide-position';
private cachedSlides: Array<Slide> = []; private cachedSlides: Array<Slide> = [];
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient,
private teamService: TeamService,
private settings: SettingsService) {}
private static prepareSlideData(slide: Slide) { private static prepareSlideData(slide: Slide) {
const slideToSave = <any>Object.assign({}, slide); const slideToSave = <any>Object.assign({}, slide);
@ -28,6 +33,11 @@ export class SlideService implements Resolve<Array<Slide>>{
} }
public list(): Observable<Array<Slide>> { public list(): Observable<Array<Slide>> {
if (this.settings.team.id) {
return this.teamService.get(this.settings.team.id).pipe(
flatMap( () => this.httpClient.get<Array<Slide>>(this.apiEndPoint))
);
}
return this.httpClient.get<Array<Slide>>(this.apiEndPoint); return this.httpClient.get<Array<Slide>>(this.apiEndPoint);
} }

View File

@ -15,7 +15,7 @@ const TIME_SEPARATOR = ':';
@Injectable() @Injectable()
export class TimerService implements OnDestroy { export class TimerService implements OnDestroy {
public paused = false; public paused = false;
private autoSwitch = false; public autoSwitch = false;
private slideShowTimer: Subscription; private slideShowTimer: Subscription;
private selfUpdateCheckerTimer: Subscription; private selfUpdateCheckerTimer: Subscription;
private slideTimerSubject: Subject<number> = new Subject<number>(); private slideTimerSubject: Subject<number> = new Subject<number>();
@ -85,8 +85,8 @@ export class TimerService implements OnDestroy {
const startsParts = this.settings.team.dailyStartTime.split(TIME_SEPARATOR).map(part => +part); const startsParts = this.settings.team.dailyStartTime.split(TIME_SEPARATOR).map(part => +part);
const endsParts = this.settings.team.dailyEndTime.split(TIME_SEPARATOR).map(part => +part); const endsParts = this.settings.team.dailyEndTime.split(TIME_SEPARATOR).map(part => +part);
const times = [ const times = [
new Date(now.getFullYear(), now.getMonth(), now.getDay(), startsParts[0], startsParts[1]), new Date(now.getFullYear(), now.getMonth(), now.getDate(), startsParts[0], startsParts[1]),
new Date(now.getFullYear(), now.getMonth(), now.getDay(), endsParts[0], endsParts[1]) new Date(now.getFullYear(), now.getMonth(), now.getDate(), endsParts[0], endsParts[1])
]; ];
const startsAt = min(...times); const startsAt = min(...times);
const endsAt = max(...times); const endsAt = max(...times);

23
src/app/shared/slide-wrapper.ts Executable file
View File

@ -0,0 +1,23 @@
import { Slide } from './slide';
export class SlideWrapper {
public type: WrappedType = null;
public slideData: Slide = null;
public slideRoute: string = null;
public constructor(type: WrappedType, data: string|Slide) {
this.type = type;
if (type === WrappedType.USER) {
this.slideData = data as Slide;
} else if (type === WrappedType.BUILTIN) {
this.slideRoute = data as string;
} else {
throw new Error(`Unknown slide type: ${type}`);
}
}
}
export enum WrappedType {
BUILTIN,
USER
}

View File

@ -7,7 +7,10 @@ export class Team {
name: String = ''; name: String = '';
members: Array<Member> = []; members: Array<Member> = [];
filterId = 0; filterId = 0;
dailyLockEnabled: false; kanbanEnabled = true;
commitTrackerEnabled = true;
watchedEnabled = true;
dailyLockEnabled = false;
dailyStartTime: string; dailyStartTime: string;
dailyEndTime: string; dailyEndTime: string;
backlogColumn: KanbanColumn = new KanbanColumn(); backlogColumn: KanbanColumn = new KanbanColumn();

View File

@ -0,0 +1,116 @@
export class TwoWayLinkedList<T> {
private count = 0;
private entryNode: Node<T> = null;
private lastNode: Node<T> = null;
private nodePtr: Node<T> = null;
public push(newNode: T): TwoWayLinkedList<T> {
if (null === this.lastNode) {
this.entryNode = new Node<T>(newNode);
this.lastNode = this.entryNode;
this.nodePtr = this.entryNode;
this.entryNode
.setPrev(this.entryNode)
.setNext(this.entryNode);
} else {
const prevPtr = this.lastNode;
const nodeToAdd = new Node(newNode);
nodeToAdd.setNext(this.entryNode).setPrev(prevPtr);
prevPtr.setNext(nodeToAdd);
this.lastNode = nodeToAdd;
}
this.count++;
return this;
}
public pop(): T {
if (null === this.lastNode) {
throw new Error('No items in list');
}
let returnData: T;
if (this.entryNode === this.lastNode) {
returnData = this.lastNode.getData();
this.lastNode.setPrev(null).setNext(null);
this.entryNode = null;
this.lastNode = null;
this.nodePtr = null;
} else {
const newLast = this.lastNode.getPrev();
if (this.nodePtr === this.lastNode) {
this.nodePtr = newLast;
}
newLast.setNext(this.entryNode);
returnData = this.lastNode.getData();
this.lastNode.setPrev(null).setNext(null);
this.lastNode = newLast;
}
this.count--;
return returnData;
}
public clear() {
while (this.count > 0) {
this.pop();
}
}
public prev(): T {
this.nodePtr = this.nodePtr.getPrev();
return this.nodePtr.getData();
}
public next(): T {
this.nodePtr = this.nodePtr.getNext();
return this.nodePtr.getData();
}
public isFirst(): boolean {
return this.nodePtr === this.lastNode;
}
public isLast(): boolean {
return this.nodePtr === this.lastNode;
}
public get first(): T {
return this.entryNode.getData();
}
public get last(): T {
return this.lastNode.getData();
}
}
export class Node<T> {
private prev: Node<T> = null;
private next: Node<T> = null;
private readonly data: T = null;
constructor(data: T) {
this.data = data;
}
public getPrev(): Node<T> {
return this.prev;
}
public setPrev(node: Node<T>): Node<T> {
this.prev = node;
return this;
}
public getNext(): Node<T> {
return this.next;
}
public setNext(node: Node<T>): Node<T> {
this.next = node;
return this;
}
public getData(): T {
return this.data;
}
}