Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/finalize UI #11

Merged
merged 16 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug movies-db-ui",
"request": "launch",
"type": "chrome",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/movies-db-ui",
},
{
"type": "lldb",
"request": "launch",
Expand Down
1 change: 1 addition & 0 deletions movies-db-service/movies-db/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ serde_json = "1.0"
async-trait = "0.1"
actix-cors = "0.6"
rusqlite = { version = "0.29", features = ["bundled"] }
serde_qs = { version = "0.12", features = ["actix4"]}

[dev-dependencies]
tempdir = "0.3"
Expand Down
3 changes: 3 additions & 0 deletions movies-db-service/movies-db/src/db/movies_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ pub trait MoviesIndex: Send + Sync {
/// # Arguments
/// `query` - The query to search for.
async fn search_movies(&self, query: MovieSearchQuery) -> Result<Vec<MovieId>, Error>;

/// Returns a list of all tags with the number of movies associated with each tag.
async fn get_tag_list_with_count(&self) -> Result<Vec<(String, usize)>, Error>;
}

#[cfg(test)]
Expand Down
18 changes: 18 additions & 0 deletions movies-db-service/movies-db/src/db/simple_movies_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,24 @@ impl MoviesIndex for SimpleMoviesIndex {

Ok(movie_ids)
}

async fn get_tag_list_with_count(&self) -> Result<Vec<(String, usize)>, Error> {
info!("Getting tag list with count");

let mut tag_map: HashMap<String, usize> = HashMap::new();

for movie in self.movies.values() {
for tag in movie.movie.tags.iter() {
let count = tag_map.entry(tag.clone()).or_insert(0);
*count += 1;
}
}

let mut tag_list: Vec<(String, usize)> = tag_map.into_iter().collect();
tag_list.sort_unstable_by(|(_, lhs), (_, rhs)| rhs.cmp(lhs));

Ok(tag_list)
}
}

impl SimpleMoviesIndex {
Expand Down
22 changes: 22 additions & 0 deletions movies-db-service/movies-db/src/db/sqlite_movies_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,28 @@ impl MoviesIndex for SqliteMoviesIndex {
async fn search_movies(&self, query: MovieSearchQuery) -> Result<Vec<MovieId>, Error> {
self.search_movies_impl(query).await
}

async fn get_tag_list_with_count(&self) -> Result<Vec<(String, usize)>, Error> {
let connection = self.connection.lock().await;

let mut stmt = connection.prepare(
"SELECT tag, COUNT(*) FROM tags GROUP BY tag ORDER BY COUNT(*) DESC, tag ASC",
)?;

let rows = stmt.query_map([], |row| {
let tag: String = row.get(0)?;
let count: usize = row.get(1)?;

Ok((tag, count))
})?;

let mut tags: Vec<(String, usize)> = Vec::new();
for row in rows {
tags.push(row?);
}

Ok(tags)
}
}

#[cfg(test)]
Expand Down
17 changes: 17 additions & 0 deletions movies-db-service/movies-db/src/service/service_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,9 @@ where
}

/// Handles the request to show the list of all movies.
///
/// # Arguments
/// * `query` - The query to search for.
pub async fn handle_search_movies(&self, query: MovieSearchQuery) -> Result<impl Responder> {
let movie_ids = match self.index.read().await.search_movies(query).await {
Ok(movie_ids) => movie_ids,
Expand All @@ -525,6 +528,20 @@ where
Ok(web::Json(movies))
}

/// Handles the request to get a list of all tags with the number of movies associated with
/// each tag.
pub async fn handle_get_tags(&self) -> Result<impl Responder> {
let tags = match self.index.read().await.get_tag_list_with_count().await {
Ok(tags) => tags,
Err(err) => {
error!("Error getting tags: {}", err);
return Self::handle_error(err);
}
};

Ok(web::Json(tags))
}

/// Handles the given error by translating it into an actix-web error response.
///
/// # Arguments
Expand Down
18 changes: 17 additions & 1 deletion movies-db-service/movies-db/src/service/service_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use actix_multipart::Multipart;
use actix_web::{http::header, web, App, HttpServer, Responder, Result};

use log::{debug, error, info, trace};
use serde_qs::actix::QsQuery;
use tokio::sync::{mpsc, RwLock};

use crate::{
Expand Down Expand Up @@ -104,6 +105,7 @@ where
.route("/movie", web::get().to(Self::handle_get_movie))
.route("/movie", web::delete().to(Self::handle_delete_movie))
.route("/movie/search", web::get().to(Self::handle_search_movie))
.route("/movie/tags", web::get().to(Self::handle_get_tags))
.route("/movie/file", web::post().to(Self::handle_upload_movie))
.route("/movie/file", web::get().to(Self::handle_download_movie))
.route(
Expand Down Expand Up @@ -188,7 +190,7 @@ where
/// * `query` - The query parameters.
async fn handle_search_movie(
handler: web::Data<RwLock<ServiceHandler<I, S>>>,
query: web::Query<MovieSearchQuery>,
query: QsQuery<MovieSearchQuery>,
) -> Result<impl Responder> {
debug!("Handling GET /api/v1/movie/search");
trace!("Request query: {:?}", query);
Expand All @@ -200,6 +202,20 @@ where
handler.handle_search_movies(query).await
}

/// Handles the GET /api/v1/tags endpoint.
///
/// # Arguments
/// * `handler` - The service handler.
async fn handle_get_tags(
handler: web::Data<RwLock<ServiceHandler<I, S>>>,
) -> Result<impl Responder> {
debug!("Handling GET /api/v1/movie/tags");

let handler = handler.read().await;

handler.handle_get_tags().await
}

/// Handles the GET /api/v1/movie endpoint.
///
/// # Arguments
Expand Down
34 changes: 31 additions & 3 deletions movies-db-ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as React from 'react';
import './App.css';
import AddVideoDialog from './components/AddVideoDialog';
import { AppBar, Box, Button, Toolbar } from '@mui/material';
import { AppBar, Box, Button, Toolbar, Typography } from '@mui/material';
import { MovieSubmit } from './service/types';
import { service } from './service/service';
import LoadingDialog, { LoadingDialogProps } from './components/LoadingDialog';
import VideosList from './components/VideosList';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import AddIcon from '@mui/icons-material/Add';
import Search from './components/Search';

const theme = createTheme({
palette: {
Expand Down Expand Up @@ -44,8 +46,34 @@ function App() {
<div>
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<Button color="primary" onClick={() => setVideoDialogOpen(true)}>Add Video</Button>
<Toolbar sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
<Box sx={{ flexGrow: 1 }}>
<Typography
variant="h6"
noWrap
component="a"
href="/"
sx={{
mr: 2,
display: { xs: 'none', md: 'flex' },
fontFamily: 'monospace',
fontWeight: 700,
letterSpacing: '.2rem',
color: 'inherit',
textDecoration: 'none',
}}
>
Movie DB
</Typography>
</Box>
<Box sx={{ flexGrow: 2 }}>
<Search onSearch={s => service.setSearchString(s)} />
</Box>
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'flex-end' }}>
<Button color="primary" variant='contained' onClick={() => setVideoDialogOpen(true)} startIcon={<AddIcon />}>
Add Video
</Button>
</Box>
</Toolbar>
</AppBar>
</Box>
Expand Down
43 changes: 43 additions & 0 deletions movies-db-ui/src/components/Search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react';
import InputBase from '@mui/material/InputBase';
import SearchIcon from '@mui/icons-material/Search';
import { IconButton, MenuItem, Paper } from '@mui/material';

export interface SearchProps {
onSearch?: (query: string) => void;
}

export default function Search(props: SearchProps) {
const [query, setQuery] = React.useState("");

const handleSearch = () => {
if (props.onSearch) {
props.onSearch(query);
}
};

return (
<Paper
sx={{ p: '2px 4px', flexGrow: 1, display: 'flex', alignItems: 'center' }}
>
<IconButton sx={{ p: '10px' }} aria-label="menu">
<MenuItem />
</IconButton>
<InputBase
value={query}
sx={{ ml: 1, flex: 1 }}
placeholder="Search"
inputProps={{ 'aria-label': 'search' }}
onChange={e => setQuery(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') {
handleSearch();
}
}}
/>
<IconButton type="button" sx={{ p: '10px' }} onClick={() => handleSearch()}>
<SearchIcon />
</IconButton>
</Paper>
);
}
6 changes: 3 additions & 3 deletions movies-db-ui/src/components/VideoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function VideoCard(props: VideoCardProps): JSX.Element {
};

return (
<Card elevation={3} sx={{ maxWidth: 345 }}>
<Card elevation={3} sx={{ width: 345, height: 448 }}>
<CardHeader
action={
<IconButton aria-label="settings" onClick={handleOnDelete}>
Expand All @@ -84,9 +84,9 @@ export default function VideoCard(props: VideoCardProps): JSX.Element {
</Typography>
</CardContent>
</CardActionArea>
<CardActions disableSpacing>
<CardActions >
{movie.tags ? movie.tags.map((tag, index) => {
return <Chip key={index} label={tag} />
return <Chip key={index} size="small" label={tag} />
}) : <div></div>}
</CardActions>

Expand Down
Loading
Loading