wip

Seto Elkahfi committed Aug 2, 2024 at 18:26 UTC 9ccfbdfe8170496c4f4d2741c4c0d48d26a434a1
15 files changed +214 -84
backend/musik88-web/.rubocop.yml
+3 -1
@@ -1,4 +1,6 @@
1 -Documentation:
1 +AllCops:
2 + NewCops: disable
3 +Style/Documentation:
4 Enabled: false
5 Style/HashSyntax:
6 Enabled: false
\ No newline at end of file
backend/musik88-web/app/models/user.rb
+1 -1
@@ -18,7 +18,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
18
19 before_save { email.downcase! }
20
21 - VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
21 + VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i
22 validates :email, presence: true,
23 length: { maximum: 255 },
24 format: { with: VALID_EMAIL_REGEX },
frontend/splitfire-desktop/app/_src/lib/tauriHandler.ts
+2
@@ -16,12 +16,14 @@ export const TAURI_PLAYER_RECORDING_LENGTH = 'player_recording_length' // in se
16 export const TAURI_ACCOUNT_LOGIN = 'account_login'
17 export const TAURI_ACCOUNT_LOGOUT = 'account_logout'
18 export const TAURI_ACCOUNT_REGISTER = 'account_register'
19 +export const TAURI_ACCOUNT_PROFILE = 'account_profile'
20
21 // Contents
22 export const TAURI_CONTENT_CAROUSEL = 'content_carousel'
23 export const TAURI_CONTENT_READY_TO_PLAY = 'content_ready_to_play'
24 export const TAURI_CONTENT_SONG_BRIDGE_DETAIL = 'content_song_bridge_detail'
25 export const TAURI_CONTENT_TOP_VOTED = 'content_top_voted'
26 +export const TAURI_CONTENT_SONG_BRIDGE_VOTE = 'content_song_bridge_vote'
27
28 // Settings
29 export const TAURI_SET_ENVIRONMENT = 'set_environment'
frontend/splitfire-desktop/app/login/page.tsx
+3 -3
@@ -72,7 +72,7 @@ export default function Page() {
72 event.preventDefault();
73 }}
74 >
75 - <Form.Field className="FormField" name="email">
75 + <Form.Field className="FormField my-6" name="email">
76 <div
77 style={{
78 display: "flex",
@@ -98,7 +98,7 @@ export default function Page() {
98 />
99 </Form.Control>
100 </Form.Field>
101 - <Form.Field className="FormField" name="password">
101 + <Form.Field className="FormField my-6" name="password">
102 <div
103 style={{
104 display: "flex",
@@ -126,7 +126,7 @@ export default function Page() {
126 />
127 </Form.Control>
128 </Form.Field>
129 - <Form.Field className="FormField" name="password">
129 + <Form.Field className="FormField my-6" name="password">
130 <Button onClick={() => login(update)} variant={"outline"} size={"lg"}>
131 Login
132 </Button>
frontend/splitfire-desktop/app/profile/page.tsx
+41 -14
@@ -1,21 +1,49 @@
1 "use client";
2
3 -import { useContext } from "react";
3 +import { useContext, useEffect, useState } from "react";
4 import { UserContext } from "../_src/lib/CurrentUserContext";
5 import { useLogger } from "../_src/lib/logger";
6 -import { SkeletonCard } from "../_ui/skeleton-card";
6 import { usernameOrId } from "../_src/models/user";
7 import { useSearchParams } from "next/navigation";
8 import Image from "next/image";
9 +import { invoke } from "@tauri-apps/api";
10 +import { TAURI_ACCOUNT_PROFILE } from "../_src/lib/tauriHandler";
11 +import { AccountProfileResponse } from "@/models/account";
12 +import { TauriResponse } from "@/models/shared";
13 +import { CheckCircleIcon } from "@heroicons/react/outline";
14
15 export default function Page() {
16 const log = useLogger("Profile/Page");
17 const searchParams = useSearchParams();
18 const userId = searchParams.get("userId");
19 const currentUser = useContext(UserContext);
20 + const [isOwnProfile, setIsOwnProfile] = useState(false);
21 log.debug("currentUser");
17 -
22 const user = currentUser?.user?.user;
23 +
24 + useEffect(() => {
25 + log.debug("Profile page loaded.");
26 + async function fetchData() {
27 + try {
28 + const result = await invoke<AccountProfileResponse>(TAURI_ACCOUNT_PROFILE, { userId });
29 + log.debug("ProfileResponse", result);
30 + if (result.status === TauriResponse.ERROR) {
31 + log.error("Error fetching profile");
32 + return;
33 + }
34 + if (!result.user) {
35 + log.error("No user found");
36 + return;
37 + }
38 +
39 + setIsOwnProfile(result.user.id === currentUser?.user?.user.id);
40 + } catch (error) {
41 + log.error(error);
42 + }
43 + }
44 + fetchData();
45 + // eslint-disable-next-line react-hooks/exhaustive-deps
46 + }, []);
47 // Sanity check
48 if (!user) {
49 log.error("No user context");
@@ -23,25 +51,24 @@ export default function Page() {
51 }
52
53 return (
26 - <div className="prose prose-sm prose-invert max-w-none">
27 - <div className="flex justify-between">
54 + <div className="prose prose-sm prose-invert max-w-none grid-rows-3 gap-6">
55 + <div className="flex justify-between">
56 <div className="self-start">
57 <h1 className="text-3xl font-bold">{user.name}</h1>
58 + {(isOwnProfile) && (<CheckCircleIcon className="h-6 w-6 text-green-500" />)}
59 </div>
60 </div>
61 <div className="grid grid-rows-2 grid-flow-row auto-rows-max">
62 <div className="max-h-1">
34 - <Image src={user.gravatar_url} alt="avatar" width={50} height={50} className="rounded-full"/>
63 + <Image
64 + src={user.gravatar_url}
65 + alt="avatar"
66 + width={50}
67 + height={50}
68 + className="rounded-full"
69 + />
70 <h2>@{usernameOrId(user)}</h2>
71 </div>
37 - <div className="max-w-none">
38 - <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
39 - <h2 className="text-2xl font-bold">Your public plays</h2>
40 - {Array.from({ length: 6 }).map((_, i) => (
41 - <SkeletonCard key={i} />
42 - ))}
43 - </div>
44 - </div>
72 </div>
73 </div>
74 );
frontend/splitfire-desktop/app/register/page.tsx
+5 -4
@@ -3,10 +3,11 @@
3 import { invoke } from "@tauri-apps/api";
4 import { useState } from "react";
5 import * as Form from "@radix-ui/react-form";
6 -import { TAURI_ACCOUNT_REGISTER, TauriResponse } from "../_src/lib/tauriHandler";
6 +import { TAURI_ACCOUNT_REGISTER } from "../_src/lib/tauriHandler";
7 import { Button } from "../_ui/components/button";
8 import { useLogger } from "../_src/lib/logger";
9 import { AccountRegisterResponse } from "@/models/account";
10 +import { TauriResponse } from "@/models/shared";
11
12 enum State {
13 LOADING,
@@ -99,7 +100,7 @@ function SignupForm({ registerCallback }: { registerCallback: (state: SignupStat
100 event.preventDefault();
101 }}
102 >
102 - <Form.Field className="FormField" name="name">
103 + <Form.Field className="FormField my-6" name="name">
104 <div
105 style={{
106 display: "flex",
@@ -151,7 +152,7 @@ function SignupForm({ registerCallback }: { registerCallback: (state: SignupStat
152 />
153 </Form.Control>
154 </Form.Field>
154 - <Form.Field className="FormField" name="password">
155 + <Form.Field className="FormField my-6" name="password">
156 <div
157 style={{
158 display: "flex",
@@ -179,7 +180,7 @@ function SignupForm({ registerCallback }: { registerCallback: (state: SignupStat
180 />
181 </Form.Control>
182 </Form.Field>
182 - <Form.Field className="FormField" name="button">
183 + <Form.Field className="FormField my-6" name="button">
184 <Button onClick={register} variant={"outline"} size={"lg"}>
185 {buttonText}
186 </Button>
frontend/splitfire-desktop/app/split/_components/song-votes.tsx
+1 -1
@@ -17,7 +17,7 @@ export default function SongVotes({
17 <h1 className="my-2">{songProvider.name}</h1>
18 <UpDownVotesView
19 votes={votes}
20 - providerId={songProvider.id}
20 + songProviderId={songProvider.id}
21 audioFile={songProvider.audio_file}
22 />
23 <Image
frontend/splitfire-desktop/app/split/_components/voters-view.tsx
+4 -4
@@ -8,11 +8,10 @@ export function VoterGravatarsViews(props: {
8 type: VoteType;
9 }) {
10 const className =
11 - props.type === VoteType.DOWN
12 - ? "justify-end"
13 - : "justify-start";
11 + props.type === VoteType.DOWN ? "justify-end" : "justify-start";
12 return (
13 <div className={className}>
14 + <div className="flex -space-x-2 overflow-hidden">
15 {props.voters.map((x, i) => {
16 return (
17 <Link href={`/profile?userId=@${x.user_id}`} key={i}>
@@ -20,12 +19,13 @@ export function VoterGravatarsViews(props: {
19 src={x.voter_gravatar}
20 width={24}
21 height={24}
23 - className="rounded-full"
22 + className="inline-block h-8 w-8 rounded-full ring-2 ring-white"
23 alt={x.voter_username_or_id}
24 />
25 </Link>
26 );
27 })}
28 </div>
29 + </div>
30 );
31 }
frontend/splitfire-desktop/app/split/_components/votes-view.tsx
+38 -44
@@ -11,13 +11,15 @@ import {
11 import { UserContext } from "@/app/_src/lib/CurrentUserContext";
12 import { CurrentUser } from "@/app/_src/lib/db";
13 import { SongProviderVote } from "@/app/_src/models/SongVotesDetailResponse";
14 -import axios from "axios";
14 import { useRouter } from "next/navigation";
15 import { CSSProperties, useState, useContext } from "react";
16 import { Spinner } from "react-bootstrap";
17 import ButtonGenerateBackingTracks from "./button-split";
18 import MainVotesView from "./votes-view-main";
19 import LetsPlayView from "./lets-play";
20 +import { invoke } from "@tauri-apps/api";
21 +import { TAURI_CONTENT_SONG_BRIDGE_VOTE } from "@/app/_src/lib/tauriHandler";
22 +import { useLogger } from "@/app/_src/lib/logger";
23
24 enum State {
25 LOADING,
@@ -39,14 +41,15 @@ export interface VoteState {
41 }
42
43 export default function UpDownVotesView({
42 - providerId,
44 + songProviderId,
45 votes,
46 audioFile,
47 }: {
46 - providerId: string;
48 + songProviderId: string;
49 votes: SongProviderVote[];
50 audioFile: AudioFile | null;
51 }) {
52 + const log = useLogger("Votes view");
53 const [state, setState] = useState(State.LOADED);
54 const [goToLogin, setGoToLogin] = useState(false);
55 const [goToPlayer, setGoToPlayer] = useState(false);
@@ -83,8 +86,6 @@ export default function UpDownVotesView({
86
87 const up = votes.filter((x) => x.vote_type === VoteType.UP).length;
88 const down = votes.filter((x) => x.vote_type === VoteType.DOWN).length;
86 - //console.log('upvotes', up)
87 - //console.log('downvotes', down)
89 const votesAggregate = up - down;
90 return votesAggregate;
91 };
@@ -112,48 +113,41 @@ export default function UpDownVotesView({
113 downVotes: votes.filter((x) => x.vote_type === VoteType.DOWN),
114 });
115
115 - const vote = (type: VoteType) => {
116 + const vote = async (type: VoteType) => {
117 if (!user || !user.accessToken || user.accessToken.length <= 1) {
118 setGoToLogin(true);
119 return;
120 }
121
122 setState(State.LOADING);
122 - axios
123 - .post(
124 - `/song-bridge/${providerId}/vote`,
125 - { vote_type: type, provider_id: providerId },
126 - {
127 - headers: {
128 - "Content-Type": "application/json",
129 - Authorization: user.accessToken,
130 - },
131 - }
132 - )
133 - .then((res) => {
134 - const response: SongBridgeResponse = res.data;
135 - //console.log(response.votes)
136 - if (response.code === HTTPStatusCode.OK) {
137 - setVoteState({
138 - aggregate: calculateVotes(response.votes),
139 - buttonUpStyle: buttonStyle(response.votes, VoteType.UP),
140 - buttonDownStyle: buttonStyle(response.votes, VoteType.DOWN),
141 - upVotes: response.votes.filter((x) => x.vote_type === VoteType.UP),
142 - downVotes: response.votes.filter(
143 - (x) => x.vote_type === VoteType.DOWN
144 - ),
145 - });
146 - setState(State.LOADED);
147 - } else {
148 - console.log("error");
149 - setState(State.ERROR);
150 - }
151 - })
152 - .catch((error) => {
153 - console.log(error);
123 + try {
124 + const payload = {
125 + songProviderId,
126 + type,
127 + accessToken: user.accessToken,
128 + };
129 + const response = await invoke<SongBridgeResponse>(TAURI_CONTENT_SONG_BRIDGE_VOTE, { payload });
130 + log.debug(response.votes);
131 + if (response.code === HTTPStatusCode.OK) {
132 + setVoteState({
133 + aggregate: calculateVotes(response.votes),
134 + buttonUpStyle: buttonStyle(response.votes, VoteType.UP),
135 + buttonDownStyle: buttonStyle(response.votes, VoteType.DOWN),
136 + upVotes: response.votes.filter((x) => x.vote_type === VoteType.UP),
137 + downVotes: response.votes.filter(
138 + (x) => x.vote_type === VoteType.DOWN
139 + ),
140 + });
141 + setState(State.LOADED);
142 + } else {
143 + console.log("error");
144 setState(State.ERROR);
155 - });
156 - };
145 + }
146 + } catch (error) {
147 + console.log(error);
148 + setState(State.ERROR);
149 + }
150 + }
151
152 const isDoneSplitting = audioFile && audioFile.status === Status.DONE;
153
@@ -184,10 +178,10 @@ export default function UpDownVotesView({
178 );
179 const mainButton = (
180 <ButtonGenerateBackingTracks
187 - providerId={providerId}
188 - audioFile={audioFile}
189 - aggregateVotes={voteState.aggregate}
190 - />
181 + providerId={songProviderId}
182 + audioFile={audioFile}
183 + aggregateVotes={voteState.aggregate}
184 + />
185 );
186 // Votes treshold is passed, show the generate button.
187 return (
frontend/splitfire-desktop/models/account.ts
+7 -1
@@ -12,4 +12,10 @@ export interface AccountRegisterResponse {
12 status: TauriResponse,
13 message: string,
14 user: User | null
15 -}
\ No newline at end of file
15 +}
16 +
17 +export interface AccountProfileResponse {
18 + status: TauriResponse,
19 + message: string,
20 + user: User | null
21 +}
frontend/splitfire-desktop/src-tauri/src/command/constants.rs
+1
@@ -27,6 +27,7 @@ pub const PATH_READY_TO_PLAY: &str = "api/v1/ready-to-play";
27 pub const PATH_SEARCH: &str = "api/v1//search";
28 pub const PATH_TOP_VOTES: &str = "api/v1/top-votes";
29 pub const PATH_SONG_BRIDGE_DETAIL: &str = "api/v1/song-bridge/{providerId}/detail";
30 +pub const PATH_SONG_BRIDGE_VOTE: &str = "api/v1/song-bridge/{providerId}/vote";
31
32 pub fn base_url_builder() -> URLBuilder {
33 let mut url_builder = URLBuilder::new();
frontend/splitfire-desktop/src-tauri/src/main.rs
+5 -6
@@ -22,13 +22,10 @@ use app::{
22 },
23 rest::{
24 account::{
25 - __cmd__account_login, __cmd__account_logout, __cmd__account_register, account_login,
26 - account_logout, account_register,
25 + __cmd__account_login, __cmd__account_logout, __cmd__account_profile, __cmd__account_register, account_login, account_logout, account_profile, account_register
26 },
27 content::{
29 - __cmd__content_carousel, __cmd__content_ready_to_play,
30 - __cmd__content_song_bridge_detail, __cmd__content_top_voted, content_carousel,
31 - content_ready_to_play, content_song_bridge_detail, content_top_voted,
28 + __cmd__content_carousel, __cmd__content_ready_to_play, __cmd__content_song_bridge_detail, __cmd__content_song_bridge_vote, __cmd__content_top_voted, content_carousel, content_ready_to_play, content_song_bridge_detail, content_top_voted, content_song_bridge_vote
29 },
30 },
31 sfai_home_dir_path,
@@ -69,6 +66,7 @@ fn main() {
66 account_login,
67 account_logout,
68 account_register,
69 + account_profile,
70 open_lyrics_editor,
71 open_player,
72 player_record,
@@ -83,7 +81,8 @@ fn main() {
81 content_carousel,
82 content_ready_to_play,
83 content_song_bridge_detail,
86 - content_top_voted
84 + content_top_voted,
85 + content_song_bridge_vote
86 ])
87 .run(tauri::generate_context!())
88 .expect("Error while running tauri application.");
frontend/splitfire-desktop/src-tauri/src/models/account.rs
-1
@@ -51,7 +51,6 @@ pub struct RegisterResponse {
51 pub struct AccountProfileResponse {
52 pub status: TauriResponse,
53 pub message: String,
54 - pub access_token: Option<String>,
54 pub user: Option<User>,
55 }
56
frontend/splitfire-desktop/src-tauri/src/rest/account.rs
+42 -2
@@ -1,9 +1,9 @@
1 use crate::{
2 command::constants::{
3 - base_url_builder, PATH_ACCOUNT_LOGIN, PATH_ACCOUNT_LOGOUT, PATH_ACCOUNT_REGISTER,
3 + base_url_builder, PATH_ACCOUNT_LOGIN, PATH_ACCOUNT_LOGOUT, PATH_ACCOUNT_PROFILE, PATH_ACCOUNT_REGISTER
4 },
5 models::{
6 - account::{AccountLoginResponse, AccountRegisterResponse, LoginResponse, RegisterResponse},
6 + account::{AccountLoginResponse, AccountProfileResponse, AccountRegisterResponse, LoginResponse, ProfileResponse, RegisterResponse},
7 player::TauriResponse,
8 },
9 };
@@ -162,6 +162,46 @@ pub async fn account_register(
162 }
163 }
164
165 +#[tauri::command]
166 +pub async fn account_profile(user_id: String) -> AccountProfileResponse {
167 + debug!("Getting profile for user_id {}", user_id);
168 + // Get profile
169 + let url = account_url_builder(PATH_ACCOUNT_PROFILE).replace("{userId}", &user_id);
170 + let response = Client::new()
171 + .get(account_url_builder(&url))
172 + .send()
173 + .await;
174 +
175 + let response = match response {
176 + Ok(response) => response,
177 + Err(e) => {
178 + debug!("Failed to get response: {:?}", e);
179 + return AccountProfileResponse {
180 + status: TauriResponse::Error,
181 + message: e.to_string(),
182 + user: None,
183 + };
184 + }
185 + };
186 + let res: ProfileResponse = match response.json().await {
187 + Ok(json) => json,
188 + Err(e) => {
189 + debug!("Failed to parse response: {:?}", e);
190 + return AccountProfileResponse {
191 + status: TauriResponse::Error,
192 + message: e.to_string(),
193 + user: None,
194 + };
195 + }
196 + };
197 + debug!("Profile response: {:?}", res);
198 + AccountProfileResponse {
199 + status: TauriResponse::Success,
200 + message: res.message,
201 + user: res.user,
202 + }
203 +}
204 +
205 fn account_url_builder(path: &str) -> String {
206 let mut base_url = base_url_builder();
207 base_url.add_route(path);
frontend/splitfire-desktop/src-tauri/src/rest/content.rs
+61 -2
@@ -1,17 +1,19 @@
1 use crate::{
2 command::constants::{
3 - base_url_builder, PATH_CAROUSEL, PATH_READY_TO_PLAY, PATH_SONG_BRIDGE_DETAIL, PATH_TOP_VOTES,
3 + base_url_builder, PATH_CAROUSEL, PATH_READY_TO_PLAY, PATH_SONG_BRIDGE_DETAIL,
4 + PATH_SONG_BRIDGE_VOTE, PATH_TOP_VOTES,
5 },
6 models::{
7 content::{
8 CarouselResponse, ContentCarouselResponse, ContentSongBridgeResponse,
8 - SongBridgeResponse,
9 + SongBridgeResponse, VoteType,
10 },
11 player::TauriResponse,
12 },
13 };
14 use log::{debug, error};
15 use reqwest::Client;
16 +use serde_json::json;
17
18 #[tauri::command]
19 pub async fn content_carousel() -> ContentCarouselResponse {
@@ -173,6 +175,63 @@ pub async fn content_top_voted() -> ContentCarouselResponse {
175 }
176 }
177
178 +#[tauri::command]
179 +pub async fn content_song_bridge_vote(
180 + song_provider_id: String,
181 + vote_type: VoteType,
182 + access_token: String,
183 +) -> ContentSongBridgeResponse {
184 + debug!("Song provider id: {:?}", song_provider_id);
185 + let url = content_url_builder(PATH_SONG_BRIDGE_VOTE).replace("{providerId}", &song_provider_id);
186 + let body = json!({ "vote_type": vote_type, "provider_id": song_provider_id }).to_string();
187 + let response: Result<reqwest::Response, reqwest::Error> = Client::new()
188 + .post(url)
189 + .body(body)
190 + .header("Authorization", format!("Bearer {}", access_token))
191 + .send()
192 + .await;
193 +
194 + let response = match response {
195 + Ok(response) => response,
196 + Err(e) => {
197 + error!("Failed to get response: {:?}", e);
198 + return ContentSongBridgeResponse {
199 + code: TauriResponse::Error,
200 + message: e.to_string(),
201 + song_provider: None,
202 + votes: vec![],
203 + };
204 + }
205 + };
206 +
207 + let res: SongBridgeResponse = match response.json().await {
208 + Ok(json) => json,
209 + Err(e) => {
210 + error!("Failed to parse response: {:?}", e);
211 + return ContentSongBridgeResponse {
212 + code: TauriResponse::Error,
213 + message: e.to_string(),
214 + song_provider: None,
215 + votes: vec![],
216 + };
217 + }
218 + };
219 +
220 + debug!("Song bridge detail: {:?}", res);
221 +
222 + let votes = match res.votes {
223 + Some(votes) => votes,
224 + None => vec![],
225 + };
226 +
227 + ContentSongBridgeResponse {
228 + code: TauriResponse::Success,
229 + message: res.message,
230 + song_provider: res.song_provider,
231 + votes,
232 + }
233 +}
234 +
235 fn content_url_builder(path: &str) -> String {
236 let mut url_builder = base_url_builder();
237 url_builder.add_route(path);