Ignore:
Timestamp:
02/01/26 17:29:49 (5 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
0792d02
Parents:
2730163
Message:

add song/album to favourites

Location:
frontend/src/components/userProfile
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • frontend/src/components/userProfile/ArtistView.tsx

    r2730163 rd2af1a6  
    11import { useNavigate } from "react-router-dom";
    2 import { Music, Disc3 } from "lucide-react";
     2import { useState } from "react";
     3import { Music, Disc3, Play, Plus } from "lucide-react";
    34import type { ArtistContribution } from "../../utils/types";
     5import axiosInstance from "../../api/axiosInstance";
    46
    57interface ArtistViewProps {
     
    911const ArtistView = ({ contributions }: ArtistViewProps) => {
    1012  const navigate = useNavigate();
    11 
    12   const albums = contributions.filter((c) => c.entityType === "ALBUM");
    13   const songs = contributions.filter((c) => c.entityType === "SONG");
     13  const [items, setItems] = useState(contributions);
     14  const [toast, setToast] = useState<{ message: string; show: boolean }>({
     15    message: "",
     16    show: false,
     17  });
     18
     19  const albums = items.filter((c) => c.entityType === "ALBUM");
     20  const songs = items.filter((c) => c.entityType === "SONG");
    1421
    1522  const getRoleColor = (role: string) => {
    1623    const colors: { [key: string]: string } = {
    17       COMPOSER: "bg-purple-500",
    18       PERFORMER: "bg-blue-500",
    19       PRODUCER: "bg-green-500",
    20       MAIN_VOCAL: "bg-pink-500",
     24      COMPOSER: "bg-purple-100 text-purple-700",
     25      PERFORMER: "bg-blue-100 text-blue-700",
     26      PRODUCER: "bg-green-100 text-green-700",
     27      MAIN_VOCAL: "bg-pink-100 text-pink-700",
    2128    };
    22     return colors[role] || "bg-gray-500";
     29    return colors[role] || "bg-gray-100 text-gray-700";
     30  };
     31
     32  const showToast = (message: string) => {
     33    setToast({ message, show: true });
     34    setTimeout(() => {
     35      setToast({ message: "", show: false });
     36    }, 2000);
     37  };
     38
     39  const handleLike = async (id: number, title: string) => {
     40    try {
     41      const response = await axiosInstance.post(`/musical-entity/${id}/like`);
     42      const data = response.data;
     43
     44      setItems((prevItems) =>
     45        prevItems.map((item) =>
     46          item.id === data.entityId
     47            ? { ...item, isLikedByCurrentUser: data.isLiked }
     48            : item,
     49        ),
     50      );
     51
     52      showToast(
     53        data.isLiked ? `Liked "${title}"` : `Removed ${title} from likes`,
     54      );
     55    } catch (err: any) {
     56      showToast(err.response?.data?.error);
     57    }
    2358  };
    2459
    2560  return (
    2661    <div className="mt-8">
     62      {toast.show && (
     63        <div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 animate-fade-in-up">
     64          <div className="bg-gray-900 text-white px-6 py-3 rounded-full shadow-lg text-sm font-medium">
     65            {toast.message}
     66          </div>
     67        </div>
     68      )}
     69
    2770      {albums.length > 0 && (
    2871        <div className="mb-12">
    2972          <div className="flex items-center gap-3 mb-6">
    30             <Disc3 className="w-8 h-8 text-purple-600" />
    31             <h2 className="text-3xl font-bold">Albums</h2>
     73            <Disc3 className="w-6 h-6 text-gray-700" />
     74            <h2 className="text-2xl font-bold text-gray-800">Albums</h2>
    3275          </div>
    3376          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    34             {albums.map((album, index) => (
     77            {albums.map((album) => (
    3578              <div
    36                 key={index}
     79                key={album.id}
    3780                className="group cursor-pointer"
    38                 onClick={() =>
    39                   navigate(`/collection/album/${album.musicalEntityId}`)
    40                 }
     81                onClick={() => navigate(`/collection/album/${album.id}`)}
    4182              >
    42                 <div className="relative aspect-square bg-linear-to-br from-purple-400 via-pink-400 to-blue-400 rounded-lg mb-3 overflow-hidden shadow-lg group-hover:shadow-2xl transition-all duration-300 group-hover:scale-105">
     83                <div className="relative aspect-square bg-gradient-to-br from-blue-400 to-purple-500 rounded-lg mb-3 overflow-hidden shadow-md group-hover:shadow-lg transition-all duration-300">
    4384                  <div className="absolute inset-0 flex items-center justify-center">
    44                     <Disc3 className="w-20 h-20 text-white opacity-40" />
    45                   </div>
    46                   <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/60 to-transparent p-4">
     85                    <Disc3 className="w-16 h-16 text-white opacity-30" />
     86                  </div>
     87
     88                  <button
     89                    className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer"
     90                    title={album.isLikedByCurrentUser ? "Unlike" : "Like"}
     91                    onClick={(e) => {
     92                      e.stopPropagation();
     93                      handleLike(album.id, album.title);
     94                    }}
     95                  >
     96                    <svg
     97                      className="w-5 h-5"
     98                      fill={album.isLikedByCurrentUser ? "#ef4444" : "none"}
     99                      stroke={
     100                        album.isLikedByCurrentUser ? "#ef4444" : "#6b7280"
     101                      }
     102                      strokeWidth="2"
     103                      viewBox="0 0 24 24"
     104                    >
     105                      <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
     106                    </svg>
     107                  </button>
     108
     109                  <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-3">
    47110                    <span
    48                       className={`text-xs px-2 py-1 rounded-full text-white ${getRoleColor(album.role)}`}
     111                      className={`text-xs px-2 py-1 rounded-full font-medium ${getRoleColor(album.role)}`}
    49112                    >
    50113                      {album.role.replace("_", " ")}
     
    52115                  </div>
    53116                </div>
    54                 <h3 className="font-semibold text-base line-clamp-2 group-hover:text-blue-600 transition-colors">
     117                <h3 className="font-semibold text-sm line-clamp-2 text-gray-900 group-hover:text-blue-600 transition-colors">
    55118                  {album.title}
    56119                </h3>
     
    64127        <div className="mb-12">
    65128          <div className="flex items-center gap-3 mb-6">
    66             <Music className="w-8 h-8 text-blue-600" />
    67             <h2 className="text-3xl font-bold">Songs</h2>
     129            <Music className="w-6 h-6 text-gray-700" />
     130            <h2 className="text-2xl font-bold text-gray-800">Songs</h2>
    68131          </div>
    69132          <div className="space-y-2">
    70             {songs.map((song, index) => (
     133            {songs.map((song) => (
    71134              <div
    72                 key={index}
    73                 className="group flex items-center gap-4 p-4 rounded-lg hover:bg-gray-50 transition-all cursor-pointer border border-transparent hover:border-gray-200"
    74                 onClick={() =>
    75                   navigate(`/musical-entity/${song.musicalEntityId}`)
    76                 }
     135                key={song.id}
     136                className="group relative flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 transition-all cursor-pointer"
     137                onClick={() => navigate(`/musical-entity/${song.id}`)}
    77138              >
    78                 <div className="shrink-0 w-14 h-14 bg-linear-to-br from-blue-400 to-cyan-400 rounded flex items-center justify-center group-hover:scale-110 transition-transform shadow-md">
    79                   <Music className="w-7 h-7 text-white" />
    80                 </div>
     139                <div className="relative shrink-0">
     140                  <div className="w-12 h-12 bg-gradient-to-br from-blue-400 to-purple-500 rounded flex items-center justify-center shadow-sm">
     141                    <Music className="w-6 h-6 text-white" />
     142                  </div>
     143                  <button
     144                    className="absolute inset-0 bg-black/60 rounded flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200"
     145                    aria-label="Play song"
     146                  >
     147                    <Play className="w-6 h-6 text-white fill-white" />
     148                  </button>
     149                </div>
     150
    81151                <div className="flex-1 min-w-0">
    82                   <h3 className="font-semibold text-lg group-hover:text-blue-600 transition-colors truncate">
     152                  <h3 className="font-semibold text-base text-gray-900 group-hover:text-blue-600 transition-colors truncate">
    83153                    {song.title}
    84154                  </h3>
    85155                  <div className="flex items-center gap-2 mt-1">
    86                     {song.genre}
    87                   </div>
    88                 </div>
     156                    <span className="text-sm text-gray-600">{song.genre}</span>
     157                  </div>
     158                </div>
     159
    89160                <span
    90                   className={`px-4 py-2 rounded-full text-white text-sm font-medium ${getRoleColor(song.role)} shadow-md`}
     161                  className={`px-3 py-1 rounded-full text-sm font-medium ${getRoleColor(song.role)}`}
    91162                >
    92163                  {song.role.replace("_", " ")}
    93164                </span>
     165
     166                <div className="flex items-center gap-2">
     167                  <button
     168                    className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer"
     169                    title="Add to playlist"
     170                  >
     171                    <Plus className="w-5 h-5 text-gray-600" />
     172                  </button>
     173
     174                  <button
     175                    className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer"
     176                    title={song.isLikedByCurrentUser ? "Unlike" : "Like"}
     177                    onClick={(e) => {
     178                      e.stopPropagation();
     179                      handleLike(song.id, song.title);
     180                    }}
     181                  >
     182                    <svg
     183                      className="w-5 h-5"
     184                      fill={song.isLikedByCurrentUser ? "#ef4444" : "none"}
     185                      stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"}
     186                      strokeWidth="2"
     187                      viewBox="0 0 24 24"
     188                    >
     189                      <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
     190                    </svg>
     191                  </button>
     192                </div>
    94193              </div>
    95194            ))}
     
    100199      {contributions.length === 0 && (
    101200        <div className="flex flex-col items-center justify-center py-16 text-gray-400">
    102           <Music className="w-24 h-24 mb-4 opacity-20" />
    103           <p className="text-xl font-medium">No contributions yet</p>
     201          <Music className="w-20 h-20 mb-4 opacity-20" />
     202          <p className="text-lg font-medium">No contributions yet</p>
    104203          <p className="text-sm mt-2">Start creating music to see it here</p>
    105204        </div>
  • frontend/src/components/userProfile/ListenerView.tsx

    r2730163 rd2af1a6  
    11import { useNavigate } from "react-router-dom";
    2 import { Heart, ListMusic, Music, Album } from "lucide-react";
     2import { Heart, ListMusic, Music, Album, Bookmark } from "lucide-react";
    33import type { Playlist, MusicalEntity } from "../../utils/types";
    4 
     4import axiosInstance from "../../api/axiosInstance";
     5import { useState } from "react";
    56interface ListenerViewProps {
    67  likedEntities: MusicalEntity[] | [];
     
    1011const ListenerView = ({ likedEntities, playlists }: ListenerViewProps) => {
    1112  const navigate = useNavigate();
    12 
    13   const likedSongs = likedEntities?.filter((e) => e.type === "SONG");
    14   const likedAlbums = likedEntities?.filter((e) => e.type === "ALBUM");
     13  const [items, setItems] = useState(likedEntities);
     14  const [toast, setToast] = useState<{ message: string; show: boolean }>({
     15    message: "",
     16    show: false,
     17  });
     18
     19  const showToast = (message: string) => {
     20    setToast({ message, show: true });
     21    setTimeout(() => {
     22      setToast({ message: "", show: false });
     23    }, 2000);
     24  };
     25
     26  const likedSongs = items.filter((e) => e.type === "SONG");
     27  const likedAlbums = items.filter((e) => e.type === "ALBUM");
     28
     29  const handleSavePlaylist = (e: React.MouseEvent, playlistId: number) => {
     30    e.stopPropagation();
     31
     32    console.log("Save playlist:", playlistId);
     33  };
     34
     35  const handleLike = async (id: number, title: string) => {
     36    try {
     37      const response = await axiosInstance.post(`/musical-entity/${id}/like`);
     38      const data = response.data;
     39
     40      setItems((prevItems) =>
     41        prevItems.map((item) =>
     42          item.id === data.entityId
     43            ? { ...item, isLikedByCurrentUser: data.isLiked }
     44            : item,
     45        ),
     46      );
     47
     48      showToast(
     49        data.isLiked ? `Liked "${title}"` : `Removed "${title}" from likes`,
     50      );
     51    } catch (err: any) {
     52      showToast(err.response?.data?.error);
     53    }
     54  };
    1555
    1656  return (
    1757    <div className="mt-8 space-y-12">
     58      {toast.show && (
     59        <div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 animate-fade-in-up">
     60          <div className="bg-gray-900 text-white px-6 py-3 rounded-full shadow-lg text-sm font-medium">
     61            {toast.message}
     62          </div>
     63        </div>
     64      )}
    1865      {playlists && playlists.length > 0 && (
    1966        <section>
    20           <div className="flex items-center gap-2 mb-6 pb-2 border-b-2 border-gray-200">
     67          <div className="flex items-center gap-3 mb-6">
    2168            <ListMusic className="w-6 h-6 text-gray-700" />
    2269            <h3 className="text-2xl font-bold text-gray-800">
    2370              Created Playlists
    2471            </h3>
    25             <span className="text-sm text-gray-400 ml-1">
    26               ({playlists.length})
    27             </span>
    28           </div>
    29 
    30           <div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
     72            <span className="text-sm text-gray-500">({playlists.length})</span>
     73          </div>
     74
     75          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    3176            {playlists.map((playlist) => (
    3277              <div
     
    3580                onClick={() => navigate(`/collection/playlist/${playlist.id}`)}
    3681              >
    37                 <div className="aspect-square rounded-md overflow-hidden bg-gray-100 mb-2 relative shadow-sm group-hover:shadow-lg transition-all">
     82                <div className="relative aspect-square rounded-lg overflow-hidden bg-gray-100 mb-3 shadow-md group-hover:shadow-lg transition-all">
    3883                  {playlist.cover ? (
    3984                    <img
    4085                      src={playlist.cover}
    4186                      alt={playlist.name}
    42                       className="w-full h-full object-cover group-hover:opacity-90 transition-opacity"
     87                      className="w-full h-full object-cover"
    4388                    />
    4489                  ) : (
    45                     <div className="w-full h-full bg-gradient-to-br from-gray-200 to-gray-300 flex items-center justify-center">
    46                       <ListMusic className="w-10 h-10 text-gray-400" />
     90                    <div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
     91                      <ListMusic className="w-16 h-16 text-white opacity-30" />
    4792                    </div>
    4893                  )}
    49                 </div>
    50                 <p className="text-sm font-medium text-gray-800 truncate group-hover:text-blue-600 transition-colors">
     94
     95                  {/* Save button overlay */}
     96                  <button
     97                    onClick={(e) => handleSavePlaylist(e, playlist.id)}
     98                    className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100"
     99                    title={playlist.isSavedByCurrentUser ? "Unsave" : "Save"}
     100                  >
     101                    <Bookmark
     102                      className={`w-5 h-5 ${
     103                        playlist.isSavedByCurrentUser
     104                          ? "fill-blue-600 text-blue-600"
     105                          : "text-gray-600"
     106                      }`}
     107                    />
     108                  </button>
     109                </div>
     110                <p className="text-sm font-semibold text-gray-900 truncate group-hover:text-blue-600 transition-colors">
    51111                  {playlist.name}
     112                </p>
     113                <p className="text-xs text-gray-500 truncate">
     114                  {playlist.creatorName}
    52115                </p>
    53116              </div>
     
    59122      {likedSongs && likedSongs.length > 0 && (
    60123        <section>
    61           <div className="flex items-center gap-2 mb-6 pb-2 border-b-2 border-gray-200">
     124          <div className="flex items-center gap-3 mb-6">
    62125            <Heart className="w-6 h-6 text-red-500 fill-red-500" />
    63126            <h3 className="text-2xl font-bold text-gray-800">Liked Songs</h3>
    64             <span className="text-sm text-gray-400 ml-1">
     127            <span className="text-sm text-gray-500">
    65128              ({likedSongs?.length})
    66129            </span>
    67130          </div>
    68131
    69           <div className="space-y-1">
     132          <div className="space-y-2">
    70133            {likedSongs.map((song, index) => (
    71134              <div
    72135                key={song.id}
    73                 className="flex items-center gap-4 p-3 rounded hover:bg-gray-50 cursor-pointer group transition-colors"
     136                className="flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 cursor-pointer group transition-colors"
    74137                onClick={() => navigate(`/musical-entity/${song.id}`)}
    75138              >
    76                 <span className="text-sm text-gray-400 w-8 text-right font-medium">
     139                <span className="text-gray-500 font-medium w-8 text-center">
    77140                  {index + 1}
    78141                </span>
    79                 <div className="w-12 h-12 rounded bg-gradient-to-br from-red-100 to-pink-100 flex items-center justify-center flex-shrink-0">
    80                   <Music className="w-6 h-6 text-red-600" />
     142                <div className="w-12 h-12 rounded bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center flex-shrink-0 shadow-sm">
     143                  <Music className="w-6 h-6 text-white" />
    81144                </div>
    82145                <div className="flex-1 min-w-0">
    83                   <p className="font-medium text-gray-800 truncate group-hover:text-blue-600 transition-colors">
     146                  <p className="font-semibold text-gray-900 truncate group-hover:text-blue-600 transition-colors">
    84147                    {song.title}
    85148                  </p>
    86                   <p className="text-xs text-gray-500">{song.genre}</p>
    87                 </div>
     149                  <p className="text-sm text-gray-600 truncate">
     150                    {song.releasedBy}
     151                  </p>
     152                </div>
     153                <span className="text-sm text-gray-600 px-3 py-1 bg-gray-100 rounded-full">
     154                  {song.genre}
     155                </span>
     156
     157                <button
     158                  onClick={(e) => {
     159                    e.stopPropagation();
     160                    handleLike(song.id, song.title);
     161                  }}
     162                  className="p-2 hover:bg-gray-200 rounded-full transition-colors duration-200 cursor-pointer"
     163                  title={song.isLikedByCurrentUser ? "Unlike" : "Like"}
     164                >
     165                  <svg
     166                    className="w-5 h-5"
     167                    fill={song.isLikedByCurrentUser ? "#ef4444" : "none"}
     168                    stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"}
     169                    strokeWidth="2"
     170                    viewBox="0 0 24 24"
     171                  >
     172                    <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
     173                  </svg>
     174                </button>
    88175              </div>
    89176            ))}
     
    94181      {likedAlbums && likedAlbums.length > 0 && (
    95182        <section>
    96           <div className="flex items-center gap-2 mb-6 pb-2 border-b-2 border-gray-200">
    97             <Album className="w-6 h-6 text-blue-600" />
     183          <div className="flex items-center gap-3 mb-6">
     184            <Album className="w-6 h-6 text-gray-700" />
    98185            <h3 className="text-2xl font-bold text-gray-800">Liked Albums</h3>
    99             <span className="text-sm text-gray-400 ml-1">
     186            <span className="text-sm text-gray-500">
    100187              ({likedAlbums.length})
    101188            </span>
    102189          </div>
    103190
    104           <div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-5">
     191          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
    105192            {likedAlbums.map((album) => (
    106193              <div
     
    109196                onClick={() => navigate(`/collection/album/${album.id}`)}
    110197              >
    111                 <div className="aspect-square rounded-lg overflow-hidden bg-gradient-to-br from-blue-100 to-indigo-100 mb-3 flex items-center justify-center shadow-sm group-hover:shadow-md transition-all">
    112                   <Album className="w-16 h-16 text-blue-600 opacity-60" />
    113                 </div>
    114                 <p className="font-medium text-sm text-gray-800 truncate group-hover:text-blue-600 transition-colors">
     198                <div className="relative aspect-square rounded-lg overflow-hidden bg-gradient-to-br from-blue-400 to-purple-500 mb-3 flex items-center justify-center shadow-md group-hover:shadow-lg transition-all">
     199                  <Album className="w-16 h-16 text-white opacity-30" />
     200
     201                  <button
     202                    onClick={(e) => {
     203                      e.stopPropagation();
     204                      handleLike(album.id, album.title);
     205                    }}
     206                    className="absolute top-2 right-2 p-2 bg-white/90 hover:bg-white rounded-full shadow-md transition-all duration-200 opacity-0 group-hover:opacity-100 cursor-pointer"
     207                    title={album.isLikedByCurrentUser ? "Unlike" : "Like"}
     208                  >
     209                    <svg
     210                      className="w-5 h-5"
     211                      fill={album.isLikedByCurrentUser ? "#ef4444" : "none"}
     212                      stroke={
     213                        album.isLikedByCurrentUser ? "#ef4444" : "#6b7280"
     214                      }
     215                      strokeWidth="2"
     216                      viewBox="0 0 24 24"
     217                    >
     218                      <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
     219                    </svg>
     220                  </button>
     221                </div>
     222                <p className="font-semibold text-sm text-gray-900 truncate group-hover:text-blue-600 transition-colors">
    115223                  {album.title}
    116224                </p>
    117                 <p className="text-xs text-gray-500 mt-0.5">{album.genre}</p>
     225                <p className="text-xs text-gray-600 truncate mt-1">
     226                  {album.releasedBy}
     227                </p>
    118228              </div>
    119229            ))}
     
    125235        likedEntities.length === 0 &&
    126236        (!playlists || playlists.length === 0) && (
    127           <div className="flex flex-col items-center justify-center py-16 text-gray-300">
    128             <p className="text-lg font-medium text-gray-400">
    129               Nothing here yet
    130             </p>
    131             <p className="text-sm text-gray-400 mt-1">
     237          <div className="flex flex-col items-center justify-center py-16 text-gray-400">
     238            <Music className="w-20 h-20 mb-4 opacity-20" />
     239            <p className="text-lg font-medium">Nothing here yet</p>
     240            <p className="text-sm mt-2">
    132241              Start exploring music to build your collection
    133242            </p>
Note: See TracChangeset for help on using the changeset viewer.