Skip to content

Commit

Permalink
Update spanish-tv-guide extension (raycast#12728)
Browse files Browse the repository at this point in the history
* Update spanish-tv-guide extension

- Update changelog
- Merge pull request #1 from doktor500/tv-guide-v2
- Formatting
- Refactor
- Imrpove markdown
- Always display up to date channel schedule
- Rename file
- Rename to DTO
- Bump dependencies
- Fix imports
- Fix time issue
- Display if a program is live
- Set selected program based on user interaction
- Refactor types
- Fix timezone issue
- Move get program details to repository
- Add maybe type
- Rename property
- Display program details
- Fix lint
- Optimise icon generation process
- Extract toId function
- Drop previous broadcasted programs when the list is too long
- Fix flickering issue
- Fix keys
- Restructure directories
- Refactor Channel details component
- Refactor Channel component
- Extract icon utils
- Refactor channels component
- Fix lint
- Use navigation api
- Selected channel view
- Create all channels view
- Use reducer
- Remove type
- Fix lint
- Update raycast username
- User friendly error handling
- Set live stream icon
- Bump dependencies
- Wait for icons to be loaded
- Fix metadata and readme
- Update CHANGELOG.md
- Fix lint
- Cleanup
- Update changelog
- Extract constant
- Bump dependencies
- Resize icons
- Optimize
- Pull contributions
- Remove live logic
- Update demo.gif
- Fix title
- Add screenshot
- Fix icon
- Bump dependencies
- Initial version

* Update spanish-tv-guide extension

- Fix lint issues
- Renamet metadata directory to docs
- Fix changelog typo
- Update changelog date

* Add eslint file

* Downgrade eslint to 8.X
  • Loading branch information
doktor500 authored Jun 12, 2024
1 parent d6ee7ea commit 2231b7d
Show file tree
Hide file tree
Showing 22 changed files with 1,147 additions and 747 deletions.
2 changes: 1 addition & 1 deletion extensions/spanish-tv-guide/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"root": true,
"extends": ["@raycast"]
}
}
7 changes: 7 additions & 0 deletions extensions/spanish-tv-guide/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# spanish-tv-guide Changelog

## [UX improvements and bug fixes] - 2024-06-02
- Display if a program is live
- Fix timezone issues
- Keep user selected program when navigating back
- Always display up to date channel schedule
- Improve program details layout

## [Live stream icon & error handling] - 2023-10-03
- Display a live stream icon for the program that is currently broadcasted
- Implemented a more user friendly error handling
Expand Down
2 changes: 1 addition & 1 deletion extensions/spanish-tv-guide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

[Raycast](https://www.raycast.com/) extension that displays the Spanish TV guide

![spanish-tv-guide-1.png](metadata/spanish-tv-guide-1.png)
![spanish-tv-guide-1.png](docs/spanish-tv-guide-1.png)
1,428 changes: 799 additions & 629 deletions extensions/spanish-tv-guide/package-lock.json

Large diffs are not rendered by default.

37 changes: 19 additions & 18 deletions extensions/spanish-tv-guide/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,32 @@
}
],
"dependencies": {
"@raycast/api": "^1.59.0",
"friendly-truncate": "^1.1.0",
"@raycast/api": "^1.75.2",
"friendly-truncate": "^1.3.0",
"isomorphic-fetch": "^3.0.0",
"jimp": "^0.22.10",
"jimp": "^0.22.12",
"lodash": "^4.17.21",
"luxon": "^3.4.3",
"luxon": "^3.4.4",
"monet": "^0.9.3",
"ramda": "^0.29.0"
"node-html-parser": "^6.1.13",
"ramda": "^0.30.0"
},
"devDependencies": {
"@raycast/eslint-config": "1.0.6",
"@types/isomorphic-fetch": "^0.0.37",
"@types/jest": "^29.5.5",
"@types/lodash": "^4.14.199",
"@types/luxon": "^3.3.2",
"@types/node": "20.8.1",
"@types/ramda": "^0.29.5",
"@types/react": "18.2.17",
"eslint": "^8.50.0",
"@raycast/eslint-config": "1.0.8",
"@types/isomorphic-fetch": "^0.0.39",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.4",
"@types/luxon": "^3.4.2",
"@types/node": "20.13.0",
"@types/ramda": "^0.30.0",
"@types/react": "18.3.3",
"eslint": "^8.56.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^3.0.3",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
"prettier": "^3.3.0",
"ts-jest": "^29.1.4",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
"scripts": {
"build": "ray build -e dist",
Expand Down
26 changes: 15 additions & 11 deletions extensions/spanish-tv-guide/src/components/ChannelDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
import { Fragment } from "react";
import { Color, Icon, List } from "@raycast/api";

import { ChannelSchedule } from "../modules/tv/domain/tvSchedule";
import { ChannelScheduleDto, ProgramDto } from "../modules/tv/domain/tvScheduleDto";
import { getTime } from "../utils/dateUtils";

const { Item } = List;

const ChannelDetails = (channel: ChannelSchedule) => (
export const ChannelDetails = (channel: ChannelScheduleDto) => (
<Item.Detail
metadata={
<Item.Detail.Metadata>
<Item.Detail.Metadata.Label title={`${channel.name}`} icon={channel.icon} />
<Item.Detail.Metadata.Separator />
{channel.schedule.map((program, index) => (
<Fragment key={index}>
<Item.Detail.Metadata.Label
title={program.description}
icon={program.live ? Icon.Livestream : ""}
text={{ value: getTime(program.startTime), color: Color.SecondaryText }}
/>
<Item.Detail.Metadata.Separator />
</Fragment>
<Program key={index} program={program} />
))}
</Item.Detail.Metadata>
}
/>
);

export default ChannelDetails;
const Program = ({ program }: { program: ProgramDto }) => {
return (
<Fragment>
<Item.Detail.Metadata.Label
title={program.title}
icon={program.isCurrentlyLive ? Icon.Clock : ""}
text={{ value: getTime(program.startTime), color: Color.SecondaryText }}
/>
<Item.Detail.Metadata.Separator />
</Fragment>
);
};
61 changes: 61 additions & 0 deletions extensions/spanish-tv-guide/src/components/ChannelList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from "react";
import { Action, ActionPanel, List, useNavigation } from "@raycast/api";

import { State } from "../index";
import { ChannelScheduleDto, TvScheduleDto, upToDateChannelSchedule } from "../modules/tv/domain/tvScheduleDto";
import { ChannelDetails } from "./ChannelDetails";
import { SelectedChannel } from "./SelectedChannel";
import { iconPath } from "../utils/iconUtils";
import { isEmpty, isNull } from "../utils/objectUtils";
import { toId } from "../utils/stringUtils";

const SELECT_CHANNEL_ACTION = "Select Channel";

export const ChannelList = ({ state, setState }: { state: State; setState: React.Dispatch<Partial<State>> }) => {
const { tvSchedule, selectedChannel } = state;

const selectChannel = (channel: string | null) => {
const selectedChannel = !isNull(channel);
if (selectedChannel) setState({ selectedChannel: channel });
};

return (
<List
selectedItemId={selectedChannel}
isLoading={isEmpty(tvSchedule)}
onSelectionChange={selectChannel}
isShowingDetail={Boolean(state.selectedChannel)}
>
{tvSchedule.map((schedule) => (
<Channel key={schedule.name} tvSchedule={state.tvSchedule} channelSchedule={schedule} />
))}
</List>
);
};

const Channel = (props: { tvSchedule: TvScheduleDto; channelSchedule: ChannelScheduleDto }) => {
const { push } = useNavigation();
const { icon, name, schedule } = props.channelSchedule;
const selectedChannel = props.tvSchedule.find((channel) => channel.name === name);

const Actions = () => (
<ActionPanel>
<Action
title={SELECT_CHANNEL_ACTION}
icon={iconPath(icon)}
onAction={() => selectedChannel && push(<SelectedChannel channel={selectedChannel} />)}
/>
</ActionPanel>
);

return (
<List.Item
key={name}
id={toId(name)}
title={name}
icon={iconPath(icon)}
detail={<ChannelDetails name={name} schedule={upToDateChannelSchedule(schedule)} icon={icon} />}
actions={<Actions />}
/>
);
};
6 changes: 2 additions & 4 deletions extensions/spanish-tv-guide/src/components/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { Detail } from "@raycast/api";
import { ERROR_MESSAGE } from "../index";

const ErrorMessage = () => <Detail markdown={formattedError(ERROR_MESSAGE)} />;
export const ERROR_MESSAGE = "Error fetching TV guide";
export const ErrorMessage = () => <Detail markdown={formattedError(ERROR_MESSAGE)} />;

const formattedError = (error: string) => `
| ❗ |
| :----------------: |
| ${error} |`;

export default ErrorMessage;
63 changes: 63 additions & 0 deletions extensions/spanish-tv-guide/src/components/SelectedChannel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState } from "react";
import { Action, ActionPanel, Icon, List, useNavigation } from "@raycast/api";

import { ChannelScheduleDto, ProgramDto, upToDateChannelSchedule } from "../modules/tv/domain/tvScheduleDto";
import { getTime } from "../utils/dateUtils";
import { iconPath } from "../utils/iconUtils";
import { SelectedProgram } from "./SelectedProgram";

type ProgramProps = {
channel: ChannelScheduleDto;
program: ProgramDto;
index: number;
onSelect: (index: number) => void;
};

const SELECT_PROGRAM_ACTION = "Select Program";

export const SelectedChannel = ({ channel }: { channel: ChannelScheduleDto }) => {
const schedule = upToDateChannelSchedule(channel.schedule);
const currentLiveProgram = schedule.findIndex((program) => program.isCurrentlyLive);
const [selectedProgramIndex, setSelectedProgramIndex] = useState<number>(currentLiveProgram);

return (
<List selectedItemId={selectedProgramIndex.toString()} navigationTitle={channel.name}>
<List.Section key={`channel-${channel.name}`}>
<List.Item key={channel.name} title={`${channel.name}`} icon={iconPath(channel.icon)} />
</List.Section>
<List.Section key={`schedule-${channel.name}`}>
{schedule.map((program, index) => (
<Program key={index} channel={channel} program={program} index={index} onSelect={setSelectedProgramIndex} />
))}
</List.Section>
</List>
);
};

const Program = ({ channel, program, index, onSelect: setSelectedProgramIndex }: ProgramProps) => {
const { push } = useNavigation();

const Actions = () => (
<ActionPanel>
<Action
title={SELECT_PROGRAM_ACTION}
icon={iconPath(channel.icon)}
onAction={() => {
setSelectedProgramIndex(index);
push(<SelectedProgram channel={channel} program={program} />);
}}
/>
</ActionPanel>
);

return (
<List.Item
key={index}
id={index.toString()}
title={`${getTime(program.startTime)} - ${program.title}`}
icon={program.isCurrentlyLive ? Icon.Clock : ""}
accessories={[{ icon: program.isLive ? Icon.Livestream : "" }]}
actions={<Actions />}
/>
);
};
39 changes: 39 additions & 0 deletions extensions/spanish-tv-guide/src/components/SelectedProgram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useEffect, useState } from "react";
import { Detail } from "@raycast/api";

import { tvScheduleRepository } from "../modules/tv/repositories/tvScheduleRepository";
import { ChannelScheduleDto, ProgramDto, ProgramDetailsDto } from "../modules/tv/domain/tvScheduleDto";
import { getTime } from "../utils/dateUtils";
import { Maybe } from "../utils/objectUtils";

export const SelectedProgram = ({ channel, program }: { channel: ChannelScheduleDto; program: ProgramDto }) => {
const [programDetails, setProgramDetails] = useState<Maybe<ProgramDetailsDto>>();

useEffect(() => void tvScheduleRepository.getProgramDetails(program).then(setProgramDetails), [program]);

return (
programDetails && (
<Detail
navigationTitle={channel.name}
markdown={formattedProgramDetails(programDetails)}
isLoading={!programDetails}
metadata={
<Detail.Metadata>
<Detail.Metadata.Label title={channel.name} icon={channel.icon} />
<Detail.Metadata.Label title={program.title} />
</Detail.Metadata>
}
/>
)
);
};

const formattedProgramDetails = ({ title, startTime, image, description }: ProgramDetailsDto) => `
### ${getTime(startTime)} ${title}
---
${description}
![${title}](${image}?raycast-width=125&raycast-height=188)
`;
68 changes: 25 additions & 43 deletions extensions/spanish-tv-guide/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,37 @@
import { List, showToast, Toast } from "@raycast/api";
import { useEffect, useState } from "react";
import Jimp from "jimp";
import { showToast, Toast } from "@raycast/api";
import React, { useEffect, useReducer } from "react";

import { ChannelSchedule, TVSchedule } from "./modules/tv/domain/tvSchedule";
import ChannelDetails from "./components/ChannelDetails";
import { TvScheduleDto } from "./modules/tv/domain/tvScheduleDto";
import { tvScheduleRepository } from "./modules/tv/repositories/tvScheduleRepository";
import { isEmpty, isNull } from "./utils/objectUtils";
import ErrorMessage from "./components/ErrorMessage";
import { ERROR_MESSAGE, ErrorMessage } from "./components/ErrorMessage";
import { ChannelList } from "./components/ChannelList";
import { generateIcon } from "./utils/iconUtils";

export type State = {
tvSchedule: TvScheduleDto;
selectedChannel?: string;
error?: Error;
};

const ICONS_DIRECTORY = "/tmp/raycast/spanish-tv-guide/icons";
export const ERROR_MESSAGE = "Error fetching TV guide";
const initialState: State = { tvSchedule: [] };
const reducer = (state: State, newState: Partial<State>) => ({ ...state, ...newState });

const Command = () => {
const [tvSchedule, setTvSchedule] = useState<TVSchedule>([]);
const [isShowingDetail, setIsShowingDetail] = useState(false);
const [iconsLoaded, setIconsLoaded] = useState(false);
const [error, setError] = useState<Error | undefined>();
const [selectedChannel, setSelectedChannel] = useState<string | undefined>();

useEffect(() => void tvScheduleRepository.getAll().then(setTvSchedule).catch(setError), []);
useEffect(() => void generateIcons(tvSchedule).then(() => setIconsLoaded(true)), [tvSchedule]);
useEffect(() => error && void showToast({ style: Toast.Style.Failure, title: ERROR_MESSAGE }), [error]);

const selectChannel = (channel: string | null) => {
const channelSelected = !isNull(channel);
if (channelSelected) setSelectedChannel(channel);
setIsShowingDetail(channelSelected);
const [state, setState] = useReducer(reducer, initialState);

const initialize = async () => {
return tvScheduleRepository
.getAll()
.then((tvSchedule) => cacheIcons(tvSchedule).then(() => setState({ tvSchedule })))
.catch((error) => setState({ error }));
};

if (error) return <ErrorMessage />;

return (
<List
isLoading={isEmpty(tvSchedule) || !iconsLoaded}
selectedItemId={selectedChannel}
isShowingDetail={isShowingDetail}
onSelectionChange={selectChannel}
>
{tvSchedule.map(renderChannel)}
</List>
);
};
useEffect(() => void initialize(), []);
useEffect(() => state.error && void showToast({ style: Toast.Style.Failure, title: ERROR_MESSAGE }), [state.error]);

const renderChannel = ({ icon, name, schedule }: ChannelSchedule) => {
const detail = <ChannelDetails name={name} schedule={schedule} icon={icon} />;
return <List.Item key={name} title={name} detail={detail} icon={iconPath(icon)} />;
return state.error ? <ErrorMessage /> : <ChannelList state={state} setState={setState} />;
};

const generateIcons = (tvSchedule: TVSchedule) => Promise.all(tvSchedule.map(({ icon }) => generateIcon(icon)));
const generateIcon = (icon: string) => Jimp.read(icon).then((image) => image.contain(256, 256).write(iconPath(icon)));
const iconPath = (icon: string) => `${ICONS_DIRECTORY}/${iconName(icon)}`;
const iconName = (icon: string) => icon.substring(icon.lastIndexOf("/") + 1);
const cacheIcons = (tvSchedule: TvScheduleDto) => Promise.all(tvSchedule.map(({ icon }) => generateIcon(icon)));

export default Command;
13 changes: 0 additions & 13 deletions extensions/spanish-tv-guide/src/modules/tv/domain/tvSchedule.ts

This file was deleted.

Loading

0 comments on commit 2231b7d

Please sign in to comment.