From d4894eadd5c56ae7dc6d9d1abd6ae1cbb13a6dac Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 17 Feb 2025 12:07:51 -0800 Subject: [PATCH 1/6] feat: add support for selecting a playlist --- .editorconfig | 2 +- .../ts/components/playlist-selection.tsx | 38 +++++++++++++++++++ src/assets/ts/components/proposal.tsx | 15 +++++++- src/assets/ts/components/sign-in.tsx | 2 +- src/assets/ts/features/popup-slice.ts | 33 ++++++++++++++++ src/assets/ts/main.ts | 35 ++++++++++++++++- 6 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/assets/ts/components/playlist-selection.tsx diff --git a/.editorconfig b/.editorconfig index 3a551cb..90e7875 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,7 @@ root = true end_of_line = lf insert_final_newline = true -[*.{js,mjs,jsx,html}] +[*.{js,ts,mjs,jsx,tsx,html}] indent_style = space indent_size = 2 diff --git a/src/assets/ts/components/playlist-selection.tsx b/src/assets/ts/components/playlist-selection.tsx new file mode 100644 index 0000000..9920d23 --- /dev/null +++ b/src/assets/ts/components/playlist-selection.tsx @@ -0,0 +1,38 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppDispatch, RootState } from '@/store'; +import { fetchPlaylists, setSelectedPlaylist } from '@/features/popup-slice'; + +interface Playlist { + id: string; + name: string; +} + +export const PlaylistSelection: React.FC = () => { + const dispatch = useDispatch(); + const playlists = useSelector((state: RootState) => state.popup.playlists); + const selectedPlaylistId = useSelector((state: RootState) => state.popup.selectedPlaylistId); + + useEffect(() => { + dispatch(fetchPlaylists()); + }, [dispatch]); + + return ( + <> + +

Select the playlist you want to add this asset to.

+ + + ); +}; diff --git a/src/assets/ts/components/proposal.tsx b/src/assets/ts/components/proposal.tsx index 8582e56..a5f489c 100644 --- a/src/assets/ts/components/proposal.tsx +++ b/src/assets/ts/components/proposal.tsx @@ -1,12 +1,15 @@ /* global browser */ import { useEffect, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import type { User } from '@/main'; +import { addAssetToPlaylist, waitForAssetToBeReady } from '@/main'; +import { RootState } from '@/store'; import { PopupSpinner } from '@/components/popup-spinner'; import { SaveAuthWarning } from '@/components/save-auth-warning'; import { SaveAuthHelp } from '@/components/save-auth-help'; +import { PlaylistSelection } from '@/components/playlist-selection'; import * as cookiejs from '@/vendor/cookie.mjs'; import { @@ -57,6 +60,7 @@ interface ApiError { export const Proposal: React.FC = () => { const dispatch = useDispatch(); + const selectedPlaylistId = useSelector((state: RootState) => state.popup.selectedPlaylistId); const [isLoading, setIsLoading] = useState(false); const [assetTitle, setAssetTitle] = useState(''); const [assetUrl, setAssetUrl] = useState(''); @@ -255,6 +259,11 @@ export const Proposal: React.FC = () => { throw new Error('No asset data returned'); } + if (selectedPlaylistId) { + await waitForAssetToBeReady(result[0].id, proposal.user); + addAssetToPlaylist(result[0].id, selectedPlaylistId, proposal.user.token); + } + State.setSavedAssetState( proposal.url, result[0].id, @@ -368,6 +377,10 @@ export const Proposal: React.FC = () => { +
+ +
+
); -}; \ No newline at end of file +}; diff --git a/src/assets/ts/features/popup-slice.ts b/src/assets/ts/features/popup-slice.ts index 69581cf..5ccf10d 100644 --- a/src/assets/ts/features/popup-slice.ts +++ b/src/assets/ts/features/popup-slice.ts @@ -4,6 +4,7 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { getPlaylists } from '@/main'; export const signIn = createAsyncThunk( 'popup/signIn', @@ -23,6 +24,18 @@ export const signOut = createAsyncThunk( } ); +export const fetchPlaylists = createAsyncThunk( + 'popup/fetchPlaylists', + async () => { + const result = await browser.storage.sync.get('token'); + if (result.token) { + const playlists = await getPlaylists(result.token); + return playlists; + } + return []; + } +); + const popupSlice = createSlice({ name: 'popup', initialState: { @@ -32,6 +45,13 @@ const popupSlice = createSlice({ showSignInSuccess: false, assetDashboardLink: '', showSettings: false, + playlists: [ + { + id: '', + title: 'None', + } + ], + selectedPlaylistId: '', }, reducers: { notifyAssetSaveSuccess: (state) => { @@ -46,6 +66,9 @@ const popupSlice = createSlice({ state.showSettings = true; state.showProposal = false; }, + setSelectedPlaylist: (state, action) => { + state.selectedPlaylistId = action.payload; + }, }, extraReducers: (builder) => { builder @@ -58,6 +81,15 @@ const popupSlice = createSlice({ .addCase(signOut.fulfilled, (state) => { state.showSettings = false; state.showSignIn = true; + }) + .addCase(fetchPlaylists.fulfilled, (state, action) => { + state.playlists = [ + { + id: '', + title: 'None', + }, + ...action.payload, + ]; }); }, }); @@ -66,5 +98,6 @@ export const { notifyAssetSaveSuccess, notifySignInSuccess, openSettings, + setSelectedPlaylist, } = popupSlice.actions; export default popupSlice.reducer; diff --git a/src/assets/ts/main.ts b/src/assets/ts/main.ts index 8fdf196..296e03f 100644 --- a/src/assets/ts/main.ts +++ b/src/assets/ts/main.ts @@ -110,14 +110,47 @@ export function updateWebAsset( } export function getWebAsset(assetId: string | null, user: User) { + const queryParams = `id=eq.${encodeURIComponent(assetId || '')}`; return callApi( 'GET', - `https://api.screenlyapp.com/api/v4/assets/${encodeURIComponent(assetId || '')}/`, + `https://api.screenlyapp.com/api/v4/assets/?${queryParams}`, null, user.token ) } +export function getPlaylists(token: string) { + return callApi( + 'GET', + 'https://api.screenlyapp.com/api/v4/playlists/', + null, + token + ); +} + +export async function waitForAssetToBeReady(assetId: string, user: User) { + const readyStates = ['downloading', 'processing', 'finished']; + do { + const assetResult = await getWebAsset(assetId, user); + const asset = assetResult[0]; + if (readyStates.includes(asset.status)) { + break; + } + } while (true); +} + +export function addAssetToPlaylist(assetId: string, playlistId: string, token: string) { + return callApi( + 'POST', + `https://api.screenlyapp.com/api/v4/playlist-items/`, + { + 'asset_id': assetId, + 'playlist_id': playlistId, + }, + token + ); +} + export function getAssetDashboardLink(assetId: string) { return `https://login.screenlyapp.com/login?next=/manage/assets/${assetId}`; } From 2b120c1da61f8a588049e81b06d56859f8d82a98 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 17 Feb 2025 15:33:54 -0800 Subject: [PATCH 2/6] fix: only add an asset to a playlist if it doesn't exist yet --- src/assets/ts/components/proposal.tsx | 2 +- src/assets/ts/main.ts | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/assets/ts/components/proposal.tsx b/src/assets/ts/components/proposal.tsx index a5f489c..4ff10ed 100644 --- a/src/assets/ts/components/proposal.tsx +++ b/src/assets/ts/components/proposal.tsx @@ -261,7 +261,7 @@ export const Proposal: React.FC = () => { if (selectedPlaylistId) { await waitForAssetToBeReady(result[0].id, proposal.user); - addAssetToPlaylist(result[0].id, selectedPlaylistId, proposal.user.token); + await addAssetToPlaylist(result[0].id, selectedPlaylistId, proposal.user); } State.setSavedAssetState( diff --git a/src/assets/ts/main.ts b/src/assets/ts/main.ts index 296e03f..54b5c06 100644 --- a/src/assets/ts/main.ts +++ b/src/assets/ts/main.ts @@ -139,7 +139,22 @@ export async function waitForAssetToBeReady(assetId: string, user: User) { } while (true); } -export function addAssetToPlaylist(assetId: string, playlistId: string, token: string) { +export async function getPlaylistItems(assetId: string, playlistId: string, user: User) { + const queryParams = `asset_id=eq.${encodeURIComponent(assetId || '')}&playlist_id=eq.${encodeURIComponent(playlistId || '')}`; + return callApi( + 'GET', + `https://api.screenlyapp.com/api/v4/playlist-items/?${queryParams}`, + null, + user.token + ); +} + +export async function addAssetToPlaylist(assetId: string, playlistId: string, user: User) { + const playlistItems = await getPlaylistItems(assetId, playlistId, user); + if (playlistItems.length > 0) { + return; + } + return callApi( 'POST', `https://api.screenlyapp.com/api/v4/playlist-items/`, @@ -147,7 +162,7 @@ export function addAssetToPlaylist(assetId: string, playlistId: string, token: s 'asset_id': assetId, 'playlist_id': playlistId, }, - token + user.token ); } From ac7f393912524418f688b2b96122f9e4a3bb741e Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 17 Feb 2025 16:09:03 -0800 Subject: [PATCH 3/6] chore: refactor code and resolve linting errors --- src/assets/ts/components/playlist-selection.tsx | 5 ----- src/assets/ts/main.ts | 8 +++----- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/assets/ts/components/playlist-selection.tsx b/src/assets/ts/components/playlist-selection.tsx index 9920d23..d29390f 100644 --- a/src/assets/ts/components/playlist-selection.tsx +++ b/src/assets/ts/components/playlist-selection.tsx @@ -3,11 +3,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { AppDispatch, RootState } from '@/store'; import { fetchPlaylists, setSelectedPlaylist } from '@/features/popup-slice'; -interface Playlist { - id: string; - name: string; -} - export const PlaylistSelection: React.FC = () => { const dispatch = useDispatch(); const playlists = useSelector((state: RootState) => state.popup.playlists); diff --git a/src/assets/ts/main.ts b/src/assets/ts/main.ts index 54b5c06..6561748 100644 --- a/src/assets/ts/main.ts +++ b/src/assets/ts/main.ts @@ -130,13 +130,11 @@ export function getPlaylists(token: string) { export async function waitForAssetToBeReady(assetId: string, user: User) { const readyStates = ['downloading', 'processing', 'finished']; + let asset; do { const assetResult = await getWebAsset(assetId, user); - const asset = assetResult[0]; - if (readyStates.includes(asset.status)) { - break; - } - } while (true); + asset = assetResult[0]; + } while (!readyStates.includes(asset.status)); } export async function getPlaylistItems(assetId: string, playlistId: string, user: User) { From b548eb74779f10be25d5311e80daaf1e9e1ffa1e Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Tue, 18 Feb 2025 19:00:10 -0800 Subject: [PATCH 4/6] fix: add support for selecting more than one playlist --- .../ts/components/playlist-selection.tsx | 47 ++++++++++----- src/assets/ts/components/proposal.tsx | 8 ++- src/assets/ts/features/popup-slice.ts | 60 +++++++++++-------- 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/src/assets/ts/components/playlist-selection.tsx b/src/assets/ts/components/playlist-selection.tsx index d29390f..7624d6e 100644 --- a/src/assets/ts/components/playlist-selection.tsx +++ b/src/assets/ts/components/playlist-selection.tsx @@ -1,33 +1,52 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { AppDispatch, RootState } from '@/store'; -import { fetchPlaylists, setSelectedPlaylist } from '@/features/popup-slice'; +import { fetchPlaylists, setSelectedPlaylists } from '@/features/popup-slice'; + +interface Playlist { + id: string; + title: string; +} export const PlaylistSelection: React.FC = () => { const dispatch = useDispatch(); const playlists = useSelector((state: RootState) => state.popup.playlists); - const selectedPlaylistId = useSelector((state: RootState) => state.popup.selectedPlaylistId); + const selectedPlaylistIds = useSelector((state: RootState) => state.popup.selectedPlaylistIds); useEffect(() => { dispatch(fetchPlaylists()); }, [dispatch]); + const handlePlaylistChange = (playlistId: string) => { + const updatedSelection = selectedPlaylistIds.includes(playlistId) + ? selectedPlaylistIds.filter((id: string) => id !== playlistId) + : [...selectedPlaylistIds, playlistId]; + dispatch(setSelectedPlaylists(updatedSelection)); + }; + return ( <> -

Select the playlist you want to add this asset to.

- handlePlaylistChange(playlist.id)} + /> + + ))} - + ); }; diff --git a/src/assets/ts/components/proposal.tsx b/src/assets/ts/components/proposal.tsx index 4ff10ed..97c2c38 100644 --- a/src/assets/ts/components/proposal.tsx +++ b/src/assets/ts/components/proposal.tsx @@ -60,7 +60,7 @@ interface ApiError { export const Proposal: React.FC = () => { const dispatch = useDispatch(); - const selectedPlaylistId = useSelector((state: RootState) => state.popup.selectedPlaylistId); + const selectedPlaylistIds = useSelector((state: RootState) => state.popup.selectedPlaylistIds); const [isLoading, setIsLoading] = useState(false); const [assetTitle, setAssetTitle] = useState(''); const [assetUrl, setAssetUrl] = useState(''); @@ -259,9 +259,11 @@ export const Proposal: React.FC = () => { throw new Error('No asset data returned'); } - if (selectedPlaylistId) { + if (selectedPlaylistIds.length > 0) { await waitForAssetToBeReady(result[0].id, proposal.user); - await addAssetToPlaylist(result[0].id, selectedPlaylistId, proposal.user); + await Promise.all(selectedPlaylistIds.map(playlistId => + addAssetToPlaylist(result[0].id, playlistId, proposal.user) + )); } State.setSavedAssetState( diff --git a/src/assets/ts/features/popup-slice.ts b/src/assets/ts/features/popup-slice.ts index 5ccf10d..722d0e8 100644 --- a/src/assets/ts/features/popup-slice.ts +++ b/src/assets/ts/features/popup-slice.ts @@ -2,10 +2,38 @@ import { createAsyncThunk, - createSlice + createSlice, + PayloadAction } from '@reduxjs/toolkit'; import { getPlaylists } from '@/main'; +interface Playlist { + id: string; + title: string; +} + +interface PopupState { + showSignIn: boolean; + showProposal: boolean; + showSuccess: boolean; + showSignInSuccess: boolean; + assetDashboardLink: string; + showSettings: boolean; + playlists: Playlist[]; + selectedPlaylistIds: string[]; +} + +const initialState: PopupState = { + showSignIn: true, + showProposal: false, + showSuccess: false, + showSignInSuccess: false, + assetDashboardLink: '', + showSettings: false, + playlists: [], + selectedPlaylistIds: [], +}; + export const signIn = createAsyncThunk( 'popup/signIn', async () => { @@ -38,21 +66,7 @@ export const fetchPlaylists = createAsyncThunk( const popupSlice = createSlice({ name: 'popup', - initialState: { - showSignIn: true, - showProposal: false, - showSuccess: false, - showSignInSuccess: false, - assetDashboardLink: '', - showSettings: false, - playlists: [ - { - id: '', - title: 'None', - } - ], - selectedPlaylistId: '', - }, + initialState, reducers: { notifyAssetSaveSuccess: (state) => { state.showSuccess = true; @@ -66,13 +80,13 @@ const popupSlice = createSlice({ state.showSettings = true; state.showProposal = false; }, - setSelectedPlaylist: (state, action) => { - state.selectedPlaylistId = action.payload; + setSelectedPlaylists: (state, action: PayloadAction) => { + state.selectedPlaylistIds = action.payload; }, }, extraReducers: (builder) => { builder - .addCase(signIn.fulfilled, (state, action) => { + .addCase(signIn.fulfilled, (state, action: PayloadAction) => { if (action.payload) { state.showSignIn = false; state.showProposal = true; @@ -82,12 +96,8 @@ const popupSlice = createSlice({ state.showSettings = false; state.showSignIn = true; }) - .addCase(fetchPlaylists.fulfilled, (state, action) => { + .addCase(fetchPlaylists.fulfilled, (state, action: PayloadAction) => { state.playlists = [ - { - id: '', - title: 'None', - }, ...action.payload, ]; }); @@ -98,6 +108,6 @@ export const { notifyAssetSaveSuccess, notifySignInSuccess, openSettings, - setSelectedPlaylist, + setSelectedPlaylists, } = popupSlice.actions; export default popupSlice.reducer; From 358cec45d6e902cb1a475fe6851b4e38d84a6368 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Tue, 18 Feb 2025 20:13:30 -0800 Subject: [PATCH 5/6] chore: set indentation size to 2 spaces --- .../ts/components/playlist-selection.tsx | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/assets/ts/components/playlist-selection.tsx b/src/assets/ts/components/playlist-selection.tsx index 7624d6e..db75e6f 100644 --- a/src/assets/ts/components/playlist-selection.tsx +++ b/src/assets/ts/components/playlist-selection.tsx @@ -4,49 +4,49 @@ import { AppDispatch, RootState } from '@/store'; import { fetchPlaylists, setSelectedPlaylists } from '@/features/popup-slice'; interface Playlist { - id: string; - title: string; + id: string; + title: string; } export const PlaylistSelection: React.FC = () => { - const dispatch = useDispatch(); - const playlists = useSelector((state: RootState) => state.popup.playlists); - const selectedPlaylistIds = useSelector((state: RootState) => state.popup.selectedPlaylistIds); + const dispatch = useDispatch(); + const playlists = useSelector((state: RootState) => state.popup.playlists); + const selectedPlaylistIds = useSelector((state: RootState) => state.popup.selectedPlaylistIds); - useEffect(() => { - dispatch(fetchPlaylists()); - }, [dispatch]); + useEffect(() => { + dispatch(fetchPlaylists()); + }, [dispatch]); - const handlePlaylistChange = (playlistId: string) => { - const updatedSelection = selectedPlaylistIds.includes(playlistId) - ? selectedPlaylistIds.filter((id: string) => id !== playlistId) - : [...selectedPlaylistIds, playlistId]; - dispatch(setSelectedPlaylists(updatedSelection)); - }; + const handlePlaylistChange = (playlistId: string) => { + const updatedSelection = selectedPlaylistIds.includes(playlistId) + ? selectedPlaylistIds.filter((id: string) => id !== playlistId) + : [...selectedPlaylistIds, playlistId]; + dispatch(setSelectedPlaylists(updatedSelection)); + }; - return ( - <> - -

Select the playlists you want to add this asset to.

-
- {playlists.map((playlist: Playlist) => ( -
- handlePlaylistChange(playlist.id)} - /> - -
- ))} -
- - ); + return ( + <> + +

Select the playlists you want to add this asset to.

+
+ {playlists.map((playlist: Playlist) => ( +
+ handlePlaylistChange(playlist.id)} + /> + +
+ ))} +
+ + ); }; From 8a0513d2d02b392abe969696ba427a5b6123b6c1 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Tue, 18 Feb 2025 21:26:10 -0800 Subject: [PATCH 6/6] fix: have the playlist checkboxes depend on backend data --- src/assets/ts/components/proposal.tsx | 10 +++++++++- src/assets/ts/main.ts | 13 +++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/assets/ts/components/proposal.tsx b/src/assets/ts/components/proposal.tsx index 97c2c38..ce947a2 100644 --- a/src/assets/ts/components/proposal.tsx +++ b/src/assets/ts/components/proposal.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import type { User } from '@/main'; -import { addAssetToPlaylist, waitForAssetToBeReady } from '@/main'; +import { addAssetToPlaylist, waitForAssetToBeReady, getPlaylistItems } from '@/main'; import { RootState } from '@/store'; import { PopupSpinner } from '@/components/popup-spinner'; @@ -24,6 +24,7 @@ import { import { notifyAssetSaveSuccess, openSettings, + setSelectedPlaylists, } from '@/features/popup-slice'; interface ErrorState { @@ -58,6 +59,10 @@ interface ApiError { json(): Promise; } +interface PlaylistItem { + playlist_id: string; +} + export const Proposal: React.FC = () => { const dispatch = useDispatch(); const selectedPlaylistIds = useSelector((state: RootState) => state.popup.selectedPlaylistIds); @@ -109,6 +114,9 @@ export const Proposal: React.FC = () => { if (currentProposal.state) { setSaveAuthentication(currentProposal.state.withCookies); + const playlistItems = await getPlaylistItems(currentProposal.user, currentProposal.state.assetId ?? undefined); + const playlistIds = playlistItems.map((item: PlaylistItem) => item.playlist_id); + dispatch(setSelectedPlaylists(playlistIds)); setButtonState('update'); } else { setButtonState('add'); diff --git a/src/assets/ts/main.ts b/src/assets/ts/main.ts index 6561748..3dce788 100644 --- a/src/assets/ts/main.ts +++ b/src/assets/ts/main.ts @@ -137,18 +137,23 @@ export async function waitForAssetToBeReady(assetId: string, user: User) { } while (!readyStates.includes(asset.status)); } -export async function getPlaylistItems(assetId: string, playlistId: string, user: User) { - const queryParams = `asset_id=eq.${encodeURIComponent(assetId || '')}&playlist_id=eq.${encodeURIComponent(playlistId || '')}`; +export async function getPlaylistItems(user: User, assetId?: string, playlistId?: string) { + const queryParams = [ + assetId && `asset_id=eq.${encodeURIComponent(assetId)}`, + playlistId && `playlist_id=eq.${encodeURIComponent(playlistId)}` + ].filter(Boolean).join('&'); + + const url = `https://api.screenlyapp.com/api/v4/playlist-items/${queryParams ? `?${queryParams}` : ''}`; return callApi( 'GET', - `https://api.screenlyapp.com/api/v4/playlist-items/?${queryParams}`, + url, null, user.token ); } export async function addAssetToPlaylist(assetId: string, playlistId: string, user: User) { - const playlistItems = await getPlaylistItems(assetId, playlistId, user); + const playlistItems = await getPlaylistItems(user, assetId, playlistId); if (playlistItems.length > 0) { return; }