From 06654ebf045e6e47485b60aea34a12caea7e7406 Mon Sep 17 00:00:00 2001 From: SamTV12345 Date: Fri, 30 Aug 2024 15:54:52 +0200 Subject: [PATCH] Added tagging system in backend --- src/controllers/podcast_controller.rs | 17 ++-- src/controllers/tags_controller.rs | 3 +- src/models/messages.rs | 3 +- src/models/podcast_dto.rs | 1 + src/models/tag.rs | 10 +- src/service/mapping_service.rs | 11 ++- src/service/podcast_episode_service.rs | 2 +- src/service/rust_service.rs | 3 +- ui/index.html | 2 +- ui/package.json | 1 + ui/pnpm-lock.yaml | 30 ++++++ ui/src/components/PodcastCard.tsx | 101 ++++++++++++++++----- ui/src/components/PodcastSettingsModal.tsx | 2 + ui/src/icons/PlusIcon.tsx | 7 +- ui/src/models/PodcastTags.tsx | 8 ++ ui/src/pages/Podcasts.tsx | 13 ++- ui/src/store/CommonSlice.ts | 7 +- 17 files changed, 171 insertions(+), 50 deletions(-) create mode 100644 ui/src/models/PodcastTags.tsx diff --git a/src/controllers/podcast_controller.rs b/src/controllers/podcast_controller.rs index a285b947..2f01b60a 100644 --- a/src/controllers/podcast_controller.rs +++ b/src/controllers/podcast_controller.rs @@ -157,15 +157,13 @@ pub async fn find_podcast_by_id( user: Option>, ) -> Result { let id_num = from_str::(&id).unwrap(); + let username = user.unwrap().username.clone(); + let podcast = PodcastService::get_podcast(conn.get().map_err(map_r2d2_error)?.deref_mut(), id_num)?; - let mapped_podcast = MappingService::map_podcast_to_podcast_dto(&podcast); - let tags = Tag::get_tags_of_podcast( - conn.get().map_err(map_r2d2_error)?.deref_mut(), - podcast.id, - &user.unwrap().username, - )?; - Ok(HttpResponse::Ok().json((mapped_podcast, tags))) + let tags = Tag::get_tags_of_podcast(conn.get().map_err(map_r2d2_error)?.deref_mut(), id_num, &username)?; + let mapped_podcast = MappingService::map_podcast_to_podcast_dto(&podcast, tags); + Ok(HttpResponse::Ok().json(mapped_podcast)) } #[utoipa::path( @@ -184,6 +182,7 @@ pub async fn find_all_podcasts( let podcasts = PodcastService::get_podcasts(conn.get().map_err(map_r2d2_error)?.deref_mut(), username)?; + Ok(HttpResponse::Ok().json(podcasts)) } @@ -492,7 +491,7 @@ pub async fn refresh_all_podcasts( podcast_episode: None, type_of: PodcastType::RefreshPodcast, message: format!("Refreshed podcast: {}", podcast.name), - podcast: Option::from(podcast.clone()), + podcast: Option::from(MappingService::map_podcast_to_podcast_dto(&podcast, vec![])), podcast_episodes: None, }).unwrap()); } @@ -697,7 +696,7 @@ async fn insert_outline( let _ = lobby.send_broadcast(MAIN_ROOM.parse().unwrap(), serde_json::to_string(&BroadcastMessage { type_of: PodcastType::OpmlAdded, message: "Refreshed podcasts".to_string(), - podcast: Option::from(podcast), + podcast: Option::from(MappingService::map_podcast_to_podcast_dto(&podcast, vec![])), podcast_episodes: None, podcast_episode: None, }).unwrap()).await; diff --git a/src/controllers/tags_controller.rs b/src/controllers/tags_controller.rs index bfda8880..cfc2f946 100644 --- a/src/controllers/tags_controller.rs +++ b/src/controllers/tags_controller.rs @@ -47,10 +47,9 @@ responses( tag="tags" )] #[get("/tags")] -pub async fn get_tags(conn: Data, requester: Option>, _mapping_service: Data>) -> +pub async fn get_tags(conn: Data, requester: Option>) -> Result { let tags = Tag::get_tags(&mut conn.get().unwrap(), requester.unwrap().username.clone())?; - Ok(HttpResponse::Ok().json(tags)) } diff --git a/src/models/messages.rs b/src/models/messages.rs index ac498f2e..9ae88309 100644 --- a/src/models/messages.rs +++ b/src/models/messages.rs @@ -1,4 +1,5 @@ use crate::constants::inner_constants::PodcastType; +use crate::models::podcast_dto::PodcastDto; use crate::models::podcast_episode::PodcastEpisode; use crate::models::podcasts::Podcast; @@ -7,7 +8,7 @@ use crate::models::podcasts::Podcast; pub struct BroadcastMessage { pub type_of: PodcastType, pub message: String, - pub podcast: Option, + pub podcast: Option, pub podcast_episodes: Option>, pub podcast_episode: Option, } diff --git a/src/models/podcast_dto.rs b/src/models/podcast_dto.rs index 4c54285e..af12b6b8 100644 --- a/src/models/podcast_dto.rs +++ b/src/models/podcast_dto.rs @@ -5,6 +5,7 @@ pub struct PodcastDto { pub(crate) id: i32, pub(crate) name: String, pub directory_id: String, + pub directory_name: String, pub(crate) rssfeed: String, pub image_url: String, pub summary: Option, diff --git a/src/models/tag.rs b/src/models/tag.rs index d7da89e7..ffba8c9f 100644 --- a/src/models/tag.rs +++ b/src/models/tag.rs @@ -24,15 +24,15 @@ pub struct Tag { #[diesel(sql_type = Text)] pub(crate) id: String, #[diesel(sql_type = Text)] - name: String, + pub name: String, #[diesel(sql_type = Text)] - username: String, + pub username: String, #[diesel(sql_type = Nullable)] - description: Option, + pub description: Option, #[diesel(sql_type = Timestamp)] - created_at: NaiveDateTime, + pub created_at: NaiveDateTime, #[diesel(sql_type = Text)] - color: String, + pub color: String, } impl Tag { diff --git a/src/service/mapping_service.rs b/src/service/mapping_service.rs index 8b393e3c..05cb225d 100644 --- a/src/service/mapping_service.rs +++ b/src/service/mapping_service.rs @@ -9,9 +9,11 @@ use crate::service::environment_service; #[derive(Clone)] pub struct MappingService {} + + impl MappingService { - pub fn map_podcast_to_podcast_dto(podcast: &Podcast) -> Podcast { - Podcast { + pub fn map_podcast_to_podcast_dto(podcast: &Podcast, tags: Vec) -> PodcastDto { + PodcastDto { id: podcast.id, name: podcast.name.clone(), directory_id: podcast.directory_id.clone(), @@ -27,7 +29,9 @@ impl MappingService { author: podcast.author.clone(), active: podcast.active, original_image_url: podcast.original_image_url.clone(), - directory_name: podcast.directory_name.clone() + directory_name: podcast.directory_name.clone(), + tags, + favorites: false } } @@ -54,6 +58,7 @@ impl MappingService { active: podcast_favorite_grouped.0.active, original_image_url: podcast_favorite_grouped.0.original_image_url.clone(), favorites: favorite, + directory_name: podcast_favorite_grouped.0.directory_name.clone(), tags, } } diff --git a/src/service/podcast_episode_service.rs b/src/service/podcast_episode_service.rs index 7231c146..eaf015bd 100644 --- a/src/service/podcast_episode_service.rs +++ b/src/service/podcast_episode_service.rs @@ -54,7 +54,7 @@ impl PodcastEpisodeService { "Episode {} is now available offline", podcast_episode.name ), - podcast: Option::from(podcast.clone()), + podcast: Option::from(MappingService::map_podcast_to_podcast_dto(&podcast, vec![])), type_of: PodcastType::AddPodcastEpisode, podcast_episode: Some(mapped_dto), podcast_episodes: None, diff --git a/src/service/rust_service.rs b/src/service/rust_service.rs index a45c2052..193d46c1 100644 --- a/src/service/rust_service.rs +++ b/src/service/rust_service.rs @@ -168,6 +168,7 @@ impl PodcastService { message: format!("Added podcast: {}", inserted_podcast.name), podcast: Option::from(MappingService::map_podcast_to_podcast_dto( &podcast.clone().unwrap(), + vec![] )), podcast_episodes: None, }).unwrap()).await; @@ -187,7 +188,7 @@ impl PodcastService { podcast_episode: None, type_of: PodcastType::AddPodcastEpisodes, message: format!("Added podcast episodes: {}", podcast.name), - podcast: Option::from(podcast.clone()), + podcast: Option::from(MappingService::map_podcast_to_podcast_dto(&podcast, vec![])), podcast_episodes: Option::from(inserted_podcasts), }).unwrap()); if let Err(e) = diff --git a/ui/index.html b/ui/index.html index 774e4e23..dbac7f18 100644 --- a/ui/index.html +++ b/ui/index.html @@ -5,7 +5,7 @@ Podfetch - +
diff --git a/ui/package.json b/ui/package.json index dc77f9e3..22383c28 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,6 +16,7 @@ "@fontsource/roboto": "^5.0.14", "@fortawesome/fontawesome-free": "^6.6.0", "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-popover": "^1.1.1", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 77e78e22..e2a16719 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-context-menu': + specifier: ^2.2.1 + version: 2.2.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -439,6 +442,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-context-menu@2.2.1': + resolution: {integrity: sha512-wvMKKIeb3eOrkJ96s722vcidZ+2ZNfcYZWBPRHIB1VWrF+fiF851Io6LX0kmK5wTDQFKdulCCKJk2c3SBaQHvA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-context@1.1.0': resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} peerDependencies: @@ -2272,6 +2288,20 @@ snapshots: optionalDependencies: '@types/react': 18.3.4 + '@radix-ui/react-context-menu@2.2.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-context': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-menu': 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.4)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.4)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.4 + '@types/react-dom': 18.3.0 + '@radix-ui/react-context@1.1.0(@types/react@18.3.4)(react@18.3.1)': dependencies: react: 18.3.1 diff --git a/ui/src/components/PodcastCard.tsx b/ui/src/components/PodcastCard.tsx index e251ac29..b16872b2 100644 --- a/ui/src/components/PodcastCard.tsx +++ b/ui/src/components/PodcastCard.tsx @@ -1,44 +1,101 @@ -import { createRef, FC } from 'react' -import { Link } from 'react-router-dom' +import {createRef, FC, useState} from 'react' +import {Link} from 'react-router-dom' import axios from 'axios' import {prependAPIKeyOnAuthEnabled} from '../utils/Utilities' -import useCommon, { Podcast } from '../store/CommonSlice' +import useCommon, {Podcast} from '../store/CommonSlice' import 'material-symbols/outlined.css' +import * as Context from '@radix-ui/react-context-menu' +import {ContextMenu} from "@radix-ui/react-context-menu"; +import {CustomInput} from "./CustomInput"; +import {PlusIcon} from "../icons/PlusIcon"; +import {PodcastTags} from "../models/PodcastTags"; type PodcastCardProps = { podcast: Podcast } -export const PodcastCard: FC = ({ podcast }) => { +export const PodcastCard: FC = ({podcast}) => { const likeButton = createRef() const updateLikePodcast = useCommon(state => state.updateLikePodcast) - + const tags = useCommon(state=>state.tags) + const setTags = useCommon(state=>state.setPodcastTags) const likePodcast = () => { - axios.put( '/podcast/favored', { + axios.put('/podcast/favored', { id: podcast.id, favored: !podcast.favorites }) } + const [newTag, setNewTag] = useState('') return ( - -
- + { + + }}> + + +
+ + + { + // Prevent icon click from triggering link to podcast detail + e.preventDefault() - { - // Prevent icon click from triggering link to podcast detail + likeButton.current?.classList.toggle('fill-amber-400') + likePodcast() + updateLikePodcast(podcast.id) + }}>favorite +
+ +
+ {podcast.name} + {podcast.author} +
+ +
+ + { e.preventDefault() + }}> +

Tags

+
+ { + tags.map(t=>{ + return { + e.preventDefault() + }} className="group text-[13px] leading-none text-violet11 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-violet9 data-[highlighted]:text-violet1 text-white"> + {t.name} + + }) + } + + + { + if(tags.map(t=>t.name).includes(newTag)||!newTag.trim()) { + return + } + const newTags: PodcastTags[] = [...tags, { + name: newTag, + color: "ffff", + id: "test123", + username: 'test', + created_at: "123123", + description: "ยง123123" + }] - likeButton.current?.classList.toggle('fill-amber-400') - likePodcast() - updateLikePodcast(podcast.id) - }}>favorite -
- -
- {podcast.name} - {podcast.author} -
- + setTags(newTags) + }}/> + { + setNewTag(event.target.value) + }}/> + + + + ) } diff --git a/ui/src/components/PodcastSettingsModal.tsx b/ui/src/components/PodcastSettingsModal.tsx index d64c1f4c..0991c21b 100644 --- a/ui/src/components/PodcastSettingsModal.tsx +++ b/ui/src/components/PodcastSettingsModal.tsx @@ -30,6 +30,8 @@ export const PodcastSettingsModal:FC = ({setOpen,open }, [podcastSettings, loaded]) + console.log(podcast) + useEffect(() => { axios.get("/podcasts/"+podcast.id+"/settings").then((res) => { if (res != null) { diff --git a/ui/src/icons/PlusIcon.tsx b/ui/src/icons/PlusIcon.tsx index 84782e5b..75bc1875 100644 --- a/ui/src/icons/PlusIcon.tsx +++ b/ui/src/icons/PlusIcon.tsx @@ -1,9 +1,10 @@ import {FC} from "react"; type PlusIconProps = { - className?: string + className?: string, + onClick?: ()=>void } -export const PlusIcon:FC = (className) => { - return +export const PlusIcon:FC = ({className, onClick}) => { + return } diff --git a/ui/src/models/PodcastTags.tsx b/ui/src/models/PodcastTags.tsx new file mode 100644 index 00000000..453a90fa --- /dev/null +++ b/ui/src/models/PodcastTags.tsx @@ -0,0 +1,8 @@ +export type PodcastTags = { + id: string, + name: string, + username: string, + description?: string, + created_at: string, + color: string +} \ No newline at end of file diff --git a/ui/src/pages/Podcasts.tsx b/ui/src/pages/Podcasts.tsx index a74f3842..ce2da180 100644 --- a/ui/src/pages/Podcasts.tsx +++ b/ui/src/pages/Podcasts.tsx @@ -20,6 +20,7 @@ import { Heading1 } from '../components/Heading1' import { PodcastCard } from '../components/PodcastCard' import 'material-symbols/outlined.css' import useModal from "../store/ModalSlice"; +import {PodcastTags} from "../models/PodcastTags"; interface PodcastsProps { onlyFavorites?: boolean @@ -40,15 +41,25 @@ export const Podcasts: FC = ({ onlyFavorites }) => { const setModalOpen = useModal(state => state.setOpenModal) const setFilters = useCommon(state => state.setFilters) const setPodcasts = useCommon(state => state.setPodcasts) + const tags = useCommon(state=>state.tags) + const setTags = useCommon(state=>state.setPodcastTags) const memorizedSelection = useMemo(() => { return JSON.stringify({sorting: filters?.filter?.toUpperCase(), ascending: filters?.ascending}) }, [filters]) const refreshAllPodcasts = () => { - axios.post( '/podcast/all') + axios.post('/podcast/all') } + + useEffect(() => { + axios.get('/tags') + .then((c: AxiosResponse)=>{ + setTags(c.data) + }) + }, []); + const performFilter = () => { if (filters === undefined) { return diff --git a/ui/src/store/CommonSlice.ts b/ui/src/store/CommonSlice.ts index 024dde11..45e5f597 100644 --- a/ui/src/store/CommonSlice.ts +++ b/ui/src/store/CommonSlice.ts @@ -11,6 +11,7 @@ import {EpisodesWithOptionalTimeline} from "../models/EpisodesWithOptionalTimeli import {PodcastWatchedModel} from "../models/PodcastWatchedModel"; import {create} from "zustand"; import {Episode} from "../models/Episode"; +import {PodcastTags} from "../models/PodcastTags"; export type Podcast = { directory: string, @@ -87,6 +88,8 @@ interface CommonProps { podcastEpisodeAlreadyPlayed: PodcastEpisodeWithPodcastWatchModel|undefined, setSidebarCollapsed: (sidebarCollapsed: boolean) => void, setPodcasts: (podcasts: Podcast[]) => void, + tags: PodcastTags[], + setPodcastTags: (t: PodcastTags[])=>void, updateLikePodcast: (id: number) => void, setSelectedEpisodes: (selectedEpisodes: EpisodesWithOptionalTimeline[]) => void, setSearchedPodcasts: (searchedPodcasts: AgnosticPodcastDataModel[]) => void, @@ -224,7 +227,9 @@ const useCommon = create((set, get) => ({ setPodcastAlreadyPlayed: (podcastAlreadyPlayed: boolean) => set({podcastAlreadyPlayed}), setPodcastEpisodeAlreadyPlayed: (podcastEpisodeAlreadyPlayed: PodcastEpisodeWithPodcastWatchModel) => set({podcastEpisodeAlreadyPlayed}), setLoggedInUser: (loggedInUser: LoggedInUser) => set({loggedInUser}), - loggedInUser: undefined + loggedInUser: undefined, + tags: [], + setPodcastTags: (t)=>set({tags: t}) })) export default useCommon