diff --git a/.editorconfig b/.editorconfig index 2fdc6187..955a6192 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,6 @@ [*] -charset=utf-8-bom +charset=utf-8 end_of_line=lf trim_trailing_whitespace=false insert_final_newline=false diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs index eefd03e2..af6489fa 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs @@ -20,7 +20,7 @@ public class CreateFFmpegProfileHandler : { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, profile => PersistFFmpegProfile(dbContext, profile)); + return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile)); } private static async Task PersistFFmpegProfile( diff --git a/ErsatzTV.Application/FFmpegProfiles/Mapper.cs b/ErsatzTV.Application/FFmpegProfiles/Mapper.cs index a92d2710..820640a5 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Mapper.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Mapper.cs @@ -35,6 +35,27 @@ internal static class Mapper ffmpegProfile.VideoFormat.ToString().ToLowerInvariant(), ffmpegProfile.AudioFormat.ToString().ToLowerInvariant()); + internal static FFmpegFullProfileResponseModel ProjectToFullResponseModel(FFmpegProfile ffmpegProfile) => + new( + ffmpegProfile.Id, + ffmpegProfile.Name, + ffmpegProfile.ThreadCount, + (int)ffmpegProfile.HardwareAcceleration, + (int)ffmpegProfile.VaapiDriver, + ffmpegProfile.VaapiDevice, + ffmpegProfile.ResolutionId, + (int)ffmpegProfile.VideoFormat, + ffmpegProfile.VideoBitrate, + ffmpegProfile.VideoBufferSize, + (int)ffmpegProfile.AudioFormat, + ffmpegProfile.AudioBitrate, + ffmpegProfile.AudioBufferSize, + ffmpegProfile.NormalizeLoudness, + ffmpegProfile.AudioChannels, + ffmpegProfile.AudioSampleRate, + ffmpegProfile.NormalizeFramerate, + ffmpegProfile.DeinterlaceVideo); + private static ResolutionViewModel Project(Resolution resolution) => new(resolution.Id, resolution.Name, resolution.Width, resolution.Height); } diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdForApi.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdForApi.cs new file mode 100644 index 00000000..da9a0480 --- /dev/null +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdForApi.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core.Api.FFmpegProfiles; + +namespace ErsatzTV.Application.FFmpegProfiles; + +public record GetFFmpegFullProfileByIdForApi(int Id) : IRequest>; diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdForApiHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdForApiHandler.cs new file mode 100644 index 00000000..49250f56 --- /dev/null +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdForApiHandler.cs @@ -0,0 +1,28 @@ +using ErsatzTV.Core.Api.FFmpegProfiles; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; +using static ErsatzTV.Application.FFmpegProfiles.Mapper; + +namespace ErsatzTV.Application.FFmpegProfiles; + +public class + GetFFmpegProfileByIdForApiHandler : IRequestHandler> +{ + private readonly IDbContextFactory _dbContextFactory; + + public GetFFmpegProfileByIdForApiHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetFFmpegFullProfileByIdForApi request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + return await dbContext.FFmpegProfiles + .Include(p => p.Resolution) + .SelectOneAsync(p => p.Id, p => p.Id == request.Id) + .MapT(ProjectToFullResponseModel); + } +} diff --git a/ErsatzTV.Core/Api/FFmpegProfiles/FFmpegFullProfileResponseModel.cs b/ErsatzTV.Core/Api/FFmpegProfiles/FFmpegFullProfileResponseModel.cs new file mode 100644 index 00000000..da76c6d0 --- /dev/null +++ b/ErsatzTV.Core/Api/FFmpegProfiles/FFmpegFullProfileResponseModel.cs @@ -0,0 +1,21 @@ +namespace ErsatzTV.Core.Api.FFmpegProfiles; + +public record FFmpegFullProfileResponseModel( + int Id, + string Name, + int ThreadCount, + int HardwareAcceleration, + int VaapiDriver, + string VaapiDevice, + int ResolutionId, + int VideoFormat, + int VideoBitrate, + int VideoBufferSize, + int AudioFormat, + int AudioBitrate, + int AudioBufferSize, + bool NormalizeLoudness, + int AudioChannels, + int AudioSampleRate, + bool NormalizeFramerate, + bool? DeinterlaceVideo); diff --git a/ErsatzTV/Controllers/Api/FFmpegProfileController.cs b/ErsatzTV/Controllers/Api/FFmpegProfileController.cs index 95837f37..21f14a52 100644 --- a/ErsatzTV/Controllers/Api/FFmpegProfileController.cs +++ b/ErsatzTV/Controllers/Api/FFmpegProfileController.cs @@ -1,4 +1,6 @@ -using ErsatzTV.Application.FFmpegProfiles; +using System.ComponentModel.DataAnnotations; +using ErsatzTV.Application.FFmpegProfiles; +using ErsatzTV.Core; using ErsatzTV.Core.Api.FFmpegProfiles; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -15,4 +17,21 @@ public class FFmpegProfileController [HttpGet("/api/ffmpeg/profiles")] public async Task> GetAll() => await _mediator.Send(new GetAllFFmpegProfilesForApi()); + + [HttpPost("/api/ffmpeg/profiles/new")] + public async Task> AddOne( + [Required] [FromBody] + CreateFFmpegProfile request) => await _mediator.Send(request); + + [HttpPut("/api/ffmpeg/profiles/update")] + public async Task> UpdateOne( + [Required] [FromBody] + UpdateFFmpegProfile request) => await _mediator.Send(request); + + [HttpGet("/api/ffmpeg/profiles/{id}")] + public async Task> GetOne(int id) => + await _mediator.Send(new GetFFmpegFullProfileByIdForApi(id)); + + [HttpDelete("/api/ffmpeg/delete/{id}")] + public async Task DeleteProfileAsync(int id) => await _mediator.Send(new DeleteFFmpegProfile(id)); } diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 0c6cf415..1c3d52ce 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -116,4 +116,6 @@ + + diff --git a/ErsatzTV/client-app/.gitignore b/ErsatzTV/client-app/.gitignore index 403adbc1..cc55018b 100644 --- a/ErsatzTV/client-app/.gitignore +++ b/ErsatzTV/client-app/.gitignore @@ -21,3 +21,8 @@ pnpm-debug.log* *.njsproj *.sln *.sw? + +# generated files +src/assets/css/*.css +src/models/*.js +src/models/*.js.map diff --git a/ErsatzTV/client-app/package-lock.json b/ErsatzTV/client-app/package-lock.json index dc01d47d..29116cb4 100644 --- a/ErsatzTV/client-app/package-lock.json +++ b/ErsatzTV/client-app/package-lock.json @@ -9,16 +9,19 @@ "version": "0.1.0", "dependencies": { "@mdi/font": "5.9.55", + "@sweetalert2/theme-dark": "^5.0.11", "axios": "^0.26.1", "core-js": "^3.8.3", "pinia": "^2.0.11", "pinia-plugin-persistedstate": "^1.5.1", "roboto-fontface": "*", + "sweetalert2": "^11.4.17", "vue": "^2.6.14", "vue-class-component": "^7.2.6", "vue-i18n": "^8.26.3", "vue-property-decorator": "^9.1.2", "vue-router": "^3.2.0", + "vue-sweetalert2": "^5.0.5", "vuetify": "^2.6.0" }, "devDependencies": { @@ -1965,6 +1968,11 @@ "integrity": "sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==", "dev": true }, + "node_modules/@sweetalert2/theme-dark": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@sweetalert2/theme-dark/-/theme-dark-5.0.11.tgz", + "integrity": "sha512-yw42SqvvFc1ObRVLNLSo5HpJo3vDTYlpS2ba+JkWHRcVIEXDnEOaDfAVRtDPSK75GJkvdz9IdfUrc18zdmiE2w==" + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -10843,6 +10851,15 @@ "node": ">= 10" } }, + "node_modules/sweetalert2": { + "version": "11.4.17", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.4.17.tgz", + "integrity": "sha512-z0iQW5fDdvtoDNle3iHTKunsuNWgPaMifVu0GPtdguR0uVPrvg9gg9bnSQeJ4tx3C7FwJOEnRshxrs6fFItRvw==", + "funding": { + "type": "individual", + "url": "https://sweetalert2.github.io/#donations" + } + }, "node_modules/table": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", @@ -11936,6 +11953,17 @@ "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=", "dev": true }, + "node_modules/vue-sweetalert2": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vue-sweetalert2/-/vue-sweetalert2-5.0.5.tgz", + "integrity": "sha512-Q7+TjQxNwGruT2jfPaSKMz18HHWs81r4umEwlrrtOf/QL5Iwl1T+tUaNkcvKB7Rf9rYftubt7NnOMjfDMnDH3A==", + "dependencies": { + "sweetalert2": "11.x" + }, + "peerDependencies": { + "vue": "*" + } + }, "node_modules/vue-template-compiler": { "version": "2.6.14", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz", @@ -14142,6 +14170,11 @@ "integrity": "sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==", "dev": true }, + "@sweetalert2/theme-dark": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@sweetalert2/theme-dark/-/theme-dark-5.0.11.tgz", + "integrity": "sha512-yw42SqvvFc1ObRVLNLSo5HpJo3vDTYlpS2ba+JkWHRcVIEXDnEOaDfAVRtDPSK75GJkvdz9IdfUrc18zdmiE2w==" + }, "@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -20737,6 +20770,11 @@ } } }, + "sweetalert2": { + "version": "11.4.17", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.4.17.tgz", + "integrity": "sha512-z0iQW5fDdvtoDNle3iHTKunsuNWgPaMifVu0GPtdguR0uVPrvg9gg9bnSQeJ4tx3C7FwJOEnRshxrs6fFItRvw==" + }, "table": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", @@ -21552,6 +21590,14 @@ } } }, + "vue-sweetalert2": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vue-sweetalert2/-/vue-sweetalert2-5.0.5.tgz", + "integrity": "sha512-Q7+TjQxNwGruT2jfPaSKMz18HHWs81r4umEwlrrtOf/QL5Iwl1T+tUaNkcvKB7Rf9rYftubt7NnOMjfDMnDH3A==", + "requires": { + "sweetalert2": "11.x" + } + }, "vue-template-compiler": { "version": "2.6.14", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz", diff --git a/ErsatzTV/client-app/package.json b/ErsatzTV/client-app/package.json index d4b5a936..0b23bd6f 100644 --- a/ErsatzTV/client-app/package.json +++ b/ErsatzTV/client-app/package.json @@ -10,16 +10,19 @@ }, "dependencies": { "@mdi/font": "5.9.55", + "@sweetalert2/theme-dark": "^5.0.11", "axios": "^0.26.1", "core-js": "^3.8.3", "pinia": "^2.0.11", "pinia-plugin-persistedstate": "^1.5.1", "roboto-fontface": "*", + "sweetalert2": "^11.4.17", "vue": "^2.6.14", "vue-class-component": "^7.2.6", "vue-i18n": "^8.26.3", "vue-property-decorator": "^9.1.2", "vue-router": "^3.2.0", + "vue-sweetalert2": "^5.0.5", "vuetify": "^2.6.0" }, "devDependencies": { diff --git a/ErsatzTV/client-app/src/assets/css/global.scss b/ErsatzTV/client-app/src/assets/css/global.scss new file mode 100644 index 00000000..b5af97c2 --- /dev/null +++ b/ErsatzTV/client-app/src/assets/css/global.scss @@ -0,0 +1,12 @@ +.swal2-container.swal2-center > .swal2-popup { + background-color: #1E1E1E !important; + border-color: green; + border-width: thin; + border-style: solid; + width: fit-content; +} + +.swal2-html-container, .swal2-title { + color: white !important; + font-family:Cambria; +} diff --git a/ErsatzTV/client-app/src/components/Navigation/SideBarMenu.vue b/ErsatzTV/client-app/src/components/Navigation/SideBarMenu.vue index 8a769bba..725fd818 100644 --- a/ErsatzTV/client-app/src/components/Navigation/SideBarMenu.vue +++ b/ErsatzTV/client-app/src/components/Navigation/SideBarMenu.vue @@ -2,14 +2,14 @@ ({ diff --git a/ErsatzTV/client-app/src/locales/en.json b/ErsatzTV/client-app/src/locales/en.json index 8a8a9a6d..7d6bc2d5 100644 --- a/ErsatzTV/client-app/src/locales/en.json +++ b/ErsatzTV/client-app/src/locales/en.json @@ -87,6 +87,13 @@ }, "ffmpeg-profiles": { "title": "FFmpeg Profiles", + "add-profile": "Add FFmpeg Profile", + "actions": "Actions", + "delete-dialog-title": "Are you sure?", + "delete-dialog-text": "Delete {profileName} FFmpeg Profile?", + "no": "No", + "yes": "Yes", + "profile-deleted": "FFmpeg Profile deleted!", "table": { "name": "Name", "resolution": "Resolution", @@ -94,6 +101,31 @@ "audio": "Audio" } }, + "edit-ffmpeg-profile": { + "General": "General", + "Video": "Video", + "Audio": "Audio", + "Name": "Name", + "thread-count": "Thread Count", + "preferred-resolution": "Preferred Resolution", + "format": "Format", + "bitrate": "Bitrate", + "buffer-size": "Buffer Size", + "hardware-acceleration": "Hardware Acceleration", + "vaapi-driver": "VAAPI Driver", + "vaapi-device": "VAAPI Device", + "normalize-framerate": "Normalize Framerate", + "auto-deinterlace-video": "Auto Deinterlace Video", + "channels": "Channels", + "sample-rate": "Sample Rate", + "normalize-loudness": "Normalize Loudness", + "save-profile": "Save Profile", + "cancel": "Cancel", + "help": "Help", + "add-profile": "Add FFmpeg Profile", + "edit-profile": "Edit FFmpeg Profile", + "profile-saved": "FFmpeg Profile Saved!" + }, "watermarks": { "title": "Watermarks" }, diff --git a/ErsatzTV/client-app/src/locales/pt-br.json b/ErsatzTV/client-app/src/locales/pt-br.json index 56da83c3..251bc1c8 100644 --- a/ErsatzTV/client-app/src/locales/pt-br.json +++ b/ErsatzTV/client-app/src/locales/pt-br.json @@ -2,6 +2,7 @@ "$vuetify": { "badge": "Distintivo", "close": "Fechar", + "Actions": "Ações", "dataIterator": { "noResultsText": "Nenhum dado encontrado", "loadingText": "Carregando itens..." @@ -87,10 +88,45 @@ }, "ffmpeg-profiles": { "title": "Perfis FFmpeg", + "add-profile": "Adicionar perfil FFmpeg", + "actions": "Ações", + "delete-dialog-title": "Tem certeza?", + "delete-dialog-text": "Excluir perfil {profileName} do FFmpeg?", + "no": "Não", + "yes": "Sim", + "profile-deleted": "Perfil do FFmpeg excluído!", "table": { - "name": "Nome" + "name": "Nome", + "resolution": "Resolução", + "video": "Vídeo", + "audio": "Áudio" } }, + "edit-ffmpeg-profile": { + "General": "Em geral", + "Video": "Vídeo", + "Audio": "Áudio", + "Name": "Nome", + "thread-count": "Contagem de fios", + "preferred-resolution": "Resolução preferencial", + "format": "Formato", + "bitrate": "Taxa de bits", + "buffer-size": "Tamanho do buffer", + "hardware-acceleration": "Aceleraçao do hardware", + "vaapi-driver": "Driver VAAPI", + "vaapi-device": "Dispositivo VAAPI", + "normalize-framerate": "Normalizar taxa de quadros", + "auto-deinterlace-video": "Vídeo de desentrelaçamento automático", + "channels": "Canais", + "sample-rate": "Taxa de amostragem", + "normalize-loudness": "Normalizar volume", + "save-profile": "Salvar perfil", + "cancel": "Cancelar", + "help": "Ajuda", + "add-profile": "Adicionar perfil FFmpeg", + "edit-profile": "Editar perfil do FFmpeg", + "profile-saved": "Perfil do FFmpeg salvo!" + }, "watermarks": { "title": "Marcas d'água" }, diff --git a/ErsatzTV/client-app/src/main.ts b/ErsatzTV/client-app/src/main.ts index c97631c6..731d4d7d 100644 --- a/ErsatzTV/client-app/src/main.ts +++ b/ErsatzTV/client-app/src/main.ts @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue from 'vue'; import App from './App.vue'; import vuetify from './plugins/vuetify'; import router from './router'; @@ -8,16 +8,26 @@ import autoPageTitleMixin from './mixins/autoPageTitle'; import 'roboto-fontface/css/roboto/roboto-fontface.css'; import '@mdi/font/css/materialdesignicons.css'; import i18n from './plugins/i18n'; +import VueSweetalert2 from 'vue-sweetalert2'; +import 'sweetalert2/dist/sweetalert2.min.css'; +import './assets/css/global.scss'; Vue.config.productionTip = false; Vue.use(PiniaVuePlugin); const pinia = createPinia(); pinia.use(piniaPluginPersistedstate); - // Mixin to automate the page title when navigating... Will default to "ErsatzTV if no title value exported from page. Vue.mixin(autoPageTitleMixin); +const options = { + confirmButtonColor: '#1E1E1E', + cancelButtonColor: '#1E1E1E', + background: '#1E1E1E', + iconColor: '#4CAF50' +}; +Vue.use(VueSweetalert2, options); + new Vue({ vuetify, router, diff --git a/ErsatzTV/client-app/src/models/FFmpegFullProfile.ts b/ErsatzTV/client-app/src/models/FFmpegFullProfile.ts new file mode 100644 index 00000000..d9552acc --- /dev/null +++ b/ErsatzTV/client-app/src/models/FFmpegFullProfile.ts @@ -0,0 +1,20 @@ +export interface FFmpegFullProfile { + Id: number; + name: string; + threadCount: number; + hardwareAcceleration: number; + vaapiDriver: number; + vaapiDevice: string; + resolutionId: number; + videoFormat: number; + videoBitrate: number; + videoBufferSize: number; + audioFormat: number; + audioBitrate: number; + audioBufferSize: number; + normalizeLoudness: boolean; + audioChannels: number; + audioSampleRate: number; + normalizeFramerate: boolean; + deinterlaceVideo: boolean; +} diff --git a/ErsatzTV/client-app/src/router/index.js b/ErsatzTV/client-app/src/router/index.js index 79a738a1..0428e2dd 100644 --- a/ErsatzTV/client-app/src/router/index.js +++ b/ErsatzTV/client-app/src/router/index.js @@ -3,6 +3,7 @@ import VueRouter from 'vue-router'; import HomePage from '../views/HomePage.vue'; import ChannelsPage from '../views/ChannelsPage.vue'; import FFmpegProfilesPage from '../views/FFmpegProfilesPage.vue'; +import AddEditFFmpegProfilePage from '../views/AddEditFFmpegProfilePage.vue'; Vue.use(VueRouter); @@ -32,7 +33,8 @@ const routes = [ meta: { icon: 'mdi-video-input-component', disabled: false - } + }, + showchildren: false }, { path: '/watermarks', @@ -49,6 +51,7 @@ const routes = [ icon: 'mdi-server-network', disabled: false }, + showchildren: true, children: [ { path: '/sources/local', @@ -91,6 +94,7 @@ const routes = [ icon: 'mdi-cog', disabled: false }, + showchildren: true, children: [ { path: '/media/libraries', @@ -157,6 +161,7 @@ const routes = [ icon: 'mdi-format-list-bulleted', disabled: false }, + showchildren: true, children: [ { path: '/lists/collections', @@ -215,6 +220,26 @@ const routes = [ icon: 'mdi-card-text', disabled: true } + }, + //hidden routes - used for non-menu routes + { + path: '/add-ffmpeg-profile', + name: 'add-ffmpeg-profile', + component: AddEditFFmpegProfilePage, + meta: { + disabled: false, + hidden: true + } + }, + { + path: '/edit-ffmpeg-profile', + name: 'edit-ffmpeg', + component: AddEditFFmpegProfilePage, + meta: { + disabled: false, + hidden: true + }, + props: (route) => ({ query: route.query.id }) } ]; diff --git a/ErsatzTV/client-app/src/services/FFmpegProfileService.ts b/ErsatzTV/client-app/src/services/FFmpegProfileService.ts index 121d235b..2bd1001b 100644 --- a/ErsatzTV/client-app/src/services/FFmpegProfileService.ts +++ b/ErsatzTV/client-app/src/services/FFmpegProfileService.ts @@ -1,5 +1,6 @@ import { AbstractApiService } from './AbstractApiService'; import { FFmpegProfile } from '@/models/FFmpegProfile'; +import { FFmpegFullProfile } from '../models/FFmpegFullProfile'; class FFmpegProfileApiService extends AbstractApiService { public constructor() { @@ -12,6 +13,101 @@ class FFmpegProfileApiService extends AbstractApiService { .then(this.handleResponse.bind(this)) .catch(this.handleError.bind(this)); } + + public getOne(id: string): Promise { + return this.http + .get('/api/ffmpeg/profiles/' + id) + .then(this.handleResponse.bind(this)) + .catch(this.handleError.bind(this)); + } + + public newFFmpegProfile( + Name: string, + ThreadCount: number, + HardwareAcceleration: number, + VaapiDriver: number, + VaapiDevice: string, + ResolutionId: number, + VideoFormat: number, + VideoBitrate: number, + VideoBufferSize: number, + AudioFormat: number, + AudioBitrate: number, + AudioBufferSize: number, + NormalizeLoudness: boolean, + AudioChannels: number, + AudioSampleRate: number, + NormalizeFramerate: boolean, + DeinterlaceVideo: boolean + ) { + const data = { + Name: Name, + ThreadCount: ThreadCount, + HardwareAcceleration: HardwareAcceleration, + VaapiDriver: VaapiDriver, + VaapiDevice: VaapiDevice, + ResolutionId: ResolutionId, + VideoFormat: VideoFormat, + VideoBitrate: VideoBitrate, + VideoBufferSize: VideoBufferSize, + AudioFormat: AudioFormat, + AudioBitrate: AudioBitrate, + AudioBufferSize: AudioBufferSize, + NormalizeLoudness: NormalizeLoudness, + AudioChannels: AudioChannels, + AudioSampleRate: AudioSampleRate, + NormalizeFramerate: NormalizeFramerate, + DeinterlaceVideo: DeinterlaceVideo + }; + this.http.post('/api/ffmpeg/profiles/new', data); + } + + public updateFFmpegProfile( + Id: number, + Name: string, + ThreadCount: number, + HardwareAcceleration: number, + VaapiDriver: number, + VaapiDevice: string, + ResolutionId: number, + VideoFormat: number, + VideoBitrate: number, + VideoBufferSize: number, + AudioFormat: number, + AudioBitrate: number, + AudioBufferSize: number, + NormalizeLoudness: boolean, + AudioChannels: number, + AudioSampleRate: number, + NormalizeFramerate: boolean, + DeinterlaceVideo: boolean + ) { + const data = { + Id: Id, + Name: Name, + ThreadCount: ThreadCount, + HardwareAcceleration: HardwareAcceleration, + VaapiDriver: VaapiDriver, + VaapiDevice: VaapiDevice, + ResolutionId: ResolutionId, + VideoFormat: VideoFormat, + VideoBitrate: VideoBitrate, + VideoBufferSize: VideoBufferSize, + AudioFormat: AudioFormat, + AudioBitrate: AudioBitrate, + AudioBufferSize: AudioBufferSize, + NormalizeLoudness: NormalizeLoudness, + AudioChannels: AudioChannels, + AudioSampleRate: AudioSampleRate, + NormalizeFramerate: NormalizeFramerate, + DeinterlaceVideo: DeinterlaceVideo + }; + this.http.put('/api/ffmpeg/profiles/update', data); + } + + public deleteRecord(id: string) { + this.http.delete('/api/ffmpeg/delete/' + id); + } } export const ffmpegProfileApiService: FFmpegProfileApiService = diff --git a/ErsatzTV/client-app/src/views/AddEditFFmpegProfilePage.vue b/ErsatzTV/client-app/src/views/AddEditFFmpegProfilePage.vue new file mode 100644 index 00000000..91933aca --- /dev/null +++ b/ErsatzTV/client-app/src/views/AddEditFFmpegProfilePage.vue @@ -0,0 +1,909 @@ + + + diff --git a/ErsatzTV/client-app/src/views/ChannelsPage.vue b/ErsatzTV/client-app/src/views/ChannelsPage.vue index 76c6ed7e..9608252e 100644 --- a/ErsatzTV/client-app/src/views/ChannelsPage.vue +++ b/ErsatzTV/client-app/src/views/ChannelsPage.vue @@ -5,7 +5,14 @@ :items="channels" :sort-by="['number']" class="elevation-1" - > + > + + @@ -28,10 +35,21 @@ export default class Channels extends Vue { { text: this.$t('channels.table.ffmpeg-profile'), value: 'ffmpegProfile' - } + }, + { text: 'Actions', value: 'actions', sortable: false } ]; } + deleteRow(item: any) { + let index = this.channels.findIndex((it) => it.id === item.id); + this.channels.splice(index, 1); + } + + editRow(item: any) { + let index = this.channels.findIndex((it) => it.id === item.id); + this.channels.splice(index, 1); + } + title: string = 'Channels'; async mounted(): Promise { diff --git a/ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue b/ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue index b601708f..2398885c 100644 --- a/ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue +++ b/ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue @@ -1,11 +1,23 @@  @@ -17,7 +29,7 @@ import { ffmpegProfileApiService } from '@/services/FFmpegProfileService'; @Component export default class FFmpegProfiles extends Vue { - private ffmpegProfiles: FFmpegProfile[] = []; + public ffmpegProfiles: FFmpegProfile[] = []; get headers() { return [ @@ -27,10 +39,81 @@ export default class FFmpegProfiles extends Vue { value: 'resolution' }, { text: this.$t('ffmpeg-profiles.table.video'), value: 'video' }, - { text: this.$t('ffmpeg-profiles.table.audio'), value: 'audio' } + { text: this.$t('ffmpeg-profiles.table.audio'), value: 'audio' }, + { + text: this.$t('ffmpeg-profiles.actions'), + value: 'actions', + sortable: false + } ]; } + addRecord() { + this.$router.push({ + name: 'add-ffmpeg-profile' + }); + } + + deleteRecord(record: any, recordName: any) { + this.$swal + .fire({ + title: this.$t( + 'ffmpeg-profiles.delete-dialog-title' + ).toString(), + // text: this.$t( + // 'Delete "' + recordName + '" FFmpeg Profile?' + // ).toString(), + text: this.$t('ffmpeg-profiles.delete-dialog-text', { + profileName: '"' + recordName + '"' + }).toString(), + icon: 'warning', + //iconColor: '#4CAF50', + showCancelButton: true, + cancelButtonText: this.$t('ffmpeg-profiles.no').toString(), + confirmButtonText: this.$t('ffmpeg-profiles.yes').toString() + }) + .then((result) => { + if (result.isConfirmed) { + let index = this.ffmpegProfiles.findIndex( + (it) => it.id === record + ); + this.ffmpegProfiles.splice(index, 1); + ffmpegProfileApiService.deleteRecord(String(record)); + const Toast = this.$swal.mixin({ + toast: true, + position: 'top-end', + showConfirmButton: false, + timer: 3000, + timerProgressBar: true, + didOpen: (toast) => { + toast.addEventListener( + 'mouseenter', + this.$swal.stopTimer + ); + toast.addEventListener( + 'mouseleave', + this.$swal.resumeTimer + ); + } + }); + + Toast.fire({ + icon: 'success', + title: this.$t( + 'ffmpeg-profiles.profile-deleted' + ).toString() + }); + } + }); + } + + editRow(id: any) { + this.$router.push({ + name: 'edit-ffmpeg', + query: { id: id } + }); + } + title: string = 'FFMpeg Profiles'; async mounted(): Promise { diff --git a/ErsatzTV/client-app/tsconfig.json b/ErsatzTV/client-app/tsconfig.json index e4da99ce..61d10bf5 100644 --- a/ErsatzTV/client-app/tsconfig.json +++ b/ErsatzTV/client-app/tsconfig.json @@ -1,4 +1,4 @@ -{ +{ "compilerOptions": { "target": "esnext", "module": "esnext", @@ -16,7 +16,8 @@ "baseUrl": ".", "types": [ "webpack-env", - "vuetify" + "vuetify", + "vue-sweetalert2" ], "paths": { "@/*": [