Browse Source

Implement movie request canceling in web UI

dev
David Ludwig 4 years ago
parent
commit
23e852f9d9
3 changed files with 63 additions and 16 deletions
  1. +38
    -9
      services/webui/src/app/components/modals/MovieModal.vue
  2. +18
    -7
      services/webui/src/app/store/actions.ts
  3. +7
    -0
      services/webui/src/app/store/getters.ts

+ 38
- 9
services/webui/src/app/components/modals/MovieModal.vue View File

@ -26,10 +26,14 @@
<div class="mt-4 hidden md:block"> <div class="mt-4 hidden md:block">
<a class="inline-block py-2 px-6 rounded-full shadow-md hover:shadow-lg focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }" <a class="inline-block py-2 px-6 rounded-full shadow-md hover:shadow-lg focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
v-if="movie.plexLink !== null" :href="movie.plexLink" target="_blank">Watch Now</a> v-if="movie.plexLink !== null" :href="movie.plexLink" target="_blank">Watch Now</a>
<button class="inline-block py-2 px-4 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
v-else-if="movie.ticketId !== null">View Request Status</button>
<template v-else-if="movie.requestedBy !== null">
<button class="inline-block py-2 px-6 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
v-if="movie.requestedBy.id == userId" @click="cancel">Cancel Request</button>
<button class="inline-block py-2 px-6 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
v-else disabled>Requested by Another User</button>
</template>
<button v-else class="inline-block py-2 w-36 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }" <button v-else class="inline-block py-2 w-36 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
:disabled="isRequesting" @click="request">{{isRequesting ? "Requesting..." : "Request"}}</button>
:disabled="isRequesting" @click="request">{{isRequesting ? "Processing..." : "Request"}}</button>
</div> </div>
</div> </div>
</div> </div>
@ -43,10 +47,14 @@
<div class="text-center"> <div class="text-center">
<a class="inline-block py-2 px-8 rounded-full shadow-sm bg-red-500 hover:bg-black transition-colors text-white focus:outline-none ring-0" <a class="inline-block py-2 px-8 rounded-full shadow-sm bg-red-500 hover:bg-black transition-colors text-white focus:outline-none ring-0"
v-if="movie.plexLink !== null" :href="movie.plexLink">Watch Now</a> v-if="movie.plexLink !== null" :href="movie.plexLink">Watch Now</a>
<button class="inline-block py-2 px-4 rounded-full bg-red-500 text-white disabled:opacity-50"
v-else-if="movie.ticketId !== null">View Request Status</button>
<template v-if="movie.requestedBy !== null">
<button class="inline-block py-2 px-8 rounded-full bg-red-500 text-white disabled:opacity-50" @click="cancel"
v-if="movie.requestedBy.id == userId">Cancel Request</button>
<button class="inline-block py-2 px-8 rounded-full bg-red-500 text-white disabled:opacity-50" disabled
v-else>Requested by Another User</button>
</template>
<button v-else class="inline-block py-2 px-8 rounded-full shadow-sm bg-red-500 hover:bg-black transition-colors text-white disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 disabled:bg-black" <button v-else class="inline-block py-2 px-8 rounded-full shadow-sm bg-red-500 hover:bg-black transition-colors text-white disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 disabled:bg-black"
:disabled="isRequesting" @click="request">{{isRequesting ? "Requesting..." : "Request"}}</button>
:disabled="isRequesting" @click="request">{{isRequesting ? "Processing..." : "Request"}}</button>
</div> </div>
</div> </div>
</div> </div>
@ -62,12 +70,14 @@ import { defineComponent } from "vue";
import { getAverageRgb } from "../../util"; import { getAverageRgb } from "../../util";
import { useStore, Mutation, Action } from "../../store"; import { useStore, Mutation, Action } from "../../store";
import MoviePoster from "../MoviePoster.vue"; import MoviePoster from "../MoviePoster.vue";
import { mapGetters } from "vuex";
export default defineComponent({ export default defineComponent({
components: { components: {
MoviePoster MoviePoster
}, },
computed: { computed: {
...mapGetters(["userId"]),
backdropOverlayStyle(): string { backdropOverlayStyle(): string {
let { r, g, b } = this.rgb; let { r, g, b } = this.rgb;
return `background-image: linear-gradient(to right, rgba(${r}, ${g}, ${b}, 1.0), rgba(${r}, ${g}, ${b}, 0.84)`; return `background-image: linear-gradient(to right, rgba(${r}, ${g}, ${b}, 1.0), rgba(${r}, ${g}, ${b}, 0.84)`;
@ -166,18 +176,37 @@ export default defineComponent({
} }
this.movie = response.result; this.movie = response.result;
}, },
async cancel() {
if (this.isRequesting || this.movie == null || this.movie.ticketId == null || this.movie.requestedBy?.id != this.userId) {
return;
}
this.isRequesting = true;
let [status, response] = await this.$store.dispatch(Action.CancelMovieRequest, this.movie.ticketId);
this.isRequesting = false;
if (status != Status.Ok) {
console.error("Failed to cancel movie request");
return;
}
this.movie.ticketId = null;
this.movie.requestedBy = null;
},
async request() { async request() {
if (this.isRequesting || this.movie == null || this.movie.tmdbId == null) { if (this.isRequesting || this.movie == null || this.movie.tmdbId == null) {
return; return;
} }
this.isRequesting = true; this.isRequesting = true;
let [status, response] = await this.$store.dispatch(Action.RequestMovie, this.movie.tmdbId);
let [status, response] = await this.$store.dispatch(Action.CreateMovieRequest, this.movie.tmdbId);
this.isRequesting = false; this.isRequesting = false;
if (status == Status.Forbidden) { if (status == Status.Forbidden) {
console.log("Failed to add movie: quota has been met");
console.error("Failed to add movie: quota has been met");
return; return;
} }
this.movie.ticketId = response.result.ticketId; this.movie.ticketId = response.result.ticketId;
this.movie.requestedBy = {
id : this.$store.state.user!.id,
isAdmin: this.$store.state.user!.isAdmin,
name : this.$store.state.user!.name,
}
} }
}, },
mounted() { mounted() {
@ -194,7 +223,7 @@ export default defineComponent({
} }
}, },
watch: { watch: {
tmdbId(newId: number|string|undefined, oldId: number|string|undefined) {
tmdbId() {
this.fetchMovieDetails(); this.fetchMovieDetails();
} }
} }


+ 18
- 7
services/webui/src/app/store/actions.ts View File

@ -35,7 +35,8 @@ export enum Action {
// Movies Methods // Movies Methods
ActiveMovieRequests = "ACTIVE_MOVIE_REQUESTS", ActiveMovieRequests = "ACTIVE_MOVIE_REQUESTS",
MovieDetails = "MOVIE_DETAILS", MovieDetails = "MOVIE_DETAILS",
RequestMovie = "REQUEST_MOVIE",
CancelMovieRequest = "CANCEL_MOVIE_REQUEST",
CreateMovieRequest = "CREATE_MOVIE_REQUEST",
SearchMovies = "SEARCH_MOVIES" SearchMovies = "SEARCH_MOVIES"
} }
@ -60,7 +61,8 @@ export type ActionsTypes = {
[Action.ActiveMovieRequests]: () => Promise<[number, IApiDataResponse<IMovie[]>]>, [Action.ActiveMovieRequests]: () => Promise<[number, IApiDataResponse<IMovie[]>]>,
[Action.MovieDetails] : (tmdbId: number | string) => Promise<[number, IApiDataResponse<IMovieDetails>]>, [Action.MovieDetails] : (tmdbId: number | string) => Promise<[number, IApiDataResponse<IMovieDetails>]>,
[Action.SearchMovies] : (query: string) => Promise<[number, IPaginatedResponse<IMovie>]>, [Action.SearchMovies] : (query: string) => Promise<[number, IPaginatedResponse<IMovie>]>,
[Action.RequestMovie] : (tmdbId: number | string) => Promise<[number, IApiDataResponse<{ ticketId: number }>]>
[Action.CancelMovieRequest] : (ticketId: number) => Promise<[number, IApiResponse]>,
[Action.CreateMovieRequest] : (tmdbId: number | string) => Promise<[number, IApiDataResponse<{ ticketId: number }>]>
} }
/** /**
@ -232,7 +234,7 @@ export const actions: Actions<IState, GettersTypes, MutationsTypes, ActionsTypes
*/ */
async [Action.ActiveMovieRequests]({dispatch}) { async [Action.ActiveMovieRequests]({dispatch}) {
return await dispatch(Action.Get, { return await dispatch(Action.Get, {
path: `/api/movie/request/tickets/active`
path: `/api/movie/request/active`
}); });
}, },
@ -250,16 +252,25 @@ export const actions: Actions<IState, GettersTypes, MutationsTypes, ActionsTypes
*/ */
async [Action.SearchMovies]({dispatch}, query: string) { async [Action.SearchMovies]({dispatch}, query: string) {
return await dispatch(Action.Get, { return await dispatch(Action.Get, {
path: `api/movie/search?query=${encodeURI(query)}`
path: `/api/movie/search?query=${encodeURI(query)}`
}); });
}, },
/** /**
* Request a movie
* Cancel a movie request
*/ */
async [Action.RequestMovie]({dispatch}, tmdbId: string|number) {
async [Action.CancelMovieRequest]({dispatch}, ticketId: number) {
return await dispatch(Action.Delete, {
path: `/api/movie/request/${ticketId}`
});
},
/**
* Create a movie request
*/
async [Action.CreateMovieRequest]({dispatch}, tmdbId: string|number) {
return await dispatch(Action.Post, { return await dispatch(Action.Post, {
path: `/api/movie/request/create/tmdb/${tmdbId}`
path: `/api/movie/request/create/${tmdbId}`
}); });
} }
}; };

+ 7
- 0
services/webui/src/app/store/getters.ts View File

@ -38,6 +38,13 @@ export const getters: GettersTypes & GetterTree<IState, IState> = {
return state.user?.token ?? null; return state.user?.token ?? null;
}, },
/**
* Retrieve the user's ID
*/
userId(state: IState) {
return state.user?.id;
},
/** /**
* Get the user's name (assumes authenticated) * Get the user's name (assumes authenticated)
*/ */


Loading…
Cancel
Save