Skip to content

Commit 01214f6

Browse files
author
Garrett Downs
committed
feat: add user and track search
1 parent fb1c875 commit 01214f6

6 files changed

Lines changed: 262 additions & 84 deletions

File tree

src/App.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
'/playlist/:playlistId': Playlist,
6363
'/playlist/:playlistId/description': PlaylistDescription,
6464
'/profile': Profile,
65-
'/search': Search,
65+
'/search/:cardId': Search,
6666
'/settings/:cardId': AppSettings,
6767
'/signin': SignIn,
6868
'/stream': Stream,

src/components/AppMenu.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
const menuItems: MenuItem[] = [
2626
{ id: 'library', text: 'Library', route: '/library', icon: MdLibraryMusic },
2727
{ id: 'stream', text: 'Stream', route: '/stream', icon: MdViewStream },
28-
{ id: 'search', text: 'Search', route: '/search', icon: MdSearch },
28+
{ id: 'search', text: 'Search', route: '/search/users', icon: MdSearch },
2929
{ id: 'player', text: 'Player', route: '/player/info', icon: MdPlayArrow },
3030
{ id: 'settings', text: 'Settings', route: '/settings/display', icon: MdSettings },
3131
{ id: 'about', text: 'About', route: '/about', icon: MdInfoOutline },
@@ -47,8 +47,8 @@
4747
<ListItem
4848
imageUrl={session.user_avatar_url}
4949
imageStyle="circle"
50-
primaryText="Profile"
51-
secondaryText={session.user_name}
50+
imageSize={IconSize.Small}
51+
primaryText="My Profile"
5252
navi={{
5353
itemId: 'profile',
5454
shortcutKey: getShortcutFromIndex(0),

src/components/SearchTracks.svelte

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<script lang="ts">
2+
import Divider from 'onyx-ui/components/divider/Divider.svelte';
3+
import InputRow from 'onyx-ui/components/form/InputRow.svelte';
4+
import SelectRow from 'onyx-ui/components/form/SelectRow.svelte';
5+
import Typography from 'onyx-ui/components/Typography.svelte';
6+
import { DataStatus } from 'onyx-ui/enums';
7+
import { updateView } from 'onyx-ui/stores/view';
8+
import { onMount } from 'svelte';
9+
import { querystring, replace } from 'svelte-spa-router';
10+
import { SoundCloud } from '../lib/soundcloud';
11+
import type { Track } from '../models';
12+
import TrackItem from './TrackItem.svelte';
13+
14+
type SortBy = 'plays' | 'favorites' | 'comments';
15+
16+
let getData: Promise<Track[]> = Promise.resolve([]);
17+
18+
function search() {
19+
if (query.length < 3) return;
20+
21+
getData = new SoundCloud().track.search(query).then((res) => {
22+
switch (sort) {
23+
case 'plays':
24+
return res.sort((a, b) => sortData(a, b, 'playback_count')).slice(0, 25);
25+
case 'favorites':
26+
return res.sort((a, b) => sortData(a, b, 'favoritings_count')).slice(0, 25);
27+
case 'comments':
28+
return res.sort((a, b) => sortData(a, b, 'comment_count')).slice(0, 25);
29+
default:
30+
return res.slice(0, 25);
31+
}
32+
});
33+
}
34+
35+
function getAccentText(track: Track, sortBy: SortBy): string {
36+
switch (sortBy) {
37+
case 'plays':
38+
return `${(track.playback_count || 0).toLocaleString()} plays`;
39+
case 'favorites':
40+
return `${(track.favoritings_count || 0).toLocaleString()} favorites`;
41+
case 'comments':
42+
return `${(track.comment_count || 0).toLocaleString()} comments`;
43+
default:
44+
return '';
45+
}
46+
}
47+
48+
function sortData(a: Track, b: Track, property: keyof Track) {
49+
if (a[property] > b[property]) return -1;
50+
if (a[property] < b[property]) return 1;
51+
return 0;
52+
}
53+
54+
onMount(async () => {
55+
if (query?.length >= 3) {
56+
search();
57+
await getData;
58+
updateView({ dataStatus: DataStatus.Loaded });
59+
}
60+
});
61+
62+
let query: string;
63+
let sort: SortBy = 'plays';
64+
$: query = new URLSearchParams($querystring).get('q') || '';
65+
$: sort = (new URLSearchParams($querystring).get('sort') as any) || 'plays';
66+
67+
$: {
68+
console.log('Sort changed:', sort);
69+
search();
70+
}
71+
</script>
72+
73+
<InputRow
74+
label="Query"
75+
value={query}
76+
placeholder="Enter text..."
77+
onChange={(val) => replace(`/search/tracks?q=${val}&sort=${sort}`)}
78+
onSubmit={() => search()}
79+
/>
80+
<SelectRow
81+
label="Sort"
82+
value={sort}
83+
options={[
84+
{ id: 'plays', label: 'Plays' },
85+
{ id: 'favorites', label: 'Favorites' },
86+
{ id: 'comments', label: 'Comments' },
87+
]}
88+
onChange={(val) => replace(`/search/tracks?q=${query}&sort=${val}`)}
89+
/>
90+
<Divider title="Results" />
91+
{#await getData}
92+
<Typography align="center">Loading...</Typography>
93+
{:then data}
94+
{#each data as track (track.id)}
95+
<TrackItem
96+
{track}
97+
primaryText={track.title}
98+
secondaryText={track.user.username}
99+
accentText={getAccentText(track, sort)}
100+
/>
101+
{:else}
102+
<Typography align="center">No results</Typography>
103+
{/each}
104+
{:catch error}
105+
<Typography align="center">Failed to load data</Typography>
106+
{/await}

src/components/SearchUsers.svelte

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<script lang="ts">
2+
import Divider from 'onyx-ui/components/divider/Divider.svelte';
3+
import InputRow from 'onyx-ui/components/form/InputRow.svelte';
4+
import SelectRow from 'onyx-ui/components/form/SelectRow.svelte';
5+
import ListItem from 'onyx-ui/components/list/ListItem.svelte';
6+
import Typography from 'onyx-ui/components/Typography.svelte';
7+
import { DataStatus } from 'onyx-ui/enums';
8+
import { Onyx } from 'onyx-ui/services';
9+
import { updateView } from 'onyx-ui/stores/view';
10+
import { getShortcutFromIndex } from 'onyx-ui/utils/getShortcutFromIndex';
11+
import { onMount } from 'svelte';
12+
import { push, querystring, replace } from 'svelte-spa-router';
13+
import { SoundCloud } from '../lib/soundcloud';
14+
import type { User } from '../models';
15+
import { formatLocation } from '../utils/formatLocation';
16+
import { getImage } from '../utils/getImage';
17+
18+
let getData: Promise<User[]> = Promise.resolve([]);
19+
20+
function search() {
21+
getData = new SoundCloud().user.search(query);
22+
}
23+
24+
let followingIds = [];
25+
onMount(async () => {
26+
new SoundCloud().me.getFollowing().then((res) => (followingIds = res.map((a) => a.id)));
27+
28+
if (query?.length >= 3) {
29+
search();
30+
await getData;
31+
updateView({ dataStatus: DataStatus.Loaded });
32+
}
33+
});
34+
35+
$: query = new URLSearchParams($querystring).get('q') || '';
36+
$: sort = new URLSearchParams($querystring).get('sort') || 'followers';
37+
</script>
38+
39+
<InputRow
40+
label="Query"
41+
value={query}
42+
placeholder="Enter text..."
43+
onChange={(val) => replace(`/search/users?q=${val}&sort=${sort}`)}
44+
onSubmit={() => search()}
45+
/>
46+
<SelectRow
47+
label="Sort"
48+
value={sort}
49+
options={[{ id: 'followers', label: 'Followers' }]}
50+
onChange={(val) => replace(`/search/users?q=${query}&sort=${val}`)}
51+
/>
52+
<Divider title="Results" />
53+
{#await getData}
54+
<Typography align="center">Loading...</Typography>
55+
{:then data}
56+
{#each data as user, i (user.id)}
57+
<ListItem
58+
imageUrl={getImage(user.avatar_url, 60)}
59+
imageStyle="circle"
60+
primaryText={user.username}
61+
secondaryText={formatLocation(user)}
62+
accentText={`${user.followers_count.toLocaleString()} followers`}
63+
navi={{
64+
itemId: `${i + 1}`,
65+
shortcutKey: getShortcutFromIndex(i),
66+
onSelect: () => push(`/user/${user.id}`),
67+
}}
68+
contextMenu={{
69+
title: user.username,
70+
items: [
71+
followingIds.includes(user.id)
72+
? {
73+
label: 'Unfollow',
74+
onSelect: async () => {
75+
await new SoundCloud().user.unfollow(user.id);
76+
followingIds = followingIds.filter((a) => a !== user.id);
77+
Onyx.contextMenu.close();
78+
},
79+
}
80+
: {
81+
label: 'Follow',
82+
onSelect: async () => {
83+
await new SoundCloud().user.follow(user.id);
84+
followingIds = [...followingIds, user.id];
85+
Onyx.contextMenu.close();
86+
},
87+
},
88+
],
89+
}}
90+
/>
91+
{:else}
92+
<Typography align="center">No results</Typography>
93+
{/each}
94+
{:catch error}
95+
<Typography align="center">Failed to load data</Typography>
96+
{/await}

src/lib/soundcloud.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ export class SoundCloud {
120120
})
121121
.slice(0, 25);
122122
},
123+
follow: async (userId: number): Promise<void> => {
124+
await this.httpPost(`me/followings/${userId}`);
125+
await Cache.invalidate('me/followings');
126+
},
127+
unfollow: async (userId: number): Promise<void> => {
128+
await this.httpDelete(`me/followings/${userId}`);
129+
await Cache.invalidate('me/followings');
130+
},
123131
};
124132

125133
track = {
@@ -141,6 +149,8 @@ export class SoundCloud {
141149
const res = await this.httpGet<CollectionResult<Track>>(
142150
`tracks?q=${query}&limit=50&linked_partitioning=true`
143151
);
152+
console.log('track search', res);
153+
144154
return res.collection;
145155
},
146156
like: async (trackId: number): Promise<void> => {

src/routes/Search.svelte

Lines changed: 46 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,61 @@
11
<script lang="ts">
2-
import Button from 'onyx-ui/components/buttons/Button.svelte';
32
import Card from 'onyx-ui/components/card/Card.svelte';
43
import CardContent from 'onyx-ui/components/card/CardContent.svelte';
54
import CardHeader from 'onyx-ui/components/card/CardHeader.svelte';
6-
import Divider from 'onyx-ui/components/divider/Divider.svelte';
7-
import InputRow from 'onyx-ui/components/form/InputRow.svelte';
8-
import SelectRow from 'onyx-ui/components/form/SelectRow.svelte';
9-
import ListItem from 'onyx-ui/components/list/ListItem.svelte';
10-
import Typography from 'onyx-ui/components/Typography.svelte';
115
import View from 'onyx-ui/components/view/View.svelte';
126
import ViewContent from 'onyx-ui/components/view/ViewContent.svelte';
13-
import { DataStatus } from 'onyx-ui/enums';
14-
import { registerView, updateView } from 'onyx-ui/stores/view';
15-
import { getShortcutFromIndex } from 'onyx-ui/utils/getShortcutFromIndex';
16-
import { onMount } from 'svelte';
17-
import { push, querystring, replace } from 'svelte-spa-router';
18-
import { SoundCloud } from '../lib/soundcloud';
19-
import { getImage } from '../utils/getImage';
7+
import { registerView, view } from 'onyx-ui/stores/view';
8+
import { replace } from 'svelte-spa-router';
9+
import SearchTracks from '../components/SearchTracks.svelte';
10+
import SearchUsers from '../components/SearchUsers.svelte';
2011
21-
let getData = Promise.resolve([]);
12+
export let params: { cardId: string };
2213
23-
registerView({});
24-
25-
function search() {
26-
getData = new SoundCloud().user.search(query);
27-
}
28-
29-
onMount(async () => {
30-
if (query?.length >= 3) {
31-
search();
32-
await getData;
33-
updateView({ dataStatus: DataStatus.Loaded });
34-
}
14+
registerView({
15+
cards: [
16+
{
17+
id: 'users',
18+
title: 'Search Users',
19+
onSelect: () => replace(`/search/users`),
20+
},
21+
{
22+
id: 'tracks',
23+
title: 'Search Tracks',
24+
onSelect: () => replace(`/search/tracks`),
25+
},
26+
{
27+
id: 'playlists',
28+
title: 'Search Playlists',
29+
onSelect: () => replace(`/search/playlists`),
30+
},
31+
],
32+
activeCardId: params.cardId ?? 'users',
3533
});
36-
37-
$: query = new URLSearchParams($querystring).get('q') || '';
38-
$: type = new URLSearchParams($querystring).get('type') || 'users';
3934
</script>
4035

4136
<View>
4237
<ViewContent>
43-
<Card>
44-
<CardHeader title="Search" />
45-
<CardContent>
46-
<InputRow
47-
label="Query"
48-
value={query}
49-
placeholder="Enter text..."
50-
onChange={(val) => replace(`/search?q=${val}&type=${type}`)}
51-
/>
52-
<SelectRow
53-
label="Result Type"
54-
value={type}
55-
disabled
56-
options={[
57-
{ id: 'tracks', label: 'Tracks' },
58-
{ id: 'playlists', label: 'Playlists' },
59-
{ id: 'users', label: 'Users' },
60-
]}
61-
onChange={(val) => replace(`/search?q=${query}&type=${val}`)}
62-
/>
63-
<Button
64-
title="Search"
65-
disabled={query.length < 3}
66-
navi={{
67-
itemId: `signinCode`,
68-
onSelect: async () => search(),
69-
}}
70-
/>
71-
<Divider title="Results" />
72-
{#await getData}
73-
<Typography align="center">Loading...</Typography>
74-
{:then data}
75-
{#each data as user, i (user.id)}
76-
<ListItem
77-
imageUrl={getImage(user.avatar_url, 60)}
78-
primaryText={user.full_name || user.username}
79-
secondaryText={`${user.followers_count.toLocaleString()} followers`}
80-
navi={{
81-
itemId: `${i + 1}`,
82-
shortcutKey: getShortcutFromIndex(i),
83-
onSelect: () => push(`/user/${user.id}`),
84-
}}
85-
/>
86-
{:else}
87-
<Typography align="center">No results</Typography>
88-
{/each}
89-
{:catch error}
90-
<Typography align="center">Failed to load data</Typography>
91-
{/await}
92-
</CardContent>
93-
</Card>
38+
{#if params.cardId === $view.cards[0].id}
39+
<Card cardId={$view.cards[0].id}>
40+
<CardHeader />
41+
<CardContent>
42+
<SearchUsers />
43+
</CardContent>
44+
</Card>
45+
{:else if params.cardId === $view.cards[1].id}
46+
<Card cardId={$view.cards[1].id}>
47+
<CardHeader />
48+
<CardContent>
49+
<SearchTracks />
50+
</CardContent>
51+
</Card>
52+
{:else if params.cardId === $view.cards[2].id}
53+
<Card cardId={$view.cards[2].id}>
54+
<CardHeader />
55+
<CardContent>
56+
<div>search playlists</div>
57+
</CardContent>
58+
</Card>
59+
{/if}
9460
</ViewContent>
9561
</View>

0 commit comments

Comments
 (0)