New versions of Angular come out every few months. Every once in a while I regenerate Shufflizer to catch up. I want to make sure Shufflizer is not using depricated features. Shufflizer is Angular v17.2 now.
Drag-and-drop
Today there is new version of Shufflizer that offers drag-and-drop for repositioning of songs.
In Android and iOS there is a drag handle. It looks like an equal sign. This is so that dragging and scrolling don’t interfere with each other (dragging is so much like scrolling in these operating systems). In computer desktop operating systems like Windows, simply drag the song using any spot in the display of the song.
Transaction integrity improved
In my previous post, I proposed some enhancements to improve Shufflizer’s transaction integrity. These are done and in production now. They are:
- The count of songs is confirmed on download and upload
- A temporary backup copy of the playlist is uploaded when doing same-playlist upload, and then deleted only if the same-playlist upload succeeds
I just wound up doing more callbacks
Most of the enhancements needed procedural (aka. synchronous) processing. “Don’t do the following step, and don’t accept user input, until the current step finishes ok.” I looked for something in ReactiveX’s JavaScript implementation to do this (RXJS), but did not find one. Perhaps JavaScript’s await would have done it, but I realized that I could:
- make steps run in series using the usual approach – callbacks
- prevent user input with something I already had – the loading spinner
So I proceeded with additional layers of callbacks and moved my command that closes the loading spinner to the bottom layer.
What I mean by a callback is a procedure that is called after the Observable, which is asynchronous, returns a value. In this example deletePlaylist “calls back” to getPlaylists. getPlaylists does not run until deletePlaylist returns a value:
this.spotSvc.deletePlaylist(b_plId,accessToken).subscribe(()=>this.getPlaylists(userId, userCountry, accessToken));
Transaction integrity
We do not have ACID transaction integrity
Shufflizer is built on the Spotify Web API. This API does not provide ACID transaction integrity. If the user’s computer crashes in the middle of an upload, or if the user simply shuts the window in the middle of an upload, the uploaded playlist will be incomplete. If the user has two sessions going (ie. two separate windows on the same device or perhaps on different devices) and is making changes to playlists in each, it is possible that uploading could lose or duplicate songs.
When uploading to a new playlist these concerns are not a big deal, but Shufflizer’s default is to upload to the same playlist. Plus Shufflizer is meant for big playlists. The bigger the playlist the bigger the bummer if it gets ruined. (It is possible to enlist the help of Spotify technical support personnel to restore a playlist to a prior state, but obviously this is an undesirable last resort.)
Here are some things that Shufflizer could do to reduce risk
Playlist initial download:
- After downloading a playlist for edit, confirm that the count of songs reported by Spotify agrees with the count of songs downloaded.
Same-playlist upload:
- first upload as a new playlist, creating a temporary backup copy
- then do the normal upload, rewriting the original playlist
- now confirm that the count of songs reported by Spotify agrees with the count of songs uploaded
- if all is well, delete the new playlist (the temporary backup copy)
With these measures in place, if something goes wrong the user will wind up with two versions of the playlist. Either the original playlist will be good or the backup version will be good, or even both could be good depending on the nature of the outage. It’s unlikely that both will be bad.
Why not just make Shufflizer always upload to a new playlist? Followers and clutter. Same-playlist upload retains a playlist’s followers. Same-playlist upload avoids the clutter of a great many similar playlists that would result from frequent use of Shufflizer.
ReactiveX Observables
I will begin working on these enhancements. I tagged this as a TypeScript and Angular blog entry because I think this will take me a bit deeper into
Toggling beenhere listen indicators
When you click a beenhere listen indicator it clears. If you click again, it reinstates. It’s a toggle.
New shuffle
Shufflizer has new shuffle algorithm. This is the action that occurs when you click a rand button.
The songs are shuffled in up to three sections. If there are any unlistened songs they are shuffled and put first. Then if there are listened songs they are split into two sections, each shuffled separately. One is less-recently listened songs and the other is more-recently listened songs.
If beenhere listen indicators are turned off then all songs are shuffled as unlistened songs.
Quietly
Shufflizer does not communicate this sectioning to the user. The rand button “just works.”
With the old algorithm, if the user was paying attention to the end of the list the user’s expectation that songs should be moving around when clicking rand was not met. Recently listened songs were pinned in a certain order at the end.
Now with the new algorithm more shuffling occurs. Shufflizer still puts recently listened songs at the end, but is less pathological about it.
Tabs, and some groundwork for testing
Shufflizer now sports Angular Material tabs along the bottom edge of the window. This provides a more conventional way to navigate between the app’s pages and also gives me a place to implement an options/features page when I get to working on that.
In addition, under the hood I put all the http calls to Spotify into their own service. This is what the Angular documentation’s HTTP page recommends. It helped me implement better error handling, and better positions the code to take advantage of Jasmine and Karma testing (which I learned how to do at the Angular Denver 2018 conference, and am looking forward to trying out in Shufflizer).
One other thing: immediately upon upload Shufflizer will start playing the playlist in Spotify, if you have a device active, running Spotify.
A more inviting playlist picker
I have been using the Angular Material mat-menu for playlist selection. It’s Material Design’s version of a drop-down select, a classic original GUI prompt concept. There was a problem with long picklists on iPad, maybe due to the modification I did to make the picklist larger – for less scrolling. Also, I wanted a more modern and obvious UI. The user pretty much cannot do anything until picking a playlist. So let’s make picking the playlist a bigger deal up front.
I decided to get rid of the mat-menu and present each playlist as a clickable card. I knew this would be more work, and it was, but it was worth it.
Tabbing
Mat-menu supports the tab key. Once the drop-down appears, the tab key works to move through items. I want the same thing for my cards. So I added tabindex=0 to each card.
A tab key user likely will use the keyboard then to select – either the enter key or the space bar. With tabindex=0 the user can tab to a card, but then neither the enter key nor the space bar work for select. No key does. I solved this by binding to the keyup event, and making my function accept an optional parameter that gives me the keystroke. These lines of code are on each card:
style="margin:10px;cursor:pointer" tabindex=0 (click)="onSelectPlaylist(PI)" (keyup)="onSelectPlaylist(PI,$event.key)"
Then, in my function:
onSelectPlaylist(I: number, key?: string): void { if (key && !key.match(/^( |Enter)$/)) {return} ...
Space bar really does send a single byte blank character through $event.key. The enter key, however, sends the string “Enter”. So my regular expression for space or enter is
/^( |Enter)$/
If the user triggers the function with any key other than space or enter, the function instantly returns, doing nothing.
Toggling
Just as with a classic drop-down select list, we want the cards to disappear when the user makes a selection, and of course reappear if the user wants to make a different selection. I spent a lot of time on this, trying many different user interface ideas. Here are a few things I learned along the way.
Avoid using *ngIf to toggle the existence of Angular Material’s paginator.
I advise this because if the user changes the items-per-page setting, it is lost when *ngIf removes the paginator from the DOM. This may not be a big deal at the time of removal, but be careful if the paginator might be subsequently restored.
Be sure hidden content is not tab selectable
I was using flex size zero to hide sections (eg. fxFlex=”0 1 0″ for the hidden state). I was horrified, however, when eventually I discovered that the user can still tab though, and trigger selection in, this hidden content. It’s much better to show/hide using [fxShow]. If you are unfamiliar with this syntax, it is from the nice flex-layout module that I am using.
Angular 6 Shufflizer
Shufflizer has been updated. There are no new features.
This version of Shufflizer was created with Angular 6. Some obsolete sections of code have been removed. All layouts have been changed from old style html tables to Angular flex-layout (previously it was a mish-mash of these two approaches).
Known issue
Playlist selection does not work on Safari/iPad when the user has enough playlists that the dropdown picklist needs to scroll. This seems to me to be an Angular Material bug. I am considering a more luxury playlist selection interface, and am motivated to make it a priority since it would address this known issue.
Global constants
One thing that was bugging me in my flex-layout performance solution was how I defined the global constants for my three layouts PHONE, TABLET, and HD. As always with constants, I’m just trying to make the code clearer to the reader. There is nothing more to this than PHONE is 1, TABLET is 2, and HD is 3.
We are not able to use the const declaration inside of class definitions.
const sizeNum = {PHONE:1,TABLET:2,HD:3}
Attempting this results in:
error TS1248: A class member cannot have the 'const' keyword.
We could define the constants outside of the class by putting the line of code just above the class, but this isn’t good encapsulation, and code fragments in the html template cannot see the constants. With the constants defined outside of the class, something like this is impossible:
<div *ngIf="activeSize()>sizeNum.PHONE">
Because of these issues, I resorted to a plain old hardcoded object, defined as usual inside of the class:
sizeNum: Object = {PHONE:1,TABLET:2,HD:3}
This doesn’t set up true constants. It is possible to programmatically change the sizeNum object later (eg. sizeNum[‘PHONE’]=5). Also, they are not global, and so I wound up having to pass the sizeNum object as a parameter to other components. Awkward.
There is a better way.
Angular dependancy injection
I am accustomed to using dependancy injection for full fledged classes, but it also can inject simpler objects. This works nicely for global constants. Set them up in app.module.ts:
app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { PlaylstService } from './playlst.service'; ... const sizeNum = {PHONE:1,TABLET:2,HD:3} @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, BrowserAnimationsModule, ... ], providers: [ PlaylstService, { provide:'SizeConstants',useValue:sizeNum } ], bootstrap: [AppComponent] }) export class AppModule { }
Put them into the constructor in desired components:
import { Component, Inject } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; ... import { PlaylstService } from './playlst.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { accessToken: string; // access token for the session. we grep this outta the url ... constructor ( private http: HttpClient, private plistSvc: PlaylstService, @Inject('SizeConstants') public szNum: any ) { // initializations try {this.accessToken=window.location.hash.match(/^#access_token=([^&]+)/)[1];} catch (ERR) {this.accessToken='BAD';} ...
Make use of them, no problem even in html code fragments:
<div *ngIf="activeSize()>szNum.PHONE">