Showing posts with label redux. Show all posts
Showing posts with label redux. Show all posts

Saturday, 20 June 2026

Web Tutorial: Bus Arrival App, Redux

Two years ago, I took you through the creation if this charming little app that could show you when your bus was arriving. And I took it on a rigorous test run that lasted that duration. In the process, I discovered what worked, and what needed work. Today I am going to be taking you through some upgrades I have done.

Don't look so shocked. Software is not dead. It evolves. Especially useful software.

Some problems I fixed...
- added visuals for bus types and capacity
- simplified data flow
- color scheme

Let's get to work! We will first streamline the data flow.

The first thing we'll want to do is remove the JavaScript. This upgrade removes all JavaScript in lieu of UI simplicity. To that end, this entire section needs to go.
<script>
function showArrivalFor($bus)
{
    var hide = document.getElementsByClassName("arrival");

    for (var i = 0; i < hide.length; i++)
    {
        hide[i].style.display = "none";
    }

    var show = document.getElementById("arrival_" + $bus);
    show.style.display = "block";
}
</script>


Remove this entire div. We want everything to be visible when the data is fetched. No more strategically hiding some of it. This means there are less steps for the user to go through, reducing interface friction.
<div id="bus" style="display:<?php echo (count($buses) == 0 ? "none" : "block");?>">
    <h1>&#128655; BUS SERVICES</h1>
    <?php
    foreach($buses as $bus)
    {
    ?>
        <button onclick="showArrivalFor('<?php echo $bus->ServiceNo; ?>');">
        <?php
            echo $bus->ServiceNo;
        ?>
        </button>    
    <?php    
     }
     ?>
</div>

<br />


Remove this too. We won't need it any more.
#bus button
{
    background-color: rgb(50, 0, 0);
    color: rgb(255, 255, 255);
    border-radius: 5px;
    border: 3px solid rgba(0, 0, 0, 0.5);
    padding: 5px;
    width: 5em;
    font-size: 20px;
    font-weight: bold;
}


In this section, the div should still be styled using the arrival CSS class, but the style that hides it, should be removed, as well as the id attribute.
<?php
    foreach($buses as $bus)
    {
?>
    <div class="arrival">
        <h1> BUS <?php echo $bus->ServiceNo; ?> ARRIVAL TIMINGS</h1>
        <?php
            if ($bus->NextBus)
            {
                 echo "<h2>" . formatArrivalTime($bus->NextBus->EstimatedArrival) . "</h2>";
            }

            if ($bus->NextBus2)
            {
                echo "<h2>" . formatArrivalTime($bus->NextBus2->EstimatedArrival) . "</h2>";
            }

            if ($bus->NextBus3)
            {
                echo "<h2>" . formatArrivalTime($bus->NextBus3->EstimatedArrival) . "</h2>";
            }
        ?>
    </div>
<?php      
    }
?>  


So far so good....


Next, the Color Scheme

Now here's a bit of styling. I enlarged the front to almost double what it was previously, changed the background color to orange, and gave container a white translucent border.
body
{
    background-color: rgb(200, 150, 0);
    font-family: sans-serif;
    font-size: 25px;
}

#container
{
    border-radius: 20px;
    border: 3px solid rgba(255, 255, 255, 0.5);
    padding: 2em;
}


I also removed some of these properties. They're no longer necessary.
#container div
{
    /* border-radius: 20px; */
    /* border: 3px solid rgba(100, 0, 0, 0.2); */
    padding: 0.5em;
    color: rgb(0, 0, 0);
}


That wasn't too hard!


More changes. A couple things are happening here. I remove borders from the rendering of the textbox and button for a clean flat look. I also change the button and hover color.
#stop input
{
    border-radius: 5px;
    border: 0px solid rgba(0, 0, 0, 0);
    padding: 5px;
    width: 10em;
    height: 1em;
}

#stop button
{
    background-color: rgb(255, 200, 0);
    color: rgb(255, 255, 255);
    border-radius: 5px;
    border: 0px solid rgba(0, 0, 0, 0);
    padding: 5px;
    width: 10em;
}

#stop button:hover
{
    background-color: rgb(150, 50, 0);
}


While we're at it, let's declutter by removing these icons.
<!--<h1>&#128655; BUS STOP <?php echo $busStop;?></h1>-->
<h1>BUS STOP <?php echo $busStop;?></h1>


<!--<h1>&#128652; BUS <?php echo $bus->ServiceNo; ?> ARRIVAL TIMINGS</h1>-->
<h1>BUS <?php echo $bus->ServiceNo; ?> ARRIVAL TIMINGS</h1>


Looks cleaner now, doesn't it?


In fact, let's clean this up even more.
<!--<h1>BUS <?php echo $bus->ServiceNo; ?> ARRIVAL TIMINGS</h1>-->
<h1><?php echo $bus->ServiceNo; ?></h1>


In the CSS, we provision styling for all h1 tags in arrival. The important thing is that it's floated left. The rest is just aesthetics. I'm going for white text on a yellow background, with rounded corners.
#stop button:hover
{
    background-color: rgb(150, 50, 0);
}

.arrival h1
{
    background-color: rgb(255, 200, 0);
    color: rgb(255, 255, 255);
    border-radius: 5px;
    border: 0px solid rgba(0, 0, 0, 0.5);
    padding: 5px;
    width: 4em;
    height: 40px;
    font-size: 30px;
    font-weight: bold;
    float: left;
    text-align: center;
}

</style>


So now that long-ass title has been reduced to a number.


Now provision some styling for h2 tags. We want them to be a more rounded white block, and appear to the right of the bus number. Thus, floating left is a must.
.arrival h1
{
    background-color: rgb(255, 200, 0);
    color: rgb(255, 255, 255);
    border-radius: 5px;
    border: 0px solid rgba(0, 0, 0, 0.5);
    padding: 5px;
    width: 4em;
    height: 40px;
    font-size: 30px;
    font-weight: bold;
    float: left;
    text-align: center;
}

.arrival h2
{
    background-color: rgb(255, 255, 255);
    border-radius: 15px;
    border: 0px solid rgba(0, 0, 0, 0.5);
    padding: 5px;
    width: 7em;
    height: 40px;
    font-size: 25px;
    font-weight: bold;
    float: left;
    text-align: center;
    margin-left: 0.5em;
}

</style>


But oops, this is a mess.


Add this line here to make sure floats are cleared.
    <?php
        if ($bus->NextBus)
        {
            echo "<h2>" . formatArrivalTime($bus->NextBus->EstimatedArrival) . "</h2>";
        }

        if ($bus->NextBus2)
        {
            echo "<h2>" . formatArrivalTime($bus->NextBus2->EstimatedArrival) . "</h2>";
        }

        if ($bus->NextBus3)
        {
            echo "<h2>" . formatArrivalTime($bus->NextBus3->EstimatedArrival) . "</h2>";
        }
    ?>
</div>
<br style="clear: both" />


All better now.


Presenting additional data

The final part is here. I want to show bus type and capacity, which is data already present in the API response. I simply did not make use of it the last time. Time to address that oversight!

First, let's relocate the logic to a function, so that the heavy lifting gets concentrated in one place. We'll create busArrivalDisplay() shortly, and retain formatArrivalTime().
<?php
/*
    if ($bus->NextBus)
    {
        echo "<h2>" . formatArrivalTime($bus->NextBus->EstimatedArrival) . "</h2>";
    }

    if ($bus->NextBus2)
    {
        echo "<h2>" . formatArrivalTime($bus->NextBus2->EstimatedArrival) . "</h2>";
    }

    if ($bus->NextBus3)
    {
        echo "<h2>" . formatArrivalTime($bus->NextBus3->EstimatedArrival) . "</h2>";
    }
*/

    if ($bus->NextBus)
    {
        echo busArrivalDisplay($bus->NextBus);
    }

    if ($bus->NextBus2)
    {
        echo busArrivalDisplay($bus->NextBus2);
    }

    if ($bus->NextBus3)
    {
        echo busArrivalDisplay($bus->NextBus);
    }
?>


In here, we modify formatArrivalTime() slightly to replace "T". It may or may not come up, but why take the chance, eh? Then create busArrivalDisplay(), with obj as a parameter. obj will contain all the information you need. The classes here are based on the Load property in the returned response.
function formatArrivalTime($strTime)
{
    $newStr = str_replace("+08:00", "", $strTime);
    $newStr = str_replace("T", " ", $newStr);
    return date("h:i A", strtotime($newStr));
}

function busArrivalDisplay($obj)
{
    $html = "<h2 class='capacity_" . $obj->Load . "'>";
    $html .= formatArrivalTime($obj->EstimatedArrival);
    $html .= "</h2>";

    return $html;
}


We will make use of the various possible values of capacity. In the CSS, we define different colors for capacity. "SEA" means that there are seats, so the color is green. "SDA" means that there's standing space, so we use yellow. "LSD" means that there's limited standing space. The bus is almost full. So use deep red for this.
.arrival h2
{
    background-color: rgb(255, 255, 255);
    border-radius: 15px;
    border: 0px solid rgba(0, 0, 0, 0.5);
    padding: 5px;
    width: 7em;
    height: 40px;
    font-size: 25px;
    font-weight: bold;
    float: left;
    text-align: center;
    margin-left: 0.5em;
}

.capacity_SEA
{
    color: rgb(0, 200, 0);
}  

.capacity_SDA
{
    color: rgb(200, 200, 0);
}    

.capacity_LSD
{
    color: rgb(200, 0, 0);
}
</style>


So now we have differently-colored times.


Now for bus types. Basically, I only care about the difference between single and double decker buses. Therefore, all other bus types will just use the same image as the single decker bus.

I used some stock images for this. I actually have only two images. The others are all duplicates with different names.

(img)
icon_.png
icon_BD.png
icon SD.png



icon_DD.png


Then we add this line. This adds a transparent PNG, according to the bus type, to the information.
function busArrivalDisplay($obj)
{
    $html = "<h2 class='capacity_" . $obj->Load . "'>";
    $html .= "<img height='30' src='icon_" . $obj->Type . ".png' /> ";
    $html .= formatArrivalTime($obj->EstimatedArrival);
    $html .= "</h2>";

    return $html;
}


Beautiful!

Enjoy this version!

I really think it's more user-friendly, especially on mobile. Before this, I tested it on desktop, but it doesn't really make sense to do that because, well, if you're trying to look up bus arrival data outdoors, why would you be using a laptop? Yeah I know, I dropped the ball. It's on me. Hopefully this makes up for it!

Stay bus-y,
T___T

Friday, 12 June 2026

Film Review; Black Mirror Season Seven, Redux (Part 3/3)

Next we have the sequel to Black Mirror Series Four's USS Callister, USS Callister: Into Infinity!

Anyone who hasn't watched USS Callister is encouraged to go have a gander - because it's great and because this sequel will make a hell of a lot more sense after that.

The Premise

Following the events of USS Callister, Nanette and the crew of USS Callister roam the universe of the Infinity game, but find life hard in this virtual universe, resorting to robbing players for credits for their continued survival. Their antics catch the attention of the original Nanette and Walton, who venture into Infinity to investigate.

The Characters

Cristin Milloti as Nanette Cole. Milloti portrays original Nanette as excitable and adorably clumsy, and lacking in confidence. Unlike clone Nanette, who has experienced loss and grown into her role of leadership. Milotti brings the acting chops she displayed in The Penguin, over to this episode, quite handily. Did I also mention that she's remarkable easy on the eyes either way?

Jimi Simpson as James Walton. In his own words, "Captalist Asshole". Seriously, Walton is an even bigger dickbag the second time around, and Simpson's acting is off the charts as he now shows different sides of the same character by playing original Walton and clone Walton. Original Walton is selfish, narcissistic and opportunistic. He doesn't care about anyone but himself (and his son, a brief but noteworthy few seconds in the episode) and his ignorance of his own company's products and staff, is a running joke and even a plot point later on. Clone Walton, on the other hand, has matured into someone who thinks of others and is deeply regretful of his past actions.

Jesse Plemons as clone Robert Daly. In Breaking Bad, Plemons acted as the unstable psycho Todd Alquist, and he carries over shades of that same performance as socially awkward nerd Daly. This time round, the performance is even more nuanced - we see an early incarnation of Daly who is still awkward and shy, but nonetheless shows Nanette (and us) why someone like him ultimately can't be trusted with power.

Osy Ikhile as coffee intern Nate Packer. Packer undergoes quite the transformation from how he was in the original USS Callister. Here, he's a grim warrior who ends up being the captain once Nanette is gone. From an intern, to a leader. I like it.

Milanka Brooks as receptionist Elena Tulaska. Her character doesn't change much, but Brooks takes the opportunity to show that while original Elena is cold and snarky, clone (and foxy blue-skinned!) Elena has a heart.

Paul G Raymond as programmer Kabir Dudani. He has the least character development in the original and the sequel. Original and clone Dudani are both computer nerds with no real severe character tics and are just unlucky. They stay pretty much constant through the episodes.

Billy Magnussen as Karl Plowman. His arc is short but significant. Offline, he's a jovial gym bro and in the game, the trauma of USS Callister has turned him into a restless manchild. He does sacrifice himself heroically to save the others, though how much of it is just poor impulse control, is up to interpretation.

Bilal Hasna has a brief appearances as Kris El Masry, reporter. Sharp as a tack. Asks hard questions, and keeps Walton on the back foot. This guy has "mild-mannered reporter" written all over his face, but when he hits hard, he hits. Hard.


Iolanthe is hilarious as Pixie Bunkin and her hot pink equipment. That character brought so much energy in.

The Mood

A colorful space adventure, complete with spaceships and laser battles and gunfights on alien planets.

What I liked

Multiple character arcs. The original had the cast play different versions of the same character. This time round, it's even more pronounced because we've had time to get used to the characters and we see how the originals are like versus how their clones have evolved into different people. Trauma really shapes character.


The visual of the space battle is epic.

The joint cameos of Nisha and Grawp. I don't like the fact that Demon 99 exists as an episode, but I couldn't help but feel intense glee when they appeared. These two are so watchable!


That gag with Rocky, the hole in its back, and Walton's relationship with it. It's vulgar as shit, and I'm all here for it!


I don't know about anyone else, but this big pink gun is some seriously cool shit.

What I didn't

The ending was just a bit too similar to the premise of Black Museum. Just felt like a huge letdown.

Michaela Coel is missing from the cast as Shania Lowery. I get it; it's not exactly the showrunners' fault that she didn't reprise the role. It could be for any given reason, and they made the effort to explain her absence as having been killed in action. It actually added to the narrative tension because this tells the audience that the crew can die now. What I have an issue with is that at the end of USS Callister, Nate Packer and her looked like they were going to be in some kind of relationship, and now in the sequel, I would have expected the character to be way more upset than Nanette at Shania's death.

Conclusion

Did USS Callister really need a sequel? It's up for debate. I thought the original left it in a good spot. This sequel is good fun, I'll grant you, but was it necessary?

My Rating

8 / 10

Final Thoughts on Black Mirror Series Seven

It's been a long time coming, but Black Mirror has finally gotten its act together and is back at its best. We're seeing six badass episodes, each hitting us in emotional places. This installment started out strong and somehow got even stronger at the end. Plaything and Eulogy were outstanding in particular, with the others being more than decent. The silliest episode in my opinion was Bete Noire, and even that was a worthy addition to the Black Mirror collective.

It's good. It's really good. Whatever sins Black Mirror may have committed in the past few iterations, all is forgiven. It's a return to form, and long overdue.

Spacing out,
T___T

Tuesday, 9 June 2026

Film Review: Black Mirror Series Seven, Redux (Part 2/3)

The next episode is Eulogy, and it's a bit of a mood whiplash from the preceding episode, but just as good, if not better.

The Premise

Philip receives a call that his old flame Carol has passed away. In order to collect recollections for her memorial, Philip goes through a program by Eulogy designed to read his memories, unearthing some interesting ones...


The Characters

Paul Giamatti absolutely knocks it out of the park in the role of Philip Connarty. Philip is a surly, bitter, cranky old guy. Giamatti carried this episode by serving up an entire buffet of emotional delivery. He also serves as narrator for most of the recollections. He gives us a moving portrayal of a deeply flawed man who loved passionately, experienced guilt, lashed out in anger, and finds closure in forgiveness. Giamatti was immediately recognizable to me as the antagonist in Shoot Em Up. Upon watching this episode, I gained a newfound respect for this man's range.

Patsy Ferran as Kelly Royce, Carol's daughter and the AI that serves Eulogy. She acted as the foil to Philip's general grumpiness, appearing upbeat and chirpy most of the time, occasionally delivering quite the witty barb. The meat of this episode belonged to Giamatti, but without Ferran to play off against, it wouldn't have worked as well.

There were plenty of others in the cast, but they either had no speaking parts, extremely limited screen time or merely appeared as still images... or all at the same time.

The Mood

It begins with a scene of an elderly man trimming rose bushes in some quiet countryside or other. A very placid scene. And the pacing stays slow and steady even as more upsetting memories and twists are revealed. There's a pervading sense of sorrow, old anger and sentimentality, though at no point does the viewer get the sense that anyone is in any particular danger. It's not that kind of Black Mirror episode, and yet it absolutely works.

What I liked

It's a very small cast. Only two actors have any sort of work to do - the rest either appear in non-speaking and/or non-moving roles. Thus, it's up to Paul Giamatti and Patsy Ferran to hold up this episode all by themselves. And these two, quite amazingly, get it done.

Black Mirror is famously full of violence, vulgarity and downer endings. On the rare occasion when things get sentimental and sweet, like now, even admidst the sorrow permeating the episode, it really hits home.



The visuals are amazing. They give us a pretty darn good idea of what it's like to virtually dive into a still photograph, and have AI render everything in 3D. The side effects of Philip using sharpies and knives to erase Carol's face from photographs, are disturbingly rendered as well. It's glorious... with just a hint of spooky.

What I didn't

DHL sending the package by drone was a detail I could have done without. That was so out of left field. I don't actually think drone delivery is too far-fetched, though having it take place in such a casual manner, probably is. There are regulatory issues to think about, especially since the company Eulogy is in the UK and Philip is in the USA or something. And it wasn't even really relevant to the story.


Still, it's a very minor detail in what was otherwise an excellent episode.

Conclusion

This episode was genius. It had the Black Mirror DNA - fantastical tech being used in novel ways, flawed characters acting out and inevitable human tragedy. Though, in this case, the tech brings about a somewhat happy ending. This might actually be my favorite episode in an above-average season of Black Mirror.

My Rating

9.5 / 10

Next

USS Callister: Into Infinity

Monday, 19 January 2026

Making the contact tracing effort mandatory, redux

Back in 2021, I wrote about the mobile app TraceTogether and its use during the COVID-19 pandemic. My rhetoric back then, in hindsight, was fiery; but I stand by what I said.

I had thought that this issue was behind us. However, last couple weeks, four entire years after I last mentioned it, the issue arose again in Parliament. A bill was passed - the Public Sector (Governance) Act (Amendment) Bill, which allows for sharing of citizens' data with bodies other than public agencies.

TraceTogether

During the debate, Opposition MP Kenneth Tiong brought up the fact that in 2021, Singaporeans discovered that data collected by TraceTogether could legally be shared with the police for the purposes of criminal investigation, and this had resulted in damage in terms of public trust. 

Minister of State for Digital Development and Information Jasmin Lau refuted this, and predictably, this led to a flurry of activity on my Facebook feed. Again, predictably, supports of the current ruling party, the PAP, stoutly re-iterated their trust in the Singapore Government, while supporters of the Opposition did the exact opposite.

Mostly incoherent sycophantic babbling on both sides, if I'm being honest. These days it feels like the most useless political opinions in the world can be found on Social Media. These political fanboys are nuts, and not even in a cool way.

My assertion back then

I was of the opinion that lives were at stake, and certain things such as privacy, had to take a back seat. I honestly do not consider there to be anything controversial about this statement, and would go so far to say that anyone who would knowingly endanger the lives of the public for the sake of their own privacy, is a selfish muppet.

Bring tracked.

Not that I suspect the Singapore Government to be the type to seize any opportunity to obsessively track its citizens like some kind of overbearing control freak Asian parent. However, the potential for abuse existed, no matter how small. The probability was a non-zero value. But it's no exaggeration when I say, if the Singapore Government's efforts to control the spread of COVID-19 led to them being able to track my whereabouts for the rest of my life, it was a tradeoff I would have taken. Happily.

However...

Certain quarters started spouting some ignorant claptrap in support of the ruling party. They claimed that anyone who objected to having their data tracked back then, had something to hide. Anyone whose conscience was clear surely would not object to having their data tracked for the greater good.

Oh FFS, children.

It's a principle of privacy. Yes, I'm sure there were those who had plenty to hide, but generally, people were objecting out of principle and not because they were planning to commit acts of moral indecency. Anyone who can't see that, either isn't familiar with the concept of principles, or has none to begin with.

Form-filling.

I had to deal with this principle several years ago when I bought an insurance policy. An insurance agent came to my place and proceeded to have me fill out a form as a matter of routine. Among the questions in the form were gems such as "have you contracted HIV or any STDs in the past year?" and the agent actually asked me this question out loud. In front of my mother.

Wow. This bloody idiot, I swear to God.

The answer was "no", but even if the answer had been "yes", did she really think it was something I would have been comfortable sharing in front of my own goddamn mother? It's the principle of the thing. Whatever happened to professional discretion? Did she somehow think that no one should be allowed to keep personal secrets from their own mother?

So yes... personal privacy as a principle is very real.

Comparisons

It's no comparison, of course. While the principle is the same, the stakes are vastly different. In the case of COVID-19, it was a global pandemic where public health and safety were at stake. In the case of that shockingly indiscreet insurance agent, it was merely a case of bureaucracy. Yes, I'm aware that it could be a lot bigger than that, but it still isn't on the level of public health and safety.

While I value my personal privacy, I don't value it over the lives of others. I certainly do not value anyone's personal privacy over the lives of others, and am mystified at the possibility that anyone could feel otherwise.

It's 2026. This really should be the final time I speak on this.

Not-so-privately,
T___T

Tuesday, 30 September 2025

Film Review: Black Mirror Series Six, Redux (Part 2/2)

This next episode, Demon 79, leans into the whole supernatural path that Black Mirror seems to be veering into, and then gleefully goes full speed ahead.

The Premise

Shop assistant Niqa discovers a bone talisman and enters a pact with a demon, Gaap, in which she has to deliver three human sacrifices. Failure to do so will bring about the end of the world. It's pretty simple, and really revolves around her attacks of conscience.

The Characters

Anjana Vasan takes on the role of Niqa Huq, and really makes a meal of it here. Niqa has a dreary soul-wearing job and Vasan really makes you feel it. When she gets driven to murder later on, I could totally believe it.

Paapa Essiedu is Gaap. He's quirky and goofy, and so much fun on screen. Even more fun is his off-handed attitude towards murder, morality and all that jazz. Endearingly awkward demeanor aside, there are moments of vulnerability where Gaap confesses his insecurities to Niqa, and they bond.

Nicholas Burns as Keith Holligan. We first see Holligan as a balding loser who also happens to be a psychopathic murderer, but as the episode goes on, it's apparent that he's a tragically lonely man who also happens to be deeply disturbed. Burns made me feel -sorry- for this guy, dammit. Right up to the point he got his skull smashed in!

Shaun Dooley as Len Fisher, the police detective who investigates the murders. Comes off as jaded, cynical and couldn't be arsed. But when presented with a proper mystery, he's all kinds of shrewd and professional. Dooley let the human side show during that confrontation with Niqa, when Niqa asks him if he's a good man, and he says "I hope so, love".

Nick Shields as politician Michael Smart. He's played as charming and intelligent, and a good orator, with the catchphrase "So don't just pray for a good future, vote for one!" That scene where he just about snake-charms Vicky into voting for him, was fantastic. I could have done with more of him in this episode, he was brilliant as an antagonist. Totally brought that dangerous energy to his game.

Katherine Rose Morley is Vicky the salesgirl. Morley plays her with acid-tongued bitchiness and absolutely no redeeming qualities, so it's easy to root for Niqa to make her the next victim.

Emily Fairn as Suzie, Fisher's assistant. Mostly got distracted by her nose, I'm sorry to say.

Nick Holder as Posset's manager, Mr Duncan. Wow, what an ass. Holder plays him as a doofus who skirts around being overtly racist but ultimately lets his true colors slip through in the presence of Michael Smart. That was great, because it kept me guessing who was going to be the next sacrifice, and hoping it might be him.

Joshua James as Chris Holligan. He probably wasn't meant to be a comedic character, but that awkward fight scene with Niqa was hysterical.

Joe Evans as Tim Simons, a creep who molests his young daughter. There's nothing overtly detestable or likeable about the guy, otherwise. The character doesn't spend much time on screen, so it's up to Gaap to tell us his sins.

Hayley Considine as Jean Simons. She's played as an oblivous wife and mother.

Lillie Mae Law as Laura Simons. She's quiet and sullen, and looks like a seriously disturbed child, possibly from being molested repeatedly by dear old dad.

Steve Garti as Bob the bartender. While the character isn't exactly played like an out-an-out racist, he's more like the dismissive "they all look the same to me" guy. Kind of like me, if I'm being honest.

Vickie Binnis plays Julie the barmaid, but this seems like a thankless role if all she really does is provide some exposition.

The Mood

The atmosphere is dreary, a cacophany of dirty rooms, dusty streets and hazy skies. We're made to sit through the drudgery of Niqa's life, and things don't improve much after meeting Gaap.

All in all, it has a very low-budget horror movie vibe, along with jarring sound effects, which I totally enjoyed.

What I liked

Vibe between Niqa and Gaap. It's heartwarming, that's what it is. Niqa has multiple breakdowns, and Gaap is supportive in his ham-fisted way.

Reappearance of the symbol from White Bear and Bandersnatch. I just about screamed when Nida found this talisman. This is rapidly turning out to be another Black Mirror staple.


Niqa's flights of fancy where she imagines herself violently killing annoying co-workers and customers, were tremendous fun to watch.

The subplot of National Front and Niqa being a potential target, really added to the narrative tension.


The entire subplot of Keith Hooligan's death, from his ham-fisted attempts to seduce Niqa, to his resigned acceptance of his fate, was just so yikes. Loved it!

We get to see one of those metal dogs again! Though this time it's in the form of a flash-forward and it's so brief, you blink and you miss it.


The car chase which ended in Niqa taking the hammer to Smart and ultimately getting caught by Fisher, was a thrilling plot point for me. This was so well done, on multiple levels. The music, the night lighting, the acting... good shit.

I loved the ending, where Niqa and Gaap actually choose to hang out with each other for eternity. It's so sweet, honestly.

And even that final conflagration. It's a downer ending sure, but done so gracefully.


Generally, the writing, and the dialogue. It feels like a lot of love and care was put into characterization.

What I didn't

This episode was all about the supernatural. If I'm tuning in to Black Mirror, I wanna see a Black Mirror episode, dammit. That means computers, phones and shit.

Are we supposed to believe only a few minutes passed between braining Keith Hollligan and then that entire fight sequence between Niqa and Chris?

Conclusion

Demon 79 had so much going for it - engaging characters, nice story, rich visuals. Unfortunately, it just didn't fit into the Black Mirror universe. Where's the tech angle? Where's the media angle? Nada. Zilch. Not that I didn't enjoy it, mind you. Just based on its own merits, Demon 79 was intensely watchable.

My Rating

8 / 10

Final Thoughts on Black Mirror Series Six

Series Six is a big fat disappointment, and that's me being charitable. Too many episodes don't fit the mold of a Black Mirror offering, and that hurts the entire series as a whole. Which really is a pity considering standouts in this series such as Joan Is Awful and Beyond the Sea. It feels like the showrunners are just going through the motions at this point, and running out of ideas for the Black Mirror concept. Demon 79, for example, seems like a nice piece of work but with very little to mark it as a Black Mirror episode.

Look in the mirror, Series Six!
T___T

Sunday, 28 September 2025

Film Review: Black Mirror Series Six, Redux (Part 1/2)

It's time to resume this review of Black Mirror Series Six. And while I've been largely complimentary of this installment so far, things are about to get significantly less positive.

The fourth episode is Mazey Day, and really, it's just the name of one of the characters. Given the lack of creativity shown thus far with regard to episode titles, I guess I shouldn't be too surprised, much less disappointed.

The Premise

The story is set in Los Angeles, USA, and centers around a freelance photographer. She and some others violate the privacy of a celebrity, Mazey Day, only to find a nasty surprise waiting for them.

The Characters

Zazie Beetz is her bubbly engaging self as Bo, a photographer who has an attack of conscience. I've always found her immensely watchable in films like Deadpool 2, Joker and Bullet Train. This time, she gets to show off some acting chops by portraying Bo as someone who's desperate for work, but not willing to compromise her ethics all the way. She's also probably the most obvious shutterbug around, having been caught twice in this episode while trying to be sneaky. I found myself wondering how this character survives this profession!

Clara Rugaard as Mazey Day, an actress who later on becomes a werewolf. The portrayal was kind of bland, to be honest. Just not very compelling, though perhaps the blame can be laid at the feet of whoever wrote the script.

Danny Ramirez as Hector. Not really sure what the character's function was here. Just another warm body for the werewolf to savage?

Robbie Tann as Whitty, a sociopathic jerk who likes to run his mouth. Some of the stuff he says is cold and unnecessarily cruel but contains some uncomfortable truths.

James P. Rees as Duke, a sleazy shutterbug who tries to take upskirt pics of Sydney Alberti. Rees plays him as a mouthy dirtbag, and no tears are shed when the character eventually gets eaten.

Jack Bandeira has a dual-purpose character, Terry the talkative bartender. He's chatty (and blond, and blue-eyed, astonishingly good-looking, really) and provided Bo with a lot of plot-pertinent information. Later on, he also functions as a casualty in the diner, accidentally shot dead by the lawman, no less.

Kenneth Collard as Dr Dmitri Babich, a celeb doctor who's into alternative medicine. Didn't do much with a largely expository role.

Corey Johnson as Clay the police officer who is, quite amusingly, really into eating chicken. I do like it when characters ramble on about stuff that doesn't necessarily tie in to the plot. It feels relatable, somehow.

David Rysdahl is Bo's housemate Nathan. He's played as passive-aggressive and annoys the hell out of me. Which I suppose is pretty effective acting because it's a major plot point that Bo wants to pay Nathan her late share of the rent.

Charles Hagerty plays Justin Camley in an extremely short appearance, as a TV actor who gets caught having a tryst with a gay partner. He appeared all of a few seconds, but I thought the actor was worth mentioning because he made the character's desperation and frustration really shine through.

Patrick Toomey is Nick, the one who pays these shutterbugs to take incriminating photographs for his scandal reporting. Toomey plays this limited role with the appropriate amount of smarminess.

Lucía Pemán also makes a short appearance as actress Sydney Alberti, who has a sex tape leaked. The purpose of this character is pretty much just to have Whitty and Duke show off what douchebags they are.

The Mood

It's a dusty atmosphere in the sunlight, but soon switches to a dim, dark color palette as the story begins taking place in the night. And soon enough, it turns into a high-stakes game of cat-and-mouse with a ravaging beast. Basic monster movie fare, really.

Later on, it's a drawn-out tragedy when Bo gives Mazey a gun to kill herself with.

What I liked

I groaned and cheered in equal measure when Whitty met his grisly end after refusing to escape while he could, and continue to take pictures of a still-transforming Mazey. The trope of passion for his craft outweighing common sense was strong here, but also because the character was such a jerk, watching him get wrecked was cathartic.


Whitty and Duke finding out about Cedarwood Spa Retreat by placing a tracker on Hector's bike. This is so character-appropriate!

The unnamed actor who players the security detail that slashes Bo's tyres is so suitably menacing and nonchalant at the same time. I heartily approve.


The episode ends with Bo taking a photograph after giving Mazey the means to off herself. It's what got her into this mess in the first place, and this is darkly poetic.

What I didn't

The showrunners might not have meant to draw attention to this tattoo under Bo's navel, but draw attention they did and now I want to know why. It's never addressed. I'm a Chinese man and I know the character for "snake" when I see it. Question is, what significance does this have? Or was this to tell us that Bo, like too many non-Chinese educated people, have an unfortunate habit of inking words on their skin in languages they can't even read?


Bo finds out where Mazey is staying by accidentally running into a food delivery worker who just happened to deliver to that address. Seems a bit convenient, no? And Bo didn't even have to pay for the information, that's the best part.

It doesn't make sense that Bo wouldn't just take the 500 that Justin offered, for her photos. It's not the first time she's working for that cheap bastard Nick, so she has to know he's going to low-ball her.

Speaking of things that don't make sense, how does Cedarwood have this big-ass fence that can be defeated by digging under the fence, just like that? The soil is even conveniently loose!


This was not a tech episode, though a case could be made for it being centered around media. Still, this detracts from Black Mirror being less science fiction and more supernatural horror. I can't say I approve, really. Black Mirror has been around long enough to have its own identity. It's not like Black Mirror is in the stages of infancy, still trying to figure out what it is.

Conclusion

This episode had decent scares, a decent plot and an OK ending. It, however, doesn't really seem to qualify as a Black Mirror episode due to the supernatural elements involved.

My Rating

6 / 10

Next

Demon 79

Sunday, 1 June 2025

Rise of the Bike-sharing Phenomenon, Redux

Almost exactly seven years ago, I wrote about bike-sharing apps and how they were making their presence felt in Singapore.

Those were some turbulent times, in the context of the bike-sharing industry, at least. Users flagrantly mistreated the bikes they rented. Due to problems arising from abuse of bicycle privileges and irresponsible parking, the Land Transport Authority (LTA) eventually mandated that bike-sharing companies would have to implement a QR code-based geo-fencing for designated parking, and gave existing bike-sharing companies a grace period in which to do so.

Cyclists in Singapore.

By the end of 2019, the majority of the companies I was talking about back then, have either been absorbed by bigger companies or left Singapore altogether due to challenges both technical and financial.

New players

As of 2025, two main companies remain. Anywheel was founded in 2017, and was a relatively small player at the time. It was only in 2019, when the dust cleared, that it began to establish itself as a major player. HelloRide, in contrast, was founded just a few years back in 2022 and only as recently as last year, received a full license to operate in Singapore

Having downloaded the Ofo app in the past, I gave the Anywheel app a go. The difference wasn't huge. Like the Ofo app, it basically included a feature to scan a QR code on the bike to unlock it, a way to relock and park the bike, and a payment interface.

Designated parking spaces.

The only real difference was the parking feature, and all it really involved was scanning a QR code at the parking spot. This is mystifying, because it doesn't strike me as anything the previous bike-sharimg operators wouldn't have been able to do. In light of this, it appears that at least part of the reason that the new players managed to meet the requirements at all, was with the cooperation of the LTA.

Same same but different?

Things seem to have improved. I no longer see bicycles thrown around nilly-willy. Once in a long while, I might see an abandoned bicycle in a canal, but these seem fewer and further in between, as compared to the old days. Perhaps Singapore as a society has grown. One can only hope.

Abandoned bicycle in canal.

And while there are markedly fewer choices, there's also a whole lot less chaos. Less mess, an improved technical implementation, and an overall better experience for the consumer. I am optimistic.


Let's ride!
T___T

Wednesday, 3 April 2024

Web Tutorial: Easter VueJS Puzzle, AI Edition

Easter's come and gone! With that, I want to introduce an exciting new spin on the Easter VueJs Puzzle Game web tutorial I did in 2020. Instead of using one single picture, we are doing to use a dynamically generated image from Open AI's API.

We'll be using PHP, and you will of course, need to sign up for a developer account at Open AI, get an application key and so on. And then you'll need the code from the existing repository here.

After that, we change the filename to a PHP extension, and start adding PHP code.
<?php

?>


<!DOCTYPE html>
<html>
    <head>
        <title>Easter Puzzle</title>


We begin by declaring variables key, org and url. key is the api key you will have been given, org is your account id, and url is the endpoint URL for OpenAI's image generation API.
<?php
    $key = "sk-xxx";
    $org = "org-FUOhDblZb1pxvaY6YylF54gl";
    $url = 'https://api.openai.com/v1/images/generations';

?>

<!DOCTYPE html>
<html>
    <head>
        <title>Easter Puzzle</title>


We follow up by declaring the headers array. In it, we use key and org to identify ourselves, and ensure that the content-type property is set to JSON.
$key = "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";
$org = "org-FUOhDblZb1pxvaY6YylF54gl";
$url = 'https://api.openai.com/v1/images/generations';

$headers =
[
    "Authorization: Bearer " . $key,
    "OpenAI-Organization: " . $org,
    "Content-Type: application/json"
];


Then we declare data as an associative array. model specifies the AI model we will be using. prompt can be anything that you want the engine to use for generating the image - in this case I have a string telling it I want a picture of Easter eggs. n is the number of images to generate, and in this case we only want one image. size can have a range of different values, and I'm going to specify a square. For response_format, you can get the API to give you the image as a string of serialized code (but then we'd have to decode on our server, so screw that) or just give us the URL to that image that will be stored on their server.
$headers =
[
    "Authorization: Bearer " . $key,
    "OpenAI-Organization: " . $org,
    "Content-Type: application/json"
];

$data = [];
$data["model"] = "dall-e-2";
$data["prompt"] = "A picture of Easter eggs.";
$data["n"] = 1;
$data["size"] = "1024x1024";
$data["response_format"] = "url";


We then use cURL to access the endpoint. If there's an error, we print it out.
    
$data = [];
$data["model"] = "dall-e-2";
$data["prompt"] = "A picture of Easter eggs.";
$data["n"] = 1;
$data["size"] = "1024x1024";
$data["response_format"] = "url";

$curl = curl_init($url);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);


$result = curl_exec($curl);
if (curl_errno($curl))
{
    echo 'Error:' . curl_error($curl);
}


If not, run json_decode() on result, and grab the url of the image, setting it as the value of imgURL. Finally, use curl_close() to sew things up.
    
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

$result = json_decode($result);
$imgeURL = $result->data[0]->url;

curl_close($curl);    


That's the easy part! Now we get to another easy part - setting the image as the puzzle image. In the CSS, modify the CSS class piece. Instead of the previously hardcoded image, use imageURL. And ensure that the background-size property is set to 500 by 500 pixels, because your image is actually 1024 by 1024 pixels.
.piece
{
    width: 100%;
    height: 100%;
    background-image: url(<?php echo $imgeURL; ?>);
    background-repeat: no-repeat;
    background-size: 500px 500px;
}


Now if you run this, you should see a new image come up.


But uh-oh, there's a delay in image loading. The timer has even started running down even before the image is done loading.


That's only to be expected. Your browser code will run faster than the server code sending data to OpenAI and back. And depending on the speed of OpenAI's server where the image is stored, there's going to be some level of lag. What we need to do is mitigate that. That's the hard part. First, we change this message in the reset() function. Originally, it reads "Time elapsed", but now we will switch it to a notice for the user to wait.
reset: function()
{
    this.stopTimer();
    this.seconds = 100;
    this.btnText = "RESET";
    this.message = "Please wait while image loads...";
    this.startTimer();


Also, now the reset() method will have a parameter, delay. What do we do with this value? Well, we pass it in as an argument when calling the startTimer() method.
reset: function(delay)
{
    this.stopTimer();
    this.seconds = 100;
    this.btnText = "RESET";
    this.message = "Please wait while image loads...";
    this.startTimer(delay);


This means we also need to change the startTimer() method. Firstly and most obviously, we introduce the parameter delay.
startTimer: function(delay)
{
    if (this.timer == undefined)
    {
        this.timer = setInterval
        (
            () =>
            {
                this.seconds = this.seconds - 1;
                this.message = "Time elapsed";

                if (this.seconds == 0)
                {
                    this.stopTimer();
                    this.btnText = "REPLAY";
                    this.message = "Better luck next time!";
                }

                if (this.checkIncorrectPieces() == 0)
                {
                    this.stopTimer();
                    this.btnText = "REPLAY";
                    this.message = "Congratulations!";
                }
            },
            1000
        );
    }
},


And then we wrap the entire section involving the setInterval() function, within a callback which will be used in a setTimeout() function, using delay as the duration. And inside it, we also set the message property value to "Time elapsed".
startTimer: function(delay)
{
    if (this.timer == undefined)
    {
        setTimeout
        (
            () =>
            {

                this.timer = setInterval
                (
                    () =>
                    {
                        this.seconds = this.seconds - 1;
                        this.message = "Time elapsed";

                        if (this.seconds == 0)
                        {
                            this.stopTimer();
                            this.btnText = "REPLAY";
                            this.message = "Better luck next time!";
                        }

                        if (this.checkIncorrectPieces() == 0)
                        {
                            this.stopTimer();
                            this.btnText = "REPLAY";
                            this.message = "Congratulations!";
                        }
                    },
                    1000
                );
            },
            delay
        );

    }
},


Then we ensure that when we call reset() upon a page load, it has about a 5 second delay.
created: function()
{
    this.reset(5000);
}


But in the HTML, this value will be 0 when we click the button to run reset(). Because if that button is being clicked, that means the user just wants to reset the game and the image has already been loaded, so there's no need for a delay.
<div id="timeContainer">
    <h2>{{message}}</h2>
    <h1>{{seconds}}</h1>
    <button v-on:click="reset(0)">{{btnText}}</button>
</div>


Now we see this happen! And the timer doesn't count down until...


...the entire image is loaded! If you click the RESET button, the pieces rearrange themselves and the timer starts counting down immediately!


One more feature...

What if you wanted to just change the image altogether? Well, the straightforward thing to do would be to refresh the browser, but it's still considered good service to add a button.
<div id="timeContainer">
    <h2>{{message}}</h2>
    <h1>{{seconds}}</h1>
    <button v-on:click="reset(0)">{{btnText}}</button>
    <button>NEW IMAGE</button>
</div>


Use the v-on:click binding attribute and set it to run the refresh() method.
<div id="timeContainer">
    <h2>{{message}}</h2>
    <h1>{{seconds}}</h1>
    <button v-on:click="reset(0)">{{btnText}}</button>
    <button v-on:click="refresh">NEW IMAGE</button>
</div>


Add the refresh() method to the methods object. In it, you set the message property to a friendly warning.
methods:
{
    refresh: function()
    {
        this.message = "Please wait while new image loads...";
    },

    reset: function(delay)
    {


Then you run the stopTimer() method, and finally refresh using the built-in JavaScript reload() method.
methods:
{
    refresh: function()
    {
        this.message = "Please wait while new image loads...";
        this.stopTimer();
        window.location.reload();

    },
    reset: function(delay)
    {


There you go. Click the NEW IMAGE button, and "Please wait while the new image loads..." should appear briefly before the page reloads entirely!


Final words, and hope you had a Happy Easter!

Well, this has been fun. AI is full of interesting new possibilities, and with this little exercise today, we've just scratched the surface.

Piece be with you,
T___T