* 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:
parent
6af8ccbf7a
commit
41ad9d9a28
@ -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">
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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
23
src/app/shared/slide-wrapper.ts
Executable 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
|
||||
}
|
||||
@ -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();
|
||||
|
||||
116
src/app/shared/two-way-linked-list.ts
Executable file
116
src/app/shared/two-way-linked-list.ts
Executable 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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user