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..db75e6f --- /dev/null +++ b/src/assets/ts/components/playlist-selection.tsx @@ -0,0 +1,52 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppDispatch, RootState } from '@/store'; +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 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 playlists you want to add this asset to.

+
+ {playlists.map((playlist: Playlist) => ( +
+ handlePlaylistChange(playlist.id)} + /> + +
+ ))} +
+ + ); +}; diff --git a/src/assets/ts/components/proposal.tsx b/src/assets/ts/components/proposal.tsx index 8582e56..ce947a2 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, getPlaylistItems } 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 { @@ -21,6 +24,7 @@ import { import { notifyAssetSaveSuccess, openSettings, + setSelectedPlaylists, } from '@/features/popup-slice'; interface ErrorState { @@ -55,8 +59,13 @@ 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); const [isLoading, setIsLoading] = useState(false); const [assetTitle, setAssetTitle] = useState(''); const [assetUrl, setAssetUrl] = useState(''); @@ -105,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'); @@ -255,6 +267,13 @@ export const Proposal: React.FC = () => { throw new Error('No asset data returned'); } + if (selectedPlaylistIds.length > 0) { + await waitForAssetToBeReady(result[0].id, proposal.user); + await Promise.all(selectedPlaylistIds.map(playlistId => + addAssetToPlaylist(result[0].id, playlistId, proposal.user) + )); + } + State.setSavedAssetState( proposal.url, result[0].id, @@ -368,6 +387,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..722d0e8 100644 --- a/src/assets/ts/features/popup-slice.ts +++ b/src/assets/ts/features/popup-slice.ts @@ -2,8 +2,37 @@ 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', @@ -23,16 +52,21 @@ 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: { - showSignIn: true, - showProposal: false, - showSuccess: false, - showSignInSuccess: false, - assetDashboardLink: '', - showSettings: false, - }, + initialState, reducers: { notifyAssetSaveSuccess: (state) => { state.showSuccess = true; @@ -46,10 +80,13 @@ const popupSlice = createSlice({ state.showSettings = true; state.showProposal = false; }, + 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; @@ -58,6 +95,11 @@ const popupSlice = createSlice({ .addCase(signOut.fulfilled, (state) => { state.showSettings = false; state.showSignIn = true; + }) + .addCase(fetchPlaylists.fulfilled, (state, action: PayloadAction) => { + state.playlists = [ + ...action.payload, + ]; }); }, }); @@ -66,5 +108,6 @@ export const { notifyAssetSaveSuccess, notifySignInSuccess, openSettings, + setSelectedPlaylists, } = popupSlice.actions; export default popupSlice.reducer; diff --git a/src/assets/ts/main.ts b/src/assets/ts/main.ts index 8fdf196..3dce788 100644 --- a/src/assets/ts/main.ts +++ b/src/assets/ts/main.ts @@ -110,14 +110,65 @@ 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']; + let asset; + do { + const assetResult = await getWebAsset(assetId, user); + asset = assetResult[0]; + } while (!readyStates.includes(asset.status)); +} + +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', + url, + null, + user.token + ); +} + +export async function addAssetToPlaylist(assetId: string, playlistId: string, user: User) { + const playlistItems = await getPlaylistItems(user, assetId, playlistId); + if (playlistItems.length > 0) { + return; + } + + return callApi( + 'POST', + `https://api.screenlyapp.com/api/v4/playlist-items/`, + { + 'asset_id': assetId, + 'playlist_id': playlistId, + }, + user.token + ); +} + export function getAssetDashboardLink(assetId: string) { return `https://login.screenlyapp.com/login?next=/manage/assets/${assetId}`; }