* 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>
<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="header">Start slideshow</div>
<div class="meta">

View File

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

View File

@ -1,29 +1,59 @@
<div class="ui main container">
<h1 class="ui dividing header">Team editor</h1>
<form class="ui form" #teamEditorForm (ngSubmit)="saveTeam(f)" #f="ngForm">
<div class="two fields">
<div class="six wide field" [class.error]="checkError(teamName)">
<label for="team_name">Team name</label>
<input id="team_name" type="text" name="team_name"
required [(ngModel)]="team.name" #teamName="ngModel">
</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">
<label for="team_name"> </label>
<div class="two inline fields">
<div class="six wide field" [class.error]="checkError(teamName)">
<label for="team_name">Team name</label>
<input id="team_name" type="text" name="team_name"
required [(ngModel)]="team.name" #teamName="ngModel">
</div>
<div class="six wide field">
<label> </label>
<div class="ui checkbox">
<input type="checkbox" id="team_is_active" name="team_is_active"
[(ngModel)]="team.isActive">
<label for="team_is_active">Active</label>
<input type="checkbox" id="team_is_active" name="team_is_active"
[(ngModel)]="team.isActive">
<label for="team_is_active">Active</label>
</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>
<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="two wide field">
<div class="ui checkbox">
@ -36,7 +66,7 @@
<label for="daily_start">Starts</label>
<div class="ui left icon input">
<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">
<i class="time icon"></i>
</div>
@ -45,7 +75,7 @@
<label for="daily_end">Ends</label>
<div class="ui left icon input">
<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">
<i class="time icon"></i>
</div>
@ -148,6 +178,7 @@
class="ui medium {{label.color}} label">{{label.name}}<i class="large delete icon"
(click)="removeLabel(label)"></i></span>
</div>
</ng-container>
<h3 class="ui dividing header">Team members</h3>
<div class="three inline fields">

View File

@ -15,18 +15,20 @@ export class AppComponent implements OnInit {
@HostListener('document:keyup', ['$event.key'])
private keyPressed(key: string) {
switch (key) {
case ' ':
this.timerService.togglePause();
break;
case 'ArrowLeft':
this.timerService.pause();
this.slideShowService.prevSlide();
break;
case 'ArrowRight':
this.timerService.pause();
this.slideShowService.nextSlide();
break;
if (this.timerService.autoSwitch) {
switch (key) {
case ' ':
this.timerService.togglePause();
break;
case 'ArrowLeft':
this.timerService.pause();
this.slideShowService.prevSlide();
break;
case 'ArrowRight':
this.timerService.pause();
this.slideShowService.nextSlide();
break;
}
}
}

View File

@ -1,76 +1,86 @@
import {Injectable} from '@angular/core';
import {Slide, SlideVisibility} from '../shared/slide';
import {SlideService} from '../shared/service/slide.service';
import {Router} from '@angular/router';
import {AnimationDirection, SettingsService} from '../shared/service/settings.service';
import { Injectable } from '@angular/core';
import { Slide, SlideVisibility } from '../shared/slide';
import { SlideService } from '../shared/service/slide.service';
import { Router } from '@angular/router';
import { AnimationDirection, SettingsService } from '../shared/service/settings.service';
import { TwoWayLinkedList } from '../shared/two-way-linked-list';
import { SlideWrapper, WrappedType } from '../shared/slide-wrapper';
@Injectable()
export class SlideShowService {
private oddEven = false;
private currentSlideIndex = -1;
private slides: Array<Slide> = [];
private cachedSlides: TwoWayLinkedList<SlideWrapper> = new TwoWayLinkedList<SlideWrapper>();
constructor(private slideService: SlideService,
private settingsService: SettingsService,
private settings: SettingsService,
private router: Router) {
this.reloadSlides();
}
public startWithFirstSlide() {
this.settings.animationDirection = AnimationDirection.RIGHT;
this.reloadSlides(() => this.switchToWrappedSlide(this.cachedSlides.first));
}
public prevSlide() {
this.settingsService.animationDirection = AnimationDirection.LEFT;
console.log('prev-in', this.slides.length, this.currentSlideIndex);
if (this.currentSlideIndex > this.slides.length) {
this.currentSlideIndex = this.slides.length;
this.router.navigate(['/watchers']);
} else if (this.currentSlideIndex === this.slides.length) {
this.currentSlideIndex--;
this.router.navigate(['/commit-tracker']);
} else if (this.currentSlideIndex < 0) {
this.currentSlideIndex = this.slides.length + 1;
this.reloadSlides();
this.router.navigate(['/kanban']);
this.settings.animationDirection = AnimationDirection.LEFT;
if (this.cachedSlides.isFirst()) {
this.reloadSlides(() => this.switchToWrappedSlide(this.cachedSlides.last));
} else {
this.oddEven = !this.oddEven;
this.router.navigate([
this.oddEven ? '/slideshow-odd' : '/slideshow-even',
this.slides[this.currentSlideIndex].id
]);
this.currentSlideIndex--;
const prevSlide = this.cachedSlides.prev();
this.switchToWrappedSlide(prevSlide);
}
console.log('prev-out', this.slides.length, this.currentSlideIndex);
}
public nextSlide() {
this.settingsService.animationDirection = AnimationDirection.RIGHT;
console.log('next-in', this.slides.length, this.currentSlideIndex);
if (this.currentSlideIndex < 0) {
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']);
this.settings.animationDirection = AnimationDirection.RIGHT;
if (this.cachedSlides.isLast()) {
this.reloadSlides(() => this.switchToWrappedSlide(this.cachedSlides.first));
} 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.router.navigate([
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() {
const team = this.settingsService.team;
private reloadSlides(onReloadFinish: () => void = null) {
this.slideService.list().subscribe(
slides => this.slides = slides.filter(
slide => slide.isVisible && (slide.visibility === SlideVisibility.Public || slide.teams.some(s => s.id === team.id))
)
slides => {
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 { Slide } from '../slide';
import { TeamService } from './team.service';
import { SettingsService } from './settings.service';
import { flatMap } from 'rxjs/operators';
@Injectable()
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 cachedSlides: Array<Slide> = [];
constructor(private httpClient: HttpClient) {}
constructor(private httpClient: HttpClient,
private teamService: TeamService,
private settings: SettingsService) {}
private static prepareSlideData(slide: Slide) {
const slideToSave = <any>Object.assign({}, slide);
@ -28,6 +33,11 @@ export class SlideService implements Resolve<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);
}

View File

@ -15,7 +15,7 @@ const TIME_SEPARATOR = ':';
@Injectable()
export class TimerService implements OnDestroy {
public paused = false;
private autoSwitch = false;
public autoSwitch = false;
private slideShowTimer: Subscription;
private selfUpdateCheckerTimer: Subscription;
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 endsParts = this.settings.team.dailyEndTime.split(TIME_SEPARATOR).map(part => +part);
const times = [
new Date(now.getFullYear(), now.getMonth(), now.getDay(), startsParts[0], startsParts[1]),
new Date(now.getFullYear(), now.getMonth(), now.getDay(), endsParts[0], endsParts[1])
new Date(now.getFullYear(), now.getMonth(), now.getDate(), startsParts[0], startsParts[1]),
new Date(now.getFullYear(), now.getMonth(), now.getDate(), endsParts[0], endsParts[1])
];
const startsAt = min(...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 = '';
members: Array<Member> = [];
filterId = 0;
dailyLockEnabled: false;
kanbanEnabled = true;
commitTrackerEnabled = true;
watchedEnabled = true;
dailyLockEnabled = false;
dailyStartTime: string;
dailyEndTime: string;
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;
}
}