Skip to content

Commit

Permalink
- bug fixes
Browse files Browse the repository at this point in the history
- audioPlayer close button
- nextcloud icon on settings
- readme
  • Loading branch information
cardo-podcast committed Aug 26, 2024
1 parent bf45580 commit dd4f8e4
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 202 deletions.
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<h1 align="center">CARDO - PODCAST PLAYER</h1>
<p align="center">
<a href="https://n0vella.github.io">
<img src="https://raw.githubusercontent.com/n0vella/cardo/master/src-tauri/icons/icon.png" alt="logo" width="256" height="256" />
</a>
</p>

## Overview

Cardo is a podcast player, inspired on Android's [Antennapod](https://antennapod.org/). Cardo could be synchonized with Antennapod and other apps using [Nextcloud Gppoder](https://github.com/thrillfall/nextcloud-gpodder/).

![1](assets/readme/1.png)

![2](assets/readme/2.png)

![3](assets/readme/3.png)

![4](assets/readme/4.png)

### Features

- Search podcasts online
- Manage your subscriptions
- Look at new episodes of your subscriptions with a glance
- Synchronizing episodes state and subscriptions using Nexcloud Gpodder
- Lightweight app (thanks to Tauri)
- Customizable themes
- [ ] Download episodes to listen them offline
- [ ] Keep your favorite episodes


## Contributing

### Helping with donations

If you like this app you can contribute buying me a cofee or whatever you want, that would be really great :)

<div style="display: inline-flex; gap: 10px; align-items: center">
<a href="https://www.buymeacoffee.com/n0vella" target="_blank">
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
</a>
<a href="https://www.paypal.com/paypalme/adriannovella">
<img src="https://www.paypalobjects.com/webstatic/icon/pp196.png" alt="Paypal" width="60" height="60" style="border-radius: 10px" />
</a>
</div>

### If you are a developer

It's also nice if you want to improve the app. The stack is Tauri v1 + React + Typescript + Tailwind.

### Translations

You can contribute with translations if you speak some other languages.
It's only needed to replicate json's files in resources / translations. There is a tool on scripts to auto translate it using Google Translate, but I didn't want to leave a bad translations, even english could be badly translated as it isn't my mother language.
Binary file added assets/readme/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/readme/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/readme/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/readme/4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion resources/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"date": "date",
"duration": "duration",
"logged_in": "You have already logged in",
"nextcloud_server_url": "https://nextcloud.example.com",
"nextcloud_server_url": "URL of Nextcloud server",
"connect": "connect",
"log_out": "log out",
"sync": "synchronization",
Expand Down
27 changes: 15 additions & 12 deletions src/DB.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,17 @@ const getEpisodeState = async (episodeUrl: string): Promise<EpisodeState | undef
}
}

const getCompletedEpisodes = async () => {
const getCompletedEpisodes = async (podcastUrl?: string) => {

const playedEpisodes: EpisodeState[] = await db.select(
`SELECT episode from episodes_history
WHERE position = total`)
const query = `SELECT episode from episodes_history
WHERE position = total
${podcastUrl? 'AND podcast = $1': ''}
`

return playedEpisodes.map(episode => episode.episode) //only returns url
}
const playedEpisodes: EpisodeState[] = await db.select(query, [podcastUrl])

return playedEpisodes.map(episode => episode.episode) //only returns url
}

const getEpisodesStates = async (timestamp = 0): Promise<EpisodeState[]> => {
const r: EpisodeState[] = await db.select(
Expand Down Expand Up @@ -155,18 +158,18 @@ const getLastPlayed = async (): Promise<EpisodeData | undefined> => {
const setLastPlaying = async (playingEpisode?: EpisodeData) => {
// empty args to set NONE as last played

const data = playingEpisode ? JSON.stringify(playingEpisode): 'NONE'
const data = playingEpisode ? JSON.stringify(playingEpisode) : 'NONE'

return await setMiscValue('lastPlaying', data)

}


const getLastUpdate = async() => {
const getLastUpdate = async () => {
return Number(await getMiscKey('lastUpdate') ?? '0')
}

const setLastUpdate = async(timestamp: number) => {
const setLastUpdate = async (timestamp: number) => {
return await setMiscValue('lastUpdate', timestamp.toString())
}

Expand Down Expand Up @@ -306,7 +309,7 @@ function initQueue() {
return r.map(episode => ({
...episode,
pubDate: new Date(episode.pubDate),
podcast: {coverUrl: episode.podcastCover}
podcast: { coverUrl: episode.podcastCover }
}))
}

Expand Down Expand Up @@ -422,7 +425,7 @@ const loadNewSubscriptionsEpisodes = async (pubdate_gt: number): Promise<NewEpis
...episode,
pubDate: new Date(episode.pubDate),
new: episode.pubDate > lastSync, // is new if it's just discovered
podcast: {coverUrl: episode.podcastCover}
podcast: { coverUrl: episode.podcastCover }
}))

}
Expand Down Expand Up @@ -461,7 +464,7 @@ function initDB() {
const updateSubscriptionsFeed = async () => {
setUpdatingFeeds(true)
for (const subscription of subscriptionsList) {
const [episodes, ] = await parseXML(subscription.feedUrl)
const [episodes,] = await parseXML(subscription.feedUrl)
const r = await saveSubscriptionsEpisodes(episodes)
if (r.rowsAffected > 0) {
loadNewEpisodes()
Expand Down
16 changes: 8 additions & 8 deletions src/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,21 @@ export function usePodcastSettings(feedUrl: string): [PodcastSettings, typeof up


const updatePodcastSettings = (newPodcastSettings: RecursivePartial<PodcastSettings>) => {
const newSettings = { ...settings }
const newSettings = settings.podcasts

if (!newSettings.podcasts[feedUrl]) {
newSettings.podcasts[feedUrl] = new PodcastSettings()
if (!newSettings[feedUrl]) {
newSettings[feedUrl] = new PodcastSettings()
}

merge(newSettings.podcasts[feedUrl], newPodcastSettings)
merge(newSettings[feedUrl], newPodcastSettings)

if (PodcastSettings.isDefault((newSettings.podcasts[feedUrl]))) {
if (PodcastSettings.isDefault((newSettings[feedUrl]))) {
// default settings aren't stored on json
delete newSettings.podcasts[feedUrl]
delete newSettings[feedUrl]
}

updateSettings({ podcasts: newSettings.podcasts })
setPodcastSettings(newSettings.podcasts[feedUrl] ?? new PodcastSettings)
updateSettings({ podcasts: newSettings })
setPodcastSettings(newSettings[feedUrl] ?? new PodcastSettings)
}


Expand Down
107 changes: 57 additions & 50 deletions src/components/AudioPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRef, useEffect, useState, RefObject, createContext, ReactNode, useContext, Dispatch, SetStateAction, useCallback, SyntheticEvent } from "react";
import { secondsToStr } from "../utils";
import { play as playIcon, pause as pauseIcon, forward as forwardIcon, backwards as backwardsIcon } from "../Icons"
import { play as playIcon, pause as pauseIcon, forward as forwardIcon, backwards as backwardsIcon, close as closeIcon } from "../Icons"
import { EpisodeData } from "..";
import { useDB } from "../DB";
import { useNavigate } from "react-router-dom";
Expand Down Expand Up @@ -240,59 +240,66 @@ function AudioPlayer({ className = '' }) {
<div className={`w-full flex flex-col`}>
{playing && <h1 className="mb-1 truncate">{playing.title}</h1>}

<div className="w-full flex items-center justify-center">
<p>{secondsToStr(position)}</p>
<input
type="range"
min="0"
max={duration}
value={position}
onChange={(event) => changeTime(Number(event.target.value))}
className="w-4/5 mx-1 h-[3px] bg-primary-3 accent-accent-6"
/>
<p className="cursor-pointer"
title={t('toggle_remaining_time')}
onClick={() => updateSettings({ playback: { displayRemainingTime: !displayRemainingTime } })}>
{secondsToStr(displayRemainingTime ? position - duration : duration)}
</p>
</div>

<div className="flex justify-center gap-3 items-center">

<button
className="flex flex-col items-center focus:outline-none hover: hover:text-accent-6 w-7"
onClick={() => {
changeTime(-1 * stepBackwards, true)
}}
>
{backwardsIcon}
<p className="text-xs text-center -mt-[7px]">{stepBackwards}</p>
</button>

<button
className="flex items-center focus:outline-none hover: hover:text-accent-6 w-9 mb-2"
onClick={handlePlayPause}
>
{audioRef.current?.paused ? playIcon : pauseIcon}
</button>

<button
className="flex flex-col items-center focus:outline-none hover: hover:text-accent-6 w-7"
onClick={() => {
changeTime(stepForward, true)
}}
>
{forwardIcon}
<p className="text-xs text-center -mt-[7px]">{stepForward}</p>
</button>
</div>
<div className="w-full flex items-center justify-center">
<p>{secondsToStr(position)}</p>
<input
type="range"
min="0"
max={duration}
value={position}
onChange={(event) => changeTime(Number(event.target.value))}
className="w-4/5 mx-1 h-[3px] bg-primary-3 accent-accent-6"
/>
<p className="cursor-pointer"
title={t('toggle_remaining_time')}
onClick={() => updateSettings({ playback: { displayRemainingTime: !displayRemainingTime } })}>
{secondsToStr(displayRemainingTime ? position - duration : duration)}
</p>
</div>

<div className="flex justify-center gap-3 items-center">

<button
className="flex flex-col items-center focus:outline-none hover: hover:text-accent-6 w-7"
onClick={() => {
changeTime(-1 * stepBackwards, true)
}}
>
{backwardsIcon}
<p className="text-xs text-center -mt-[7px]">{stepBackwards}</p>
</button>

<button
className="flex items-center focus:outline-none hover: hover:text-accent-6 w-9 mb-2"
onClick={handlePlayPause}
>
{audioRef.current?.paused ? playIcon : pauseIcon}
</button>

<button
className="flex flex-col items-center focus:outline-none hover: hover:text-accent-6 w-7"
onClick={() => {
changeTime(stepForward, true)
}}
>
{forwardIcon}
<p className="text-xs text-center -mt-[7px]">{stepForward}</p>
</button>
</div>


<audio ref={audioRef} onLoadedMetadata={handleLoadedMetadata} className="hidden" />
</div>

<div className="w-24 aspect-square shrink-0">
{/* space to keep simetry */}

<div className="group w-24 h-full aspect-square shrink-0">
{/* extra width is to keep simetry */}
<div className="w-full justify-end flex">
<button className="w-7 group-hover:text-red-600 text-transparent"
onClick={quit}
>
{closeIcon}
</button>
</div>
</div>

</div>
Expand Down
Loading

0 comments on commit dd4f8e4

Please sign in to comment.