Skip to content

Commit

Permalink
Merge pull request #56 from zembrodt/develop
Browse files Browse the repository at this point in the history
Merge 0.6.0-dev into main
  • Loading branch information
zembrodt authored Aug 14, 2023
2 parents 2fa9540 + 1b32cc3 commit 3e9a496
Show file tree
Hide file tree
Showing 39 changed files with 2,190 additions and 832 deletions.
7 changes: 6 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: 2.1

orbs:
browser-tools: circleci/browser-tools@1.4.1
browser-tools: circleci/browser-tools@1.4.2
codecov: codecov/codecov@3.2.5

jobs:
Expand All @@ -12,6 +12,11 @@ jobs:
steps:
- browser-tools/install-chrome
- browser-tools/install-chromedriver
- run:
name: "Fix PATH"
command: |
echo "see https://github.com/CircleCI-Public/browser-tools-orb/blob/de5fa4e28909039438189815dbb42ac308e49bc9/src/scripts/install-chromedriver.sh#L187"
echo "export PATH='/usr/local/bin/chromedriver:$PATH'" >> $BASH_ENV
- run:
name: "Check Chrome install"
command: |
Expand Down
18 changes: 1 addition & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,10 @@ Explanation of configurations (**^** denotes required config):
- `"name"` - (*string*) the name of the environment
- **^**`"domain"` - (*string*) the domain this app is running on
- **^**`"spotifyApiUrl"` - (*string*) Spotify's API url
- `"albumColorUrl"` - (*string*) the URL for a server to return dynamic colors (see relevant section below)
- `"auth"`
- **^**`"clientId"` (*string*) the client ID for accessing Spotify's API
- `"clientSecret"` - (*string*) the client secret for accessing Spotify's API (if using non-PKCE method)
- `"scopes"` - (*string*) comma-separated list of API Spotify scopes needed to grant the application access during OAuth
- **^**`"scopes"` - (*string*) space-separated list of API Spotify scopes needed to grant the application access during OAuth
- `"tokenUrl"` - (*string*) the 3rd party backend URL for authentication if not using direct authorization with Spotify
- `"forcePkce"` - (*boolean*) used to force the application to use PKCE for authentication disregarding what other configs are set
- `"showDialog"` - (*boolean*) determines if Spotify's OAuth page is opened in a new window or not
Expand All @@ -87,18 +86,3 @@ These configurations can also be set as environment variables instead of a json
upper camel case. For example: `spotifyApiUrl` as an environment variable will be `SHOWTUNES_SPOTIFY_API_URL`.

Environment variables will always overwrite anything in the config file.

## Dynamic Colors

One feature within this application is the ability to style certain elements dynamically based on the album artwork of the
song currently playing. This functionality is enabled when its required configuration `"env" / "albumColorUrl"` is set.

The application expects the API endpoint to receive a GET request with a `url` param containing the URL to the album artwork.
The endpoint is then expected to send a response object of type `SmartColorResponse`, where `color` contains the hex color value (such as `#ABC123`).

The expected functionality of this endpoint is to analyze the image from the given URL, and send back the most common color to be used as theming within the application.

Current theming options are an exact color matched Spotify code and the selection of a material theme color that matches the dynamic color the closest. The usage of this is
configurable within the application, and these options are disabled if an `albumColorUrl` is not configured.

See the `/color` endpoint within https://github.com/zembrodt/showtunes-api for an example implementation.
3 changes: 0 additions & 3 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ gulp.task('generate-config', () => {
if ('SHOWTUNES_SPOTIFY_API_URL' in process.env) {
configJson.env.spotifyApiUrl = process.env.SHOWTUNES_SPOTIFY_API_URL;
}
if ('SHOWTUNES_ALBUM_COLOR_URL' in process.env) {
configJson.env.albumColorUrl = process.env.SHOWTUNES_ALBUM_COLOR_URL;
}
if ('SHOWTUNES_CLIENT_ID' in process.env) {
configJson.auth.clientId = process.env.SHOWTUNES_CLIENT_ID;
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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.5.2",
"version": "0.6.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
Expand Down
170 changes: 96 additions & 74 deletions src/app/components/album-display/album-display.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* tslint:disable:no-string-literal */

import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { HttpClient } from '@angular/common/http';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, flushMicrotasks, TestBed, waitForAsync } from '@angular/core/testing';
import { FlexLayoutModule } from '@angular/flex-layout';
import { expect } from '@angular/flex-layout/_private-utils/testing';
import { MatProgressBar, MatProgressBarModule } from '@angular/material/progress-bar';
Expand All @@ -10,15 +12,15 @@ import { By } from '@angular/platform-browser';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { NgxsModule, Store } from '@ngxs/store';
import { MockProvider } from 'ng-mocks';
import { BehaviorSubject, of } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { AppConfig } from '../../app.config';
import { DominantColor, DominantColorFinder } from '../../core/dominant-color/dominant-color-finder';
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 { ChangeDynamicColor } from '../../core/settings/settings.actions';
import { NgxsSelectorMock } from '../../core/testing/ngxs-selector-mock';
import { FontColor } from '../../core/util';
import { ImageResponse } from '../../models/image.model';
import { SpotifyService } from '../../services/spotify/spotify.service';

import { AlbumDisplayComponent } from './album-display.component';

const TEST_IMAGE_RESPONSE: ImageResponse = {
Expand Down Expand Up @@ -48,6 +50,17 @@ const TEST_TRACK_MODEL: TrackModel = {
uri: 'track-uri'
};

const TEST_DOMINANT_COLOR: DominantColor = {
hex: 'ABC123',
rgb: {
r: 100,
g: 100,
b: 100,
a: 255
},
foregroundFontColor: FontColor.White
};

describe('AlbumDisplayComponent', () => {
const mockSelectors = new NgxsSelectorMock<AlbumDisplayComponent>();
let component: AlbumDisplayComponent;
Expand All @@ -60,16 +73,18 @@ describe('AlbumDisplayComponent', () => {
let trackProducer: BehaviorSubject<TrackModel>;
let albumProducer: BehaviorSubject<AlbumModel>;
let isIdleProducer: BehaviorSubject<boolean>;
let useSmartCodeColorProducer: BehaviorSubject<boolean>;
let useDynamicCodeColorProducer: BehaviorSubject<boolean>;
let dynamicColorProducer: BehaviorSubject<DominantColor>;
let showSpotifyCodeProducer: BehaviorSubject<boolean>;
let backgroundColorProducer: BehaviorSubject<string>;
let barColorProducer: BehaviorSubject<string>;
let useDynamicThemeAccentProducer: BehaviorSubject<boolean>;

let mockDominantColorFinder: MockDominantColorFinder;

beforeAll(() => {
AppConfig.settings = {
env: {
albumColorUrl: 'test-album-color-url',
spotifyApiUrl: null,
name: null,
domain: null
Expand Down Expand Up @@ -109,12 +124,16 @@ describe('AlbumDisplayComponent', () => {
trackProducer = mockSelectors.defineNgxsSelector<TrackModel>(component, 'track$');
albumProducer = mockSelectors.defineNgxsSelector<AlbumModel>(component, 'album$');
isIdleProducer = mockSelectors.defineNgxsSelector<boolean>(component, 'isIdle$');
useSmartCodeColorProducer = mockSelectors.defineNgxsSelector<boolean>(component, 'useSmartCodeColor$');
useDynamicCodeColorProducer = mockSelectors.defineNgxsSelector<boolean>(component, 'useDynamicCodeColor$');
dynamicColorProducer = mockSelectors.defineNgxsSelector<DominantColor>(component, 'dynamicColor$');
showSpotifyCodeProducer = mockSelectors.defineNgxsSelector<boolean>(component, 'showSpotifyCode$');
backgroundColorProducer = mockSelectors.defineNgxsSelector<string>(component, 'backgroundColor$');
barColorProducer = mockSelectors.defineNgxsSelector<string>(component, 'barColor$');
useDynamicThemeAccentProducer = mockSelectors.defineNgxsSelector<boolean>(component, 'useDynamicThemeAccent$');

mockDominantColorFinder = new MockDominantColorFinder();
component['dominantColorFinder'] = mockDominantColorFinder;

fixture.detectChanges();
}));

Expand Down Expand Up @@ -214,32 +233,71 @@ describe('AlbumDisplayComponent', () => {
expect(loading).toBeFalsy();
});

it('should update Spotify code URL when the track is updated', () => {
component.spotifyCodeUrl = 'test';
it('should set Spotify code URL when the track$ is updated', () => {
component['setSpotifyCodeUrl'] = jasmine.createSpy();
trackProducer.next(TEST_TRACK_MODEL);
fixture.detectChanges();
expect(component.spotifyCodeUrl).not.toEqual('test');
expect(component['setSpotifyCodeUrl']).toHaveBeenCalled();
});

it('should update Spotify code URL when the background color is updated', () => {
component.spotifyCodeUrl = 'test';
it('should update dynamic color when coverArt$ is updated and dominantColorFinder returns a result', fakeAsync(() => {
mockDominantColorFinder.expects(Promise.resolve(TEST_DOMINANT_COLOR));
coverArtProducer.next(TEST_IMAGE_RESPONSE);
flushMicrotasks();
expect(store.dispatch).toHaveBeenCalledWith(new ChangeDynamicColor(TEST_DOMINANT_COLOR));
}));

it('should set dynamic color to null when coverArt$ is updated and dominantColorFinder returns null', fakeAsync(() => {
mockDominantColorFinder.expects(Promise.resolve(null));
coverArtProducer.next(TEST_IMAGE_RESPONSE);
flushMicrotasks();
expect(store.dispatch).toHaveBeenCalledWith(new ChangeDynamicColor(null));
}));

it('should set dynamic color to null when coverArt$ is updated and dominantColorFinder returns an invalid hex', fakeAsync(() => {
mockDominantColorFinder.expects(Promise.resolve({
...TEST_DOMINANT_COLOR,
hex: 'bad-hex'
}));
coverArtProducer.next(TEST_IMAGE_RESPONSE);
flushMicrotasks();
expect(store.dispatch).toHaveBeenCalledWith(new ChangeDynamicColor(null));
}));

it('should set dynamic color to null when coverArt$ is updated and dominantColorFinder rejects its promise', fakeAsync(() => {
mockDominantColorFinder.expects(Promise.reject('test-error'));
spyOn(console, 'error');
coverArtProducer.next(TEST_IMAGE_RESPONSE);
flushMicrotasks();
expect(console.error).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith(new ChangeDynamicColor(null));
}));

it('should set Spotify code URL when the backgroundColor$ is updated', () => {
component['setSpotifyCodeUrl'] = jasmine.createSpy();
backgroundColorProducer.next('bg-color');
fixture.detectChanges();
expect(component.spotifyCodeUrl).not.toEqual('test');
expect(component['setSpotifyCodeUrl']).toHaveBeenCalled();
});

it('should update Spotify code URL when the bar color is updated', () => {
component.spotifyCodeUrl = 'test';
it('should set Spotify code URL when the barColor$ is updated', () => {
component['setSpotifyCodeUrl'] = jasmine.createSpy();
barColorProducer.next('bar-color');
fixture.detectChanges();
expect(component.spotifyCodeUrl).not.toEqual('test');
expect(component['setSpotifyCodeUrl']).toHaveBeenCalled();
});

it('should update Spotify code URL when use smart code color is updated', () => {
component.spotifyCodeUrl = 'test';
useSmartCodeColorProducer.next(true);
it('should set Spotify code URL when useDynamicCodeColor$ is updated', () => {
component['setSpotifyCodeUrl'] = jasmine.createSpy();
useDynamicCodeColorProducer.next(true);
fixture.detectChanges();
expect(component.spotifyCodeUrl).not.toEqual('test');
expect(component['setSpotifyCodeUrl']).toHaveBeenCalled();
});

it('should set Spotify code URL when dynamicColor$ is updated', () => {
component['setSpotifyCodeUrl'] = jasmine.createSpy();
dynamicColorProducer.next(TEST_DOMINANT_COLOR);
expect(component['setSpotifyCodeUrl']).toHaveBeenCalled();
});

it('should create a Spotify code URL', () => {
Expand Down Expand Up @@ -291,73 +349,37 @@ describe('AlbumDisplayComponent', () => {
expect(component.spotifyCodeUrl).toBeNull();
});

it('should create Spotify code URL with smart colors when using smart color code', () => {
component.smartBackgroundColor = 'smart-bg-color';
component.smartBarColor = 'smart-bar-color';
it('should create Spotify code URL with dynamic colors when using dynamic color code', () => {
backgroundColorProducer.next('bg-color');
barColorProducer.next('bar-color');
dynamicColorProducer.next(TEST_DOMINANT_COLOR);
trackProducer.next(TEST_TRACK_MODEL);
useSmartCodeColorProducer.next(true);
useDynamicCodeColorProducer.next(true);
fixture.detectChanges();
expect(component.spotifyCodeUrl).toEqual(
`https://www.spotifycodes.com/downloadCode.php?uri=jpeg%2Fsmart-bg-color%2Fsmart-bar-color%2F512%2F${TEST_TRACK_MODEL.uri}`);
`https://www.spotifycodes.com/downloadCode.php?uri=jpeg%2F${TEST_DOMINANT_COLOR.hex}%2F${TEST_DOMINANT_COLOR.foregroundFontColor}%2F512%2F${TEST_TRACK_MODEL.uri}`);
});

it('should create Spotify code URL without smart colors when not using smart color code', () => {
component.smartBackgroundColor = 'smart-bg-color';
component.smartBarColor = 'smart-bar-color';
it('should create Spotify code URL without dynamic colors when not using dynamic color code', () => {
backgroundColorProducer.next('bg-color');
barColorProducer.next('bar-color');
dynamicColorProducer.next(TEST_DOMINANT_COLOR);
trackProducer.next(TEST_TRACK_MODEL);
useSmartCodeColorProducer.next(false);
useDynamicCodeColorProducer.next(false);
fixture.detectChanges();
expect(component.spotifyCodeUrl).toEqual(
`https://www.spotifycodes.com/downloadCode.php?uri=jpeg%2Fbg-color%2Fbar-color%2F512%2F${TEST_TRACK_MODEL.uri}`);
});
});

it('should set Spotify code smart colors on useSmartCodeColor update', () => {
spotify.getAlbumColor = jasmine.createSpy().and.returnValue(of({ color: '#ABC123' }));
coverArtProducer.next(TEST_IMAGE_RESPONSE);
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();
});
class MockDominantColorFinder extends DominantColorFinder {
private expectedDominantColor: Promise<DominantColor> = Promise.resolve(null);

it('should set Spotify code smart colors to default values on invalid smart album color', () => {
spyOn(console, 'error');
spotify.getAlbumColor = jasmine.createSpy().and.returnValue(of('bad-hex'));
coverArtProducer.next(TEST_IMAGE_RESPONSE);
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);
});
expects(expectedDominantColor: Promise<DominantColor>): void {
this.expectedDominantColor = expectedDominantColor;
}

it('should set smart color when using dynamic accent theme', () => {
spotify.getAlbumColor = jasmine.createSpy().and.returnValue(of({ color: '#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');
});
});
getColor(src: string): Promise<DominantColor> {
return this.expectedDominantColor;
}
}
Loading

0 comments on commit 3e9a496

Please sign in to comment.