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">
<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>
<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 }"
:disabled="isRequesting" @click="request">{{isRequesting ? "Requesting..." : "Request"}}</button>
:disabled="isRequesting" @click="request">{{isRequesting ? "Processing..." : "Request"}}</button>
</div>
</div>
</div>
@ -43,10 +47,14 @@
<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"
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"
:disabled="isRequesting" @click="request">{{isRequesting ? "Requesting..." : "Request"}}</button>
:disabled="isRequesting" @click="request">{{isRequesting ? "Processing..." : "Request"}}</button>
</div>
</div>
</div>
@ -62,12 +70,14 @@ import { defineComponent } from "vue";
import { getAverageRgb } from "../../util";
import { useStore, Mutation, Action } from "../../store";
import MoviePoster from "../MoviePoster.vue";
import { mapGetters } from "vuex";
export default defineComponent({
components: {
MoviePoster
},
computed: {
...mapGetters(["userId"]),
backdropOverlayStyle(): string {
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)`;
@ -166,18 +176,37 @@ export default defineComponent({
}
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() {
if (this.isRequesting || this.movie == null || this.movie.tmdbId == null) {
return;
}
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;
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;
}
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() {
@ -194,7 +223,7 @@ export default defineComponent({
}
},
watch: {
tmdbId(newId: number|string|undefined, oldId: number|string|undefined) {
tmdbId() {
this.fetchMovieDetails();
}
}


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

@ -35,7 +35,8 @@ export enum Action {
// Movies Methods
ActiveMovieRequests = "ACTIVE_MOVIE_REQUESTS",
MovieDetails = "MOVIE_DETAILS",
RequestMovie = "REQUEST_MOVIE",
CancelMovieRequest = "CANCEL_MOVIE_REQUEST",
CreateMovieRequest = "CREATE_MOVIE_REQUEST",
SearchMovies = "SEARCH_MOVIES"
}
@ -60,7 +61,8 @@ export type ActionsTypes = {
[Action.ActiveMovieRequests]: () => Promise<[number, IApiDataResponse<IMovie[]>]>,
[Action.MovieDetails] : (tmdbId: number | string) => Promise<[number, IApiDataResponse<IMovieDetails>]>,
[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}) {
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) {
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, {
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;
},
/**
* Retrieve the user's ID
*/
userId(state: IState) {
return state.user?.id;
},
/**
* Get the user's name (assumes authenticated)
*/


Loading…
Cancel
Save