Skip to content

Commit

Permalink
Merge pull request #46 from zembrodt/develop
Browse files Browse the repository at this point in the history
Merge 0.5.0-dev into main
  • Loading branch information
zembrodt authored Jun 18, 2022
2 parents 9306bf7 + 211af06 commit 13e3fd7
Show file tree
Hide file tree
Showing 24 changed files with 1,549 additions and 141 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "showtunes",
"version": "0.4.4",
"version": "0.5.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
Expand Down Expand Up @@ -64,6 +65,7 @@ export function initializeApp(appConfig: AppConfig): () => Promise<void> {
BrowserAnimationsModule,
FlexLayoutModule,
FontAwesomeModule,
FormsModule,
NgxsModule.forRoot(
[ AuthState, PlaybackState, SettingsState ],
{ developmentMode: !environment.production }
Expand Down
28 changes: 26 additions & 2 deletions src/app/components/album-display/album-display.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import { MatProgressBar, MatProgressBarModule } from '@angular/material/progress
import { MatProgressSpinnerModule, MatSpinner } from '@angular/material/progress-spinner';
import { By } from '@angular/platform-browser';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { NgxsModule } from '@ngxs/store';
import { NgxsModule, Store } from '@ngxs/store';
import { MockProvider } from 'ng-mocks';
import { BehaviorSubject, of } from 'rxjs';
import { AppConfig } from '../../app.config';
import { AlbumModel, TrackModel } from '../../core/playback/playback.model';
import { ChangeSmartColor } from '../../core/settings/settings.actions';
import { DEFAULT_BAR_CODE_COLOR, DEFAULT_CODE_COLOR } from '../../core/settings/settings.model';
import { NgxsSelectorMock } from '../../core/testing/ngxs-selector-mock';
import { ImageResponse } from '../../models/image.model';
Expand Down Expand Up @@ -53,6 +54,7 @@ describe('AlbumDisplayComponent', () => {
let fixture: ComponentFixture<AlbumDisplayComponent>;
let loader: HarnessLoader;
let spotify: SpotifyService;
let store: Store;

let coverArtProducer: BehaviorSubject<ImageResponse>;
let trackProducer: BehaviorSubject<TrackModel>;
Expand All @@ -62,6 +64,7 @@ describe('AlbumDisplayComponent', () => {
let showSpotifyCodeProducer: BehaviorSubject<boolean>;
let backgroundColorProducer: BehaviorSubject<string>;
let barColorProducer: BehaviorSubject<string>;
let useDynamicThemeAccentProducer: BehaviorSubject<boolean>;

beforeAll(() => {
AppConfig.settings = {
Expand Down Expand Up @@ -91,10 +94,12 @@ describe('AlbumDisplayComponent', () => {
provide: AppConfig,
deps: [ MockProvider(HttpClient) ]
},
MockProvider(SpotifyService)
MockProvider(SpotifyService),
MockProvider(Store)
]
}).compileComponents();
spotify = TestBed.inject(SpotifyService);
store = TestBed.inject(Store);
});

beforeEach(() => {
Expand All @@ -110,6 +115,7 @@ describe('AlbumDisplayComponent', () => {
showSpotifyCodeProducer = mockSelectors.defineNgxsSelector<boolean>(component, 'showSpotifyCode$');
backgroundColorProducer = mockSelectors.defineNgxsSelector<string>(component, 'backgroundColor$');
barColorProducer = mockSelectors.defineNgxsSelector<string>(component, 'barColor$');
useDynamicThemeAccentProducer = mockSelectors.defineNgxsSelector<boolean>(component, 'useDynamicThemeAccent$');

fixture.detectChanges();
});
Expand Down Expand Up @@ -317,9 +323,11 @@ describe('AlbumDisplayComponent', () => {
albumProducer.next(TEST_ALBUM_MODEL);
expect(component.smartBackgroundColor).toBeFalsy();
expect(component.smartBarColor).toBeFalsy();
expect(store.dispatch).not.toHaveBeenCalled();
useSmartCodeColorProducer.next(true);
fixture.detectChanges();
expect(spotify.getAlbumColor).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith(new ChangeSmartColor('ABC123'));
expect(component.smartBackgroundColor).toEqual('ABC123');
expect(component.smartBarColor).not.toBeFalsy();
});
Expand All @@ -331,11 +339,27 @@ describe('AlbumDisplayComponent', () => {
albumProducer.next(TEST_ALBUM_MODEL);
expect(component.smartBackgroundColor).toBeFalsy();
expect(component.smartBarColor).toBeFalsy();
expect(store.dispatch).not.toHaveBeenCalled();
useSmartCodeColorProducer.next(true);
fixture.detectChanges();
expect(spotify.getAlbumColor).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith(new ChangeSmartColor(null));
expect(component.smartBackgroundColor).toEqual(DEFAULT_CODE_COLOR);
expect(component.smartBarColor).toEqual(DEFAULT_BAR_CODE_COLOR);
expect(console.error).toHaveBeenCalledTimes(1);
});

it('should set smart color when using dynamic accent theme', () => {
spotify.getAlbumColor = jasmine.createSpy().and.returnValue(of('#ABC123'));
coverArtProducer.next(TEST_IMAGE_RESPONSE);
albumProducer.next(TEST_ALBUM_MODEL);
expect(component.smartBackgroundColor).toBeFalsy();
expect(component.smartBarColor).toBeFalsy();
expect(store.dispatch).not.toHaveBeenCalled();
useDynamicThemeAccentProducer.next(true);
fixture.detectChanges();
expect(spotify.getAlbumColor).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith(new ChangeSmartColor('ABC123'));
expect(component.smartBackgroundColor).toEqual('ABC123');
});
});
28 changes: 21 additions & 7 deletions src/app/components/album-display/album-display.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { faSpotify } from '@fortawesome/free-brands-svg-icons';
import { Select } from '@ngxs/store';
import { Select, Store } from '@ngxs/store';
import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AppConfig } from '../../app.config';
import { AlbumModel, TrackModel } from '../../core/playback/playback.model';
import { PlaybackState } from '../../core/playback/playback.state';
import { ChangeSmartColor } from '../../core/settings/settings.actions';
import { BAR_COLOR_BLACK, BAR_COLOR_WHITE, DEFAULT_BAR_CODE_COLOR, DEFAULT_CODE_COLOR } from '../../core/settings/settings.model';
import { SettingsState } from '../../core/settings/settings.state';
import { expandHexColor, hexToRgb, isHexColor } from '../../core/util';
Expand Down Expand Up @@ -46,19 +47,22 @@ export class AlbumDisplayComponent implements OnInit, OnDestroy {
@Select(SettingsState.spotifyCodeBarColor) barColor$: Observable<string>;
private barColor: string;

@Select(SettingsState.useDynamicThemeAccent) useDynamicThemeAccent$: Observable<boolean>;
private useDynamicThemeAccent;

spotifyCodeUrl: string;
smartBackgroundColor: string;
smartBarColor: string;

// Template constants
readonly spotifyIcon = faSpotify;

constructor(private spotifyService: SpotifyService) {}
constructor(private spotifyService: SpotifyService, private store: Store) {}

ngOnInit(): void {
// Set initial spotify code color
if (this.useSmartCodeColor) {
this.setSmartCodeColor();
if (this.useSmartCodeColor || this.useDynamicThemeAccent) {
this.setSmartColor();
}
this.spotifyCodeUrl = this.getSpotifyCodeUrl();

Expand All @@ -79,7 +83,7 @@ export class AlbumDisplayComponent implements OnInit, OnDestroy {
}*/
this.coverArt = coverArt;
if (this.useSmartCodeColor) {
this.setSmartCodeColor();
this.setSmartColor();
}
});
this.backgroundColor$
Expand All @@ -99,13 +103,21 @@ export class AlbumDisplayComponent implements OnInit, OnDestroy {
.subscribe((useSmartCodeColor) => {
this.useSmartCodeColor = useSmartCodeColor;
if (useSmartCodeColor) {
this.setSmartCodeColor();
this.setSmartColor();
}
this.spotifyCodeUrl = this.getSpotifyCodeUrl();
});
this.showSpotifyCode$
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((showSpotifyCode) => this.showSpotifyCode = showSpotifyCode);
this.useDynamicThemeAccent$
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((useDynamicThemeAccent) => {
this.useDynamicThemeAccent = useDynamicThemeAccent;
if (useDynamicThemeAccent) {
this.setSmartColor();
}
});
}

ngOnDestroy(): void {
Expand Down Expand Up @@ -133,18 +145,20 @@ export class AlbumDisplayComponent implements OnInit, OnDestroy {
return this.createSpotifyCodeUrl(this.backgroundColor, this.barColor);
}

private setSmartCodeColor(): void {
private setSmartColor(): void {
if (AppConfig.settings.env.albumColorUrl && this.coverArt && this.coverArt.url) {
this.spotifyService.getAlbumColor(this.coverArt.url).subscribe((smartColor) => {
this.smartBackgroundColor = smartColor.replace('#', '');
// Check we have a valid code color value
if (isHexColor(this.smartBackgroundColor)) {
this.store.dispatch(new ChangeSmartColor(this.smartBackgroundColor));
// Determine if the bar code should be white or black
// See https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
const rgbColor = hexToRgb(this.smartBackgroundColor);
this.smartBarColor = rgbColor.r * 0.299 + rgbColor.g * 0.587 + rgbColor.b * 0.144 > BAR_CODE_COLOR_THRESHOLD ?
BAR_COLOR_BLACK : BAR_COLOR_WHITE;
} else {
this.store.dispatch(new ChangeSmartColor(null));
console.error(`Retrieved invalid smart code color ${smartColor}: not a valid hex color`);
this.smartBackgroundColor = DEFAULT_CODE_COLOR;
this.smartBarColor = DEFAULT_BAR_CODE_COLOR;
Expand Down
7 changes: 6 additions & 1 deletion src/app/components/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<div class="showtunes-app"
[class]="theme$ | async"
[class]="(theme$ | async) !== null ? (
((useDynamicThemeAccent$ | async) && (dynamicAccentColor$ | async) !== null) ?
(dynamicAccentColor$ | async) + '-' + (theme$ | async) :
(customAccentColor$ | async) !== null ? (customAccentColor$ | async) + '-' + (theme$ | async) :
(theme$ | async)
) : ''"
[style]="{ cursor: fadeCursor ? 'none' : 'inherit' }">
<mat-sidenav-container class="container">
<router-outlet></router-outlet>
Expand Down
107 changes: 106 additions & 1 deletion src/app/components/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MatSidenavModule } from '@angular/material/sidenav';
import { By } from '@angular/platform-browser';
import { RouterOutlet } from '@angular/router';
import { NgxsModule } from '@ngxs/store';
import { MockComponent, MockProvider, MockRender, ngMocks } from 'ng-mocks';
import { MockComponent, MockProvider } from 'ng-mocks';
import { BehaviorSubject } from 'rxjs';
import { PlayerControlsOptions } from '../../core/settings/settings.model';
import { NgxsSelectorMock } from '../../core/testing/ngxs-selector-mock';
Expand All @@ -22,7 +22,10 @@ describe('AppComponent', () => {
let playback: PlaybackService;

let themeProducer: BehaviorSubject<string>;
let customAccentColorProducer: BehaviorSubject<string>;
let showPlayerControlsProducer: BehaviorSubject<PlayerControlsOptions>;
let useDynamicThemeAccentProducer: BehaviorSubject<boolean>;
let dynamicAccentColorProducer: BehaviorSubject<string>;
let inactiveProducer: BehaviorSubject<boolean>;
let spotifyInitSpy: Spy<() => boolean>;

Expand Down Expand Up @@ -54,7 +57,10 @@ describe('AppComponent', () => {
app = fixture.componentInstance;

themeProducer = mockSelectors.defineNgxsSelector<string>(app, 'theme$');
customAccentColorProducer = mockSelectors.defineNgxsSelector<string>(app, 'customAccentColor$');
showPlayerControlsProducer = mockSelectors.defineNgxsSelector<PlayerControlsOptions>(app, 'showPlayerControls$');
useDynamicThemeAccentProducer = mockSelectors.defineNgxsSelector<boolean>(app, 'useDynamicThemeAccent$');
dynamicAccentColorProducer = mockSelectors.defineNgxsSelector<string>(app, 'dynamicAccentColor$');

SpotifyService.initialized = false;
spotifyInitSpy = spyOn(SpotifyService, 'initialize').and.returnValue(true);
Expand Down Expand Up @@ -119,6 +125,105 @@ describe('AppComponent', () => {
expect(main.classes['dark-theme']).toBeFalsy();
});

it('should use dynamic theme when useDynamicThemeAccent and dynamicAccentColor exists', () => {
themeProducer.next('light-theme');
useDynamicThemeAccentProducer.next(true);
dynamicAccentColorProducer.next('cyan');
fixture.detectChanges();
const main = fixture.debugElement.query(By.css('.showtunes-app'));
expect(main.classes['light-theme']).toBeFalsy();
expect(main.classes['cyan-light-theme']).toBeTruthy();
});

it('should use dynamic theme over custom theme when useDynamicThemeAccent and dynamicAccentColor exists', () => {
themeProducer.next('light-theme');
useDynamicThemeAccentProducer.next(true);
dynamicAccentColorProducer.next('cyan');
customAccentColorProducer.next('green');
fixture.detectChanges();
const main = fixture.debugElement.query(By.css('.showtunes-app'));
expect(main.classes['light-theme']).toBeFalsy();
expect(main.classes['cyan-light-theme']).toBeTruthy();
expect(main.classes['green-light-theme']).toBeFalsy();
});

it('should not use dynamic theme when not useDynamicThemeAccent and dynamicAccentColor exists', () => {
themeProducer.next('light-theme');
useDynamicThemeAccentProducer.next(false);
dynamicAccentColorProducer.next('cyan');
fixture.detectChanges();
const main = fixture.debugElement.query(By.css('.showtunes-app'));
expect(main.classes['light-theme']).toBeTruthy();
expect(main.classes['cyan-light-theme']).toBeFalsy();
});

it('should not use dynamic theme when useDynamicThemeAccent and dynamicAccentColor is null', () => {
themeProducer.next('light-theme');
useDynamicThemeAccentProducer.next(true);
dynamicAccentColorProducer.next(null);
fixture.detectChanges();
const main = fixture.debugElement.query(By.css('.showtunes-app'));
expect(main.classes['light-theme']).toBeTruthy();
expect(main.classes['cyan-light-theme']).toBeFalsy();
});

it('should use custom theme when customAccentColor exists and not dynamic', () => {
themeProducer.next('light-theme');
customAccentColorProducer.next('green');
fixture.detectChanges();
const main = fixture.debugElement.query(By.css('.showtunes-app'));
expect(main.classes['light-theme']).toBeFalsy();
expect(main.classes['green-light-theme']).toBeTruthy();
});

it('should use custom theme when customAccentColor exists and not useDynamicAccentTheme and dynamicAccentColor exists', () => {
themeProducer.next('light-theme');
useDynamicThemeAccentProducer.next(false);
dynamicAccentColorProducer.next('cyan');
customAccentColorProducer.next('green');
fixture.detectChanges();
const main = fixture.debugElement.query(By.css('.showtunes-app'));
expect(main.classes['light-theme']).toBeFalsy();
expect(main.classes['cyan-light-theme']).toBeFalsy();
expect(main.classes['green-light-theme']).toBeTruthy();
});

it('should use custom theme when customAccentColor exists and useDynamicAccentTheme and dynamicAccentColor is null', () => {
themeProducer.next('light-theme');
useDynamicThemeAccentProducer.next(true);
dynamicAccentColorProducer.next(null);
customAccentColorProducer.next('green');
fixture.detectChanges();
const main = fixture.debugElement.query(By.css('.showtunes-app'));
expect(main.classes['light-theme']).toBeFalsy();
expect(main.classes['green-light-theme']).toBeTruthy();
});

it('should use no theme when useDynamicAccentTheme and dynamicAccentColor exists and theme is null', () => {
themeProducer.next(null);
useDynamicThemeAccentProducer.next(true);
dynamicAccentColorProducer.next('cyan');
fixture.detectChanges();
const main = fixture.debugElement.query(By.css('.showtunes-app'));
expect(main.classes['light-theme']).toBeFalsy();
expect(main.classes['dark-theme']).toBeFalsy();
expect(main.classes['cyan-light-theme']).toBeFalsy();
expect(main.classes['cyan-dark-theme']).toBeFalsy();
expect(main.classes['cyan-null']).toBeFalsy();
});

it('should use no theme when customAccentColor exists and theme is null', () => {
themeProducer.next(null);
customAccentColorProducer.next('green');
fixture.detectChanges();
const main = fixture.debugElement.query(By.css('.showtunes-app'));
expect(main.classes['light-theme']).toBeFalsy();
expect(main.classes['dark-theme']).toBeFalsy();
expect(main.classes['green-light-theme']).toBeFalsy();
expect(main.classes['green-dark-theme']).toBeFalsy();
expect(main.classes['green-null']).toBeFalsy();
});

it('should show cursor when not fading and not inactive', () => {
const main = fixture.debugElement.query(By.css('.showtunes-app'));
app.fadePlayerControls = false;
Expand Down
3 changes: 3 additions & 0 deletions src/app/components/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export class AppComponent implements OnInit, OnDestroy {
private ngUnsubscribe = new Subject();

@Select(SettingsState.theme) theme$: Observable<string>;
@Select(SettingsState.customAccentColor) customAccentColor$: Observable<string>;
@Select(SettingsState.showPlayerControls) showPlayerControls$: Observable<PlayerControlsOptions>;
@Select(SettingsState.useDynamicThemeAccent) useDynamicThemeAccent$: Observable<boolean>;
@Select(SettingsState.dynamicAccentColor) dynamicAccentColor$: Observable<string>;

fadePlayerControls = false;
fadeCursor = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
.foreground-text {
color: mat.get-color-from-palette($foreground, text);
}

.select-accent-color {
color: mat.get-color-from-palette($foreground, text);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.actions {
justify-content: end;
padding-top: 0;
}

.help-dialog-title {
Expand All @@ -17,3 +18,7 @@
::ng-deep .mat-raised-button.mat-primary {
color: white !important;
}

.footer-content {
padding-top: 24px;
}
Loading

0 comments on commit 13e3fd7

Please sign in to comment.