Browse Source

UI rewrite - ffmpeg profiles (#823)

* ffmpeg profile functionality, sweetalert2

* add new files

* cleanup controller; remove unused classes

* apply formatting

* cleanup core project

* don't use bom

* whitespace

* remove generated css

* remove generated js/map

* Remove attempted linter fix, channels button, watermarks page. Fixed handlerror.

* Changed deleted confirmation to toast.

* Localized strings for language change. Modified Action icons to buttons and left default sizes. Changed Cancel to No where Yes is an option

* lint

Co-authored-by: Jason Dove <jason@jasondove.me>
pull/834/head
Jake 3 years ago committed by GitHub
parent
commit
3cb37003cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .editorconfig
  2. 2
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
  3. 21
      ErsatzTV.Application/FFmpegProfiles/Mapper.cs
  4. 5
      ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdForApi.cs
  5. 28
      ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdForApiHandler.cs
  6. 21
      ErsatzTV.Core/Api/FFmpegProfiles/FFmpegFullProfileResponseModel.cs
  7. 21
      ErsatzTV/Controllers/Api/FFmpegProfileController.cs
  8. 2
      ErsatzTV/ErsatzTV.csproj
  9. 5
      ErsatzTV/client-app/.gitignore
  10. 46
      ErsatzTV/client-app/package-lock.json
  11. 3
      ErsatzTV/client-app/package.json
  12. 12
      ErsatzTV/client-app/src/assets/css/global.scss
  13. 4
      ErsatzTV/client-app/src/components/Navigation/SideBarMenu.vue
  14. 4
      ErsatzTV/client-app/src/components/Navigation/SideBarMenuItemExpandable.vue
  15. 32
      ErsatzTV/client-app/src/locales/en.json
  16. 38
      ErsatzTV/client-app/src/locales/pt-br.json
  17. 14
      ErsatzTV/client-app/src/main.ts
  18. 20
      ErsatzTV/client-app/src/models/FFmpegFullProfile.ts
  19. 27
      ErsatzTV/client-app/src/router/index.js
  20. 96
      ErsatzTV/client-app/src/services/FFmpegProfileService.ts
  21. 909
      ErsatzTV/client-app/src/views/AddEditFFmpegProfilePage.vue
  22. 22
      ErsatzTV/client-app/src/views/ChannelsPage.vue
  23. 87
      ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue
  24. 5
      ErsatzTV/client-app/tsconfig.json

2
.editorconfig

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
[*]
charset=utf-8-bom
charset=utf-8
end_of_line=lf
trim_trailing_whitespace=false
insert_final_newline=false

2
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs

@ -20,7 +20,7 @@ public class CreateFFmpegProfileHandler : @@ -20,7 +20,7 @@ public class CreateFFmpegProfileHandler :
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, FFmpegProfile> 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<CreateFFmpegProfileResult> PersistFFmpegProfile(

21
ErsatzTV.Application/FFmpegProfiles/Mapper.cs

@ -35,6 +35,27 @@ internal static class Mapper @@ -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);
}

5
ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdForApi.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core.Api.FFmpegProfiles;
namespace ErsatzTV.Application.FFmpegProfiles;
public record GetFFmpegFullProfileByIdForApi(int Id) : IRequest<Option<FFmpegFullProfileResponseModel>>;

28
ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdForApiHandler.cs

@ -0,0 +1,28 @@ @@ -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<GetFFmpegFullProfileByIdForApi,
Option<FFmpegFullProfileResponseModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetFFmpegProfileByIdForApiHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Option<FFmpegFullProfileResponseModel>> 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);
}
}

21
ErsatzTV.Core/Api/FFmpegProfiles/FFmpegFullProfileResponseModel.cs

@ -0,0 +1,21 @@ @@ -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);

21
ErsatzTV/Controllers/Api/FFmpegProfileController.cs

@ -1,4 +1,6 @@ @@ -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 @@ -15,4 +17,21 @@ public class FFmpegProfileController
[HttpGet("/api/ffmpeg/profiles")]
public async Task<List<FFmpegProfileResponseModel>> GetAll() =>
await _mediator.Send(new GetAllFFmpegProfilesForApi());
[HttpPost("/api/ffmpeg/profiles/new")]
public async Task<Either<BaseError, CreateFFmpegProfileResult>> AddOne(
[Required] [FromBody]
CreateFFmpegProfile request) => await _mediator.Send(request);
[HttpPut("/api/ffmpeg/profiles/update")]
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> UpdateOne(
[Required] [FromBody]
UpdateFFmpegProfile request) => await _mediator.Send(request);
[HttpGet("/api/ffmpeg/profiles/{id}")]
public async Task<Option<FFmpegFullProfileResponseModel>> 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));
}

2
ErsatzTV/ErsatzTV.csproj

@ -116,4 +116,6 @@ @@ -116,4 +116,6 @@
</Content>
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties client-app_4package_1json__JsonSchema="https://json.schemastore.org/phraseapp.json" /></VisualStudio></ProjectExtensions>
</Project>

5
ErsatzTV/client-app/.gitignore vendored

@ -21,3 +21,8 @@ pnpm-debug.log* @@ -21,3 +21,8 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
# generated files
src/assets/css/*.css
src/models/*.js
src/models/*.js.map

46
ErsatzTV/client-app/package-lock.json generated

@ -9,16 +9,19 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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",

3
ErsatzTV/client-app/package.json

@ -10,16 +10,19 @@ @@ -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": {

12
ErsatzTV/client-app/src/assets/css/global.scss

@ -0,0 +1,12 @@ @@ -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;
}

4
ErsatzTV/client-app/src/components/Navigation/SideBarMenu.vue

@ -2,14 +2,14 @@ @@ -2,14 +2,14 @@
<v-list nav dense>
<span v-for="(nav, i) in navigation" :key="i">
<SideBarMenuItem
v-if="!nav.children"
v-if="(!nav.children || !nav.showchildren) && !nav.meta.hidden"
:name="nav.name"
:path="nav.path"
:icon="nav.meta.icon"
:disabled="nav.meta.disabled"
/>
<SideBarMenuItemExpandable
v-else
v-else-if="nav.children"
@click.native="disableMiniNavigation()"
:name="nav.name"
:icon="nav.meta.icon"

4
ErsatzTV/client-app/src/components/Navigation/SideBarMenuItemExpandable.vue

@ -50,6 +50,10 @@ export default { @@ -50,6 +50,10 @@ export default {
children: {
type: Array,
required: true
},
nonmenuchildren: {
type: Array,
required: false
}
},
data: () => ({

32
ErsatzTV/client-app/src/locales/en.json

@ -87,6 +87,13 @@ @@ -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 @@ @@ -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"
},

38
ErsatzTV/client-app/src/locales/pt-br.json

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
"$vuetify": {
"badge": "Distintivo",
"close": "Fechar",
"Actions": "Ações",
"dataIterator": {
"noResultsText": "Nenhum dado encontrado",
"loadingText": "Carregando itens..."
@ -87,10 +88,45 @@ @@ -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"
},

14
ErsatzTV/client-app/src/main.ts

@ -1,4 +1,4 @@ @@ -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'; @@ -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,

20
ErsatzTV/client-app/src/models/FFmpegFullProfile.ts

@ -0,0 +1,20 @@ @@ -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;
}

27
ErsatzTV/client-app/src/router/index.js

@ -3,6 +3,7 @@ import VueRouter from 'vue-router'; @@ -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 = [ @@ -32,7 +33,8 @@ const routes = [
meta: {
icon: 'mdi-video-input-component',
disabled: false
}
},
showchildren: false
},
{
path: '/watermarks',
@ -49,6 +51,7 @@ const routes = [ @@ -49,6 +51,7 @@ const routes = [
icon: 'mdi-server-network',
disabled: false
},
showchildren: true,
children: [
{
path: '/sources/local',
@ -91,6 +94,7 @@ const routes = [ @@ -91,6 +94,7 @@ const routes = [
icon: 'mdi-cog',
disabled: false
},
showchildren: true,
children: [
{
path: '/media/libraries',
@ -157,6 +161,7 @@ const routes = [ @@ -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 = [ @@ -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 })
}
];

96
ErsatzTV/client-app/src/services/FFmpegProfileService.ts

@ -1,5 +1,6 @@ @@ -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 { @@ -12,6 +13,101 @@ class FFmpegProfileApiService extends AbstractApiService {
.then(this.handleResponse.bind(this))
.catch(this.handleError.bind(this));
}
public getOne(id: string): Promise<FFmpegFullProfile[]> {
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 =

909
ErsatzTV/client-app/src/views/AddEditFFmpegProfilePage.vue

@ -0,0 +1,909 @@ @@ -0,0 +1,909 @@
<template>
<div id="AddEditFFmpegProfile">
<h1 id="title" class="mx-4" />
<v-divider color="success" class="ma-2"></v-divider>
<v-app>
<v-form
ref="form"
v-model="isFormValid"
id="ffmpegForm"
lazy-validation
>
<v-container>
<v-row justify="center">
<v-flex shrink class="pb-10">
<v-container style="max-height: 80px">
<v-row justify="center">
<h2 class="mx">
{{ $t('edit-ffmpeg-profile.General') }}
</h2>
</v-row>
</v-container>
<v-container style="max-height: 80px">
<v-text-field
v-model="newProfile.name"
:rules="textRules"
:label="$t('edit-ffmpeg-profile.Name')"
required
></v-text-field>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-text-field
ref="myThreadCount"
v-model="threadCount"
:label="
$t('edit-ffmpeg-profile.thread-count')
"
:rules="validInt"
number
required
></v-text-field>
<v-tooltip v-model="threadCountShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container style="max-height: 40px">
<v-select
v-model="selectedResolution"
@change="preferredResolutionChange"
:items="preferredResolutions"
item-value="id"
item-text="name"
:label="
$t(
'edit-ffmpeg-profile.preferred-resolution'
)
"
required
></v-select>
</v-container>
</v-flex>
<v-divider
style="max-height: 500px"
vertical
color="grey"
></v-divider>
<v-flex shrink class="pb-10">
<v-container style="max-height: 80px">
<v-row justify="center">
<h2 class="mx">
{{ $t('edit-ffmpeg-profile.Video') }}
</h2>
</v-row>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-select
v-model="selectedVideoFormat"
@change="videoFormatChanged"
:items="videoFormats"
item-value="id"
item-text="name"
id="videoFormatSelector"
:label="$t('edit-ffmpeg-profile.format')"
required
></v-select>
<v-tooltip v-model="videoFormatShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-text-field
v-model="newProfile.videoBitRate"
:label="$t('edit-ffmpeg-profile.bitrate')"
:rules="validIntNonZero"
required
></v-text-field>
<v-tooltip v-model="videoBitRateShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-text-field
v-model="newProfile.videoBufferSize"
:label="
$t('edit-ffmpeg-profile.buffer-size')
"
:rules="validIntNonZero"
required
></v-text-field>
<v-tooltip v-model="videoBufferSizeShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-select
v-model="selectedHardwareAcceleration"
@change="hardwareAccelerationChanged"
:items="hardwareAccelerations"
item-value="id"
item-text="name"
:label="
$t(
'edit-ffmpeg-profile.hardware-acceleration'
)
"
required
></v-select>
<v-tooltip
v-model="hardwareAccelerationShow"
top
>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-select
v-model="selectedVaapiDriver"
@change="vaapiDriverChanged"
:items="vaapiDrivers"
:disabled="vaapiDriverDisabled"
item-value="id"
item-text="name"
:label="
$t('edit-ffmpeg-profile.vaapi-driver')
"
required
></v-select>
<v-tooltip v-model="vaapiDriverShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-select
v-model="selectedVaapiDevice"
@change="vaapiDeviceChanged"
:items="vaapiDevices"
:disabled="vaapiDriverDisabled"
:label="
$t('edit-ffmpeg-profile.vaapi-device')
"
required
></v-select>
<v-tooltip v-model="vaapiDeviceShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 50px"
>
<v-checkbox
class="mr-2"
v-model="normalizeFrameRate"
:label="
$t(
'edit-ffmpeg-profile.normalize-framerate'
)
"
color="green lighten-1"
required
></v-checkbox>
<v-tooltip v-model="normalizeFrameRateShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pt-8"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 50px"
>
<v-checkbox
class="mr-2"
v-model="autoDeinterlaceVideo"
:label="
$t(
'edit-ffmpeg-profile.auto-deinterlace-video'
)
"
color="green lighten-1"
required
></v-checkbox>
<v-tooltip
v-model="autoDeinterlaceVideoShow"
top
>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pt-8"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
</v-flex>
<v-divider
style="max-height: 500px"
vertical
color="grey"
></v-divider>
<v-flex shrink class="pb-10">
<v-container style="max-height: 80px">
<v-row justify="center">
<h2 class="mx">
{{ $t('edit-ffmpeg-profile.Audio') }}
</h2>
</v-row>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-select
v-model="selectedAudioFormat"
@change="audioFormatChanged"
:items="audioFormats"
item-value="id"
item-text="name"
:label="$t('edit-ffmpeg-profile.format')"
ref="audioFormat"
required
></v-select>
<v-tooltip v-model="audioFormatShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-text-field
v-model="newProfile.audioBitRate"
:label="$t('edit-ffmpeg-profile.bitrate')"
:rules="validIntNonZero"
required
></v-text-field>
<v-tooltip v-model="audioBitRateShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-text-field
v-model="newProfile.audioBufferSize"
:label="
$t('edit-ffmpeg-profile.buffer-size')
"
:rules="validIntNonZero"
required
></v-text-field>
<v-tooltip v-model="audioBufferSizeShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-text-field
v-model="newProfile.channels"
:label="$t('edit-ffmpeg-profile.channels')"
:rules="validIntNonZero"
required
></v-text-field>
<v-tooltip v-model="audioChannelsShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-text-field
v-model="newProfile.audioSampleRate"
:label="
$t('edit-ffmpeg-profile.sample-rate')
"
:rules="validIntNonZero"
required
></v-text-field>
<v-tooltip v-model="audioSampleRateShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-container
class="d-flex"
style="max-height: 80px"
>
<v-checkbox
v-model="normalizeLoudness"
:label="
$t(
'edit-ffmpeg-profile.normalize-loudness'
)
"
color="green lighten-1"
required
></v-checkbox>
<v-tooltip v-model="normalizeLoudnessShow" top>
<template v-slot:activator="{ on, attrs }">
<v-icon
class="pl-2"
color="grey"
v-ripple="false"
:retain-focus="false"
v-bind="attrs"
v-on="on"
>
mdi-help-circle-outline
</v-icon>
</template>
<span>Recommended Thread Count: 0</span>
</v-tooltip>
</v-container>
<v-spacer style="height: 80px"></v-spacer>
<v-container>
<v-btn
color="green lighten-1"
class="ma-2"
:disabled="!isFormValid"
@click="saveFFmpegProfile()"
>
{{ $t('edit-ffmpeg-profile.save-profile') }}
</v-btn>
<v-btn
color="cancel"
class="ma-2"
@click="cancelAdd()"
>
{{ $t('edit-ffmpeg-profile.cancel') }}
</v-btn>
<v-btn
color="indigo accent-1"
class="ma-2"
@click="cancelAdd()"
>
{{ $t('edit-ffmpeg-profile.help') }}
</v-btn>
</v-container>
</v-flex>
</v-row>
</v-container>
</v-form>
</v-app>
</div>
</template>
<script lang="ts">
import { Vue, Component, Watch } from 'vue-property-decorator';
import { ffmpegProfileApiService } from '@/services/FFmpegProfileService';
@Component
export default class AddEditFFmpegProfile extends Vue {
//@Name({ default: 'AddEditFFmpegProfile' }) AddEditFFmpegProfile!: string;
//@Prop({ default: -1 }) private id!: number;
public newProfile: any = {};
private refForm: any = this.$refs.form;
public isFormValid = false;
public threadCountShow = false;
public videoFormatShow = false;
public videoBitRateShow = false;
public videoBufferSizeShow = false;
public hardwareAccelerationShow = false;
public vaapiDriverShow = false;
public vaapiDeviceShow = false;
public normalizeFrameRateShow = false;
public autoDeinterlaceVideoShow = false;
public audioFormatShow = false;
public audioBitRateShow = false;
public audioBufferSizeShow = false;
public audioChannelsShow = false;
public audioSampleRateShow = false;
public normalizeLoudnessShow = false;
public AddEditFFmpegProfile() {
console.log('test');
}
public audioFormats: [
{ id: number; name: string },
{ id: number; name: string }
] = [
{ id: 1, name: 'aac' },
{ id: 2, name: 'ac3' }
];
private _selectedAudioFormat: number = 2;
public selectedAudioFormat: { id: number; name: string } = {
id: 2,
name: 'ac3'
};
public preferredResolutions: [
{ id: number; name: string },
{ id: number; name: string },
{ id: number; name: string },
{ id: number; name: string }
] = [
{ id: 0, name: '720x480' },
{ id: 1, name: '1280x720' },
{ id: 2, name: '1920x1080' },
{ id: 3, name: '3840x2160' }
];
private _selectedResolution: number = 2;
public selectedResolution: { id: number; name: string } = {
id: 2,
name: '1920x1080'
};
public videoFormats: [
{ id: number; name: string },
{ id: number; name: string },
{ id: number; name: string }
] = [
{ id: 1, name: 'h264' },
{ id: 2, name: 'hevc' },
{ id: 3, name: 'mpeg-2' }
];
private _selectedVideoFormat: number = 1;
public selectedVideoFormat: { id: number; name: string } = {
id: 1,
name: 'h264'
};
public hardwareAccelerations: [
{ id: number; name: string },
{ id: number; name: string },
{ id: number; name: string },
{ id: number; name: string },
{ id: number; name: string }
] = [
{ id: 0, name: 'None' },
{ id: 1, name: 'Qsv' },
{ id: 2, name: 'Nvenc' },
{ id: 3, name: 'Vaapi' },
{ id: 4, name: 'VideoToolbox' }
];
private _selectedHardwareAcceleration: number = 0;
public selectedHardwareAcceleration: { id: number; name: string } = {
id: 0,
name: 'None'
};
public vaapiDrivers: [
{ id: number; name: string },
{ id: number; name: string },
{ id: number; name: string },
{ id: number; name: string },
{ id: number; name: string }
] = [
{ id: 0, name: 'Default' },
{ id: 1, name: 'iHD' },
{ id: 2, name: 'i965' },
{ id: 3, name: 'RadeonSI' },
{ id: 4, name: 'Nouveau' }
];
public vaapiDriverDisabled = true;
private _selectedVaapiDriver: number = 0;
public selectedVaapiDriver: { id: number; name: string } = {
id: 0,
name: 'Default'
};
public selectedVaapiDevice: string = '';
public vaapiDevices: [string, string] = ['', '/dev/dri/renderD128'];
public normalizeFrameRate: boolean = false;
public autoDeinterlaceVideo: boolean = true;
public normalizeLoudness: boolean = true;
saveFFmpegProfile() {
//this means we're adding
if (isNaN(this.id)) {
ffmpegProfileApiService.newFFmpegProfile(
this.newProfile.name,
this.threadCount,
this._selectedHardwareAcceleration,
this._selectedVaapiDriver,
this.selectedVaapiDevice,
this._selectedResolution,
this._selectedVideoFormat,
this.newProfile.videoBitRate,
this.newProfile.videoBufferSize,
this._selectedAudioFormat,
this.newProfile.audioBitRate,
this.newProfile.audioBufferSize,
this.normalizeLoudness,
this.newProfile.channels,
this.newProfile.audioSampleRate,
this.normalizeFrameRate,
this.autoDeinterlaceVideo
);
} else {
//this means we're editing
ffmpegProfileApiService.updateFFmpegProfile(
this.id,
this.newProfile.name,
this.threadCount,
this._selectedHardwareAcceleration,
this._selectedVaapiDriver,
this.selectedVaapiDevice,
this._selectedResolution,
this._selectedVideoFormat,
this.newProfile.videoBitRate,
this.newProfile.videoBufferSize,
this._selectedAudioFormat,
this.newProfile.audioBitRate,
this.newProfile.audioBufferSize,
this.normalizeLoudness,
this.newProfile.channels,
this.newProfile.audioSampleRate,
this.normalizeFrameRate,
this.autoDeinterlaceVideo
);
}
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('edit-ffmpeg-profile.profile-saved').toString()
});
this.$router.push({
name: 'ffmpeg-profiles.title'
});
}
cancelAdd() {
this.$router.push({
name: 'ffmpeg-profiles.title'
});
}
//~change events~//
public audioFormatChanged(selectObj: number) {
this._selectedAudioFormat = selectObj;
}
public preferredResolutionChange(selectObj: number) {
this._selectedResolution = selectObj + 1;
}
public videoFormatChanged(selectObj: number) {
this._selectedVideoFormat = selectObj;
}
public hardwareAccelerationChanged(selectObj: number) {
this._selectedHardwareAcceleration = selectObj;
this.applyVaapiValidation();
}
public applyVaapiValidation() {
//If they pick VAAPI as the hardware acceleration,
//they can now choose a vaapi driver and device.
//If not, they cannot change the default options.
if (this._selectedHardwareAcceleration == 3) {
this.vaapiDriverDisabled = false;
} else {
this.vaapiDriverDisabled = true;
this._selectedVaapiDriver = 0;
this.selectedVaapiDriver = { id: 0, name: 'Default' };
this.selectedVaapiDevice = '';
}
}
public vaapiDriverChanged(selectObj: number) {
this._selectedVaapiDriver = selectObj;
}
public vaapiDeviceChanged(selectObj: string) {
this.selectedVaapiDevice = selectObj;
}
//~ end change events~//
public threadCount = 0;
get validIntNonZero() {
return [
(v: any) =>
(v && /^[0-9]+$/.test(v)) || this.$t('Must be a valid number.'),
(v: any) => (v && v > 0) || 'Must be greater than 0.'
];
}
get validInt() {
return [
(v: any) =>
(v && /^[0-9]+$/.test(v)) || this.$t('Must be a valid number.')
];
}
get textRules() {
return [(v: any) => (v && v.length > 0) || 'Value must not be empty.'];
}
props!: { id: number };
@Watch('id', { immediate: true }) async onItemChanged() {
console.log('ID', this.id);
this.id = Number(this.$route.query.id) ?? -1;
await this.loadPage();
}
private loaded = false;
private id = -1;
title: string = 'Modify FFmpeg Profile';
async loadPage(): Promise<void> {
if (this.loaded) {
return;
}
var title = document.getElementById('title');
if (title === null || title === undefined) {
//sometimes the element isn't loaded yet, it'll come
//back to this when it's good to go. So skip for now.
return;
}
if (!isNaN(this.id)) {
title.innerHTML = this.$t(
'edit-ffmpeg-profile.edit-profile'
).toString();
var ffmpegFullProfile = await ffmpegProfileApiService.getOne(
this.id.toString()
);
var result = ffmpegFullProfile[0];
if (result !== undefined) {
//We have a profile, let's load it.
this.threadCount = result.threadCount;
this.selectedVaapiDevice = result.vaapiDevice;
this.autoDeinterlaceVideo = result.deinterlaceVideo;
this.normalizeFrameRate = result.normalizeFramerate;
this.normalizeLoudness = result.normalizeLoudness;
this.newProfile = {
name: result.name,
videoBitRate: result.videoBitrate,
videoBufferSize: result.videoBufferSize,
audioBitRate: result.audioBitrate,
audioBufferSize: result.audioBufferSize,
channels: result.audioChannels,
audioSampleRate: result.audioSampleRate
};
this._selectedAudioFormat = result.audioFormat;
this.selectedAudioFormat =
this.audioFormats[result.audioFormat - 1];
this._selectedVideoFormat = result.videoFormat;
this.selectedVideoFormat =
this.videoFormats[result.videoFormat - 1];
this._selectedResolution = result.resolutionId;
this.selectedResolution =
this.preferredResolutions[result.resolutionId - 1];
this._selectedHardwareAcceleration =
result.hardwareAcceleration;
this.selectedHardwareAcceleration =
this.hardwareAccelerations[result.hardwareAcceleration];
this._selectedVaapiDriver = result.vaapiDriver;
this.selectedVaapiDriver =
this.vaapiDrivers[result.vaapiDriver];
this.applyVaapiValidation();
this.loaded = true;
} else {
//an ID was entered (probably in the URL) that doesn't exist. Let's returnt to the profile list.
console.log('No ffmpeg profile found for ID: ' + this.id);
this.$router.push({
name: 'ffmpeg-profiles.title'
});
}
} else {
//new profile!
title.innerHTML = this.$t(
'edit-ffmpeg-profile.add-profile'
).toString();
this._selectedAudioFormat = 2;
this._selectedResolution = 3;
this._selectedVideoFormat = 1;
this._selectedHardwareAcceleration = 0;
this._selectedVaapiDriver = 0;
this.selectedVaapiDevice = '';
this.newProfile = {
name: 'New Profile',
videoBitRate: 2000,
videoBufferSize: 4000,
audioBitRate: 192,
audioBufferSize: 384,
channels: 2,
audioSampleRate: 48
};
this.loaded = true;
}
}
}
</script>

22
ErsatzTV/client-app/src/views/ChannelsPage.vue

@ -5,7 +5,14 @@ @@ -5,7 +5,14 @@
:items="channels"
:sort-by="['number']"
class="elevation-1"
></v-data-table>
>
<template v-slot:[`item.actions`]="{ item }">
<v-icon small class="mr-2" @click="editRow(item.id)">
mdi-lead-pencil
</v-icon>
<v-icon small @click="deleteRow(item.id)">mdi-delete</v-icon>
</template>
</v-data-table>
</div>
</template>
@ -28,10 +35,21 @@ export default class Channels extends Vue { @@ -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<void> {

87
ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue

@ -1,11 +1,23 @@ @@ -1,11 +1,23 @@
<template>
<div>
<v-btn color="success" class="ma-4" @click="addRecord()">{{
$t('ffmpeg-profiles.add-profile')
}}</v-btn>
<v-data-table
:headers="headers"
:items="ffmpegProfiles"
:sort-by="['name']"
class="elevation-1"
>
<template v-slot:[`item.actions`]="{ item }">
<v-btn icon class="mr-2" @click="editRow(item.id)">
<v-icon>mdi-lead-pencil</v-icon>
</v-btn>
<v-btn icon @click.stop="deleteRecord(item.id, item.name)">
<v-icon>mdi-delete</v-icon></v-btn
>
</template>
</v-data-table>
</div>
</template>
@ -17,7 +29,7 @@ import { ffmpegProfileApiService } from '@/services/FFmpegProfileService'; @@ -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 { @@ -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<void> {

5
ErsatzTV/client-app/tsconfig.json

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
@ -16,7 +16,8 @@ @@ -16,7 +16,8 @@
"baseUrl": ".",
"types": [
"webpack-env",
"vuetify"
"vuetify",
"vue-sweetalert2"
],
"paths": {
"@/*": [

Loading…
Cancel
Save