Browse Source

add vue ui at /v2 (#698)

* add vue ui

* add channels mock api

* Initial Vue framework with Vuetify UI (#688)

* fix hls direct streaming mode (#682)

* duration analysis on files with missing duration metadata (#683)

* first pass

* analyze zero-duration files

* add readme note for WIP

* add vuetify and basic sidebar layout - responsive

* add vue-router and initial home page

* setup composition-api for vue2

* install pinia ie... vuex4

* mixing for automatic page title

* add logo files

* tweak theme colors

* install store

* use store for menu toggle

* replicate old menu

* implement menu and children menus

* rename state to application state

* update vue files and add version to sidebar

* lock logo and make expandable list remove minified menu

* remove todo, will add to PR

* top bar links and attempt at snackbar with state

* fix snackbar

* add search basic component

* fix search bar placement

* remove un-used footer

* Revert "Merge branch 'jasongdove:main' into intitial-vuetify-ui"

This reverts commit 43016d502b.

Co-authored-by: Jason Dove <jason@jasondove.me>

* Add ESLint and Prettier for VueJS (#691)

* add prettier to config

* run npm run lint over project to clean up files

* replace hr tag with v-dividers

* add vue-lint github action

* fix the dodo in me

* Hu

* Fix path

* convert to multi-run step

* forgot the name

* add vue-lint github action

fix the dodo in me

Hu

Fix path

convert to multi-run step

forgot the name

* Fix new line at end of file

* WIP

* dockerfile consistency

* use npm ci and node v14

* force prettier indenting and end of line (#695)

* disable filename hashing

* don't build tests in docker

* update dependencies

* fix running both uis

Co-authored-by: James Mackay <info@notexpectedyet.com>
pull/700/head
Jason Dove 3 years ago committed by GitHub
parent
commit
6a9075dc11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      .github/workflows/vue-lint.yml
  2. 1
      .gitignore
  3. 19
      ErsatzTV/ErsatzTV.csproj
  4. 37
      ErsatzTV/Startup.cs
  5. 23
      ErsatzTV/client-app/.gitignore
  6. 25
      ErsatzTV/client-app/README.md
  7. 3
      ErsatzTV/client-app/babel.config.js
  8. 19
      ErsatzTV/client-app/jsconfig.json
  9. 20446
      ErsatzTV/client-app/package-lock.json
  10. 63
      ErsatzTV/client-app/package.json
  11. BIN
      ErsatzTV/client-app/public/favicon.ico
  12. 17
      ErsatzTV/client-app/public/index.html
  13. 37
      ErsatzTV/client-app/src/App.vue
  14. BIN
      ErsatzTV/client-app/src/assets/images/ersatztv-500.png
  15. BIN
      ErsatzTV/client-app/src/assets/images/ersatztv.png
  16. 27
      ErsatzTV/client-app/src/components/Navigation/SideBarLogo.vue
  17. 43
      ErsatzTV/client-app/src/components/Navigation/SideBarMenu.vue
  18. 37
      ErsatzTV/client-app/src/components/Navigation/SideBarMenuItem.vue
  19. 69
      ErsatzTV/client-app/src/components/Navigation/SideBarMenuItemExpandable.vue
  20. 25
      ErsatzTV/client-app/src/components/Navigation/SideBarVersion.vue
  21. 49
      ErsatzTV/client-app/src/components/Navigation/ToolBar.vue
  22. 97
      ErsatzTV/client-app/src/components/Navigation/ToolBarLinks.vue
  23. 18
      ErsatzTV/client-app/src/components/Navigation/ToolBarSearch.vue
  24. 35
      ErsatzTV/client-app/src/components/PopUps/SnackBar.vue
  25. 23
      ErsatzTV/client-app/src/main.js
  26. 15
      ErsatzTV/client-app/src/mixins/autoPageTitle.js
  27. 28
      ErsatzTV/client-app/src/plugins/vuetify.js
  28. 223
      ErsatzTV/client-app/src/router/index.js
  29. 42
      ErsatzTV/client-app/src/stores/applicationState.js
  30. 27
      ErsatzTV/client-app/src/stores/snackbarState.js
  31. 13
      ErsatzTV/client-app/src/views/HomePage.vue
  32. 13
      ErsatzTV/client-app/vue.config.js
  33. 92
      api/ersatztv-mock-api.json
  34. 11
      docker/Dockerfile
  35. 12
      docker/nvidia/Dockerfile
  36. 11
      docker/vaapi/Dockerfile

22
.github/workflows/vue-lint.yml

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
name: Lint VueJS Files on PR Request
on:
pull_request:
jobs:
vue-lint:
runs-on: ubuntu-latest
steps:
# Checkout the current repo
- name: Checkout current repository
uses: actions/checkout@v2
# Setup NodeJS version 14
- name: Setup NodeJS V14.x.x
uses: actions/setup-node@v2
with:
node-version: '14'
# CD into the current client directory and lint and build the client
- name: Lint and Build the client
run: |
cd ./ErsatzTV/client-app/
npm ci --no-optional
npm run lint
npm run build --if-present

1
.gitignore vendored

@ -43,3 +43,4 @@ scripts/generate-api-sdk/swagger.json @@ -43,3 +43,4 @@ scripts/generate-api-sdk/swagger.json
docker-compose.override.yml
ErsatzTV/wwwroot/v2/

19
ErsatzTV/ErsatzTV.csproj

@ -2,6 +2,11 @@ @@ -2,6 +2,11 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<SpaRoot>client-app\</SpaRoot>
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>VSTHRD200</NoWarn>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
@ -13,14 +18,15 @@ @@ -13,14 +18,15 @@
<ItemGroup>
<PackageReference Include="Bugsnag.AspNet.Core" Version="3.0.0" />
<PackageReference Include="FluentValidation" Version="10.3.6" />
<PackageReference Include="FluentValidation.AspNetCore" Version="10.3.6" />
<PackageReference Include="FluentValidation" Version="10.4.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="10.4.0" />
<PackageReference Include="HtmlSanitizer" Version="7.1.488" />
<PackageReference Include="LanguageExt.Core" Version="4.0.4" />
<PackageReference Include="Markdig" Version="0.27.0" />
<PackageReference Include="Markdig" Version="0.28.0" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -40,6 +46,13 @@ @@ -40,6 +46,13 @@
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
<PackageReference Include="System.Text.Encoding.Extensions" Version="4.3.0" />
<PackageReference Include="System.Runtime.Handles" Version="4.3.0" />
<PackageReference Include="VueCliMiddleware" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<Content Remove="$(SpaRoot)**" />
<None Remove="$(SpaRoot)**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup>
<ItemGroup>

37
ErsatzTV/Startup.cs

@ -53,6 +53,7 @@ using FluentValidation.AspNetCore; @@ -53,6 +53,7 @@ using FluentValidation.AspNetCore;
using Ganss.XSS;
using MediatR;
using MediatR.Courier.DependencyInjection;
using Microsoft.AspNetCore.SpaServices;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
@ -61,6 +62,7 @@ using Newtonsoft.Json; @@ -61,6 +62,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Refit;
using Serilog;
using VueCliMiddleware;
namespace ErsatzTV;
@ -129,6 +131,10 @@ public class Startup @@ -129,6 +131,10 @@ public class Startup
options.ImplicitlyValidateChildProperties = true;
});
services.AddSpaStaticFiles(options => options.RootPath = "wwwroot/v2");
services.AddMemoryCache();
services.AddRazorPages();
services.AddServerSideBlazor();
@ -253,12 +259,43 @@ public class Startup @@ -253,12 +259,43 @@ public class Startup
app.UseRouting();
if (!env.IsDevelopment())
{
app.Map(
"/v2",
app2 =>
{
if (string.IsNullOrWhiteSpace(env.WebRootPath))
{
env.WebRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot");
}
app2.UseRouting();
app2.UseEndpoints(e => e.MapFallbackToFile("index.html"));
app2.UseFileServer(
new FileServerOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(env.WebRootPath, "v2"))
});
});
}
app.UseEndpoints(
endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
if (env.IsDevelopment())
{
endpoints.MapToVueCliProxy(
"/v2/{*path}",
new SpaOptions { SourcePath = "client-app" },
"serve",
regex: "Compiled successfully",
forceKill: true);
}
});
}

23
ErsatzTV/client-app/.gitignore vendored

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

25
ErsatzTV/client-app/README.md

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
# ErsatzTV Vuetify UI - WIP
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

3
ErsatzTV/client-app/babel.config.js

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

19
ErsatzTV/client-app/jsconfig.json

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

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

File diff suppressed because it is too large Load Diff

63
ErsatzTV/client-app/package.json

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
{
"name": "client-app",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@mdi/font": "5.9.55",
"core-js": "^3.8.3",
"pinia": "^2.0.11",
"roboto-fontface": "*",
"vue": "^2.6.14",
"vue-router": "^3.2.0",
"vuetify": "^2.6.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/composition-api": "^1.4.9",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "~1.32.0",
"sass-loader": "^10.0.0",
"vue-cli-plugin-vuetify": "~2.4.7",
"vue-template-compiler": "^2.6.14",
"vuetify-loader": "^1.7.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"plugin:prettier/recommended",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {
"prettier/prettier": [
"error", {
"endOfLine": "auto",
"tabWidth": 4
}
]
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

BIN
ErsatzTV/client-app/public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

17
ErsatzTV/client-app/public/index.html

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

37
ErsatzTV/client-app/src/App.vue

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
<template>
<v-app>
<Toolbar />
<!-- Sizes your content based upon application components -->
<v-main>
<!-- Provides the application the proper gutter -->
<v-container fluid>
<!-- If using vue-router -->
<router-view></router-view>
</v-container>
<SnackBar />
</v-main>
</v-app>
</template>
<script>
import Vue from "vue";
import Toolbar from "@/components/Navigation/ToolBar.vue";
import SnackBar from "@/components/PopUps/SnackBar";
export default Vue.extend({
name: "App",
components: { Toolbar, SnackBar },
data: () => ({}),
});
</script>
<style>
main {
background-color: black;
background-attachment: fixed;
background-repeat: no-repeat;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
background-position: center;
}
</style>

BIN
ErsatzTV/client-app/src/assets/images/ersatztv-500.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
ErsatzTV/client-app/src/assets/images/ersatztv.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

27
ErsatzTV/client-app/src/components/Navigation/SideBarLogo.vue

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
<template>
<span>
<v-img
v-if="!isNavigationMini"
src="@/assets/images/ersatztv.png"
class="ma-3"
></v-img>
<v-img
v-if="isNavigationMini"
src="@/assets/images/ersatztv-500.png"
class="ma-1"
></v-img>
<v-divider inset></v-divider>
</span>
</template>
<script>
import { mapState } from "pinia";
import { applicationState } from "@/stores/applicationState";
export default {
name: "SideBarLogo",
computed: {
...mapState(applicationState, ["isNavigationMini"]),
},
};
</script>

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

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
<template>
<v-list nav dense>
<span v-for="(nav, i) in navigation" :key="i">
<SideBarMenuItem
v-if="!nav.children"
:name="nav.name"
:path="nav.path"
:icon="nav.meta.icon"
:disabled="nav.meta.disabled"
/>
<SideBarMenuItemExpandable
v-else
@click.native="disableMiniNavigation()"
:name="nav.name"
:icon="nav.meta.icon"
:disabled="nav.meta.disabled"
:children="nav.children"
/>
</span>
</v-list>
</template>
<script>
import SideBarMenuItem from "./SideBarMenuItem";
import SideBarMenuItemExpandable from "./SideBarMenuItemExpandable";
import { mapState } from "pinia";
import { applicationState } from "@/stores/applicationState";
export default {
name: "NavSidebar",
components: { SideBarMenuItem, SideBarMenuItemExpandable },
data: () => ({
navigation: null,
}),
computed: {
...mapState(applicationState, ["disableMiniNavigation"]),
},
beforeMount: function () {
//Pull in navigation from routes and load into DOM
this.navigation = this.$router.options.routes;
},
};
</script>

37
ErsatzTV/client-app/src/components/Navigation/SideBarMenuItem.vue

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
<template>
<v-list-item-group color="primary">
<v-list-item :to="path" :disabled="disabled">
<v-list-item-icon>
<v-icon v-text="icon" :disabled="disabled" />
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title v-text="name" />
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</template>
<script>
export default {
name: "SideBarMenuItem",
props: {
name: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
icon: {
type: String,
required: true,
},
disabled: {
type: Boolean,
required: true,
},
},
};
</script>

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

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
<template>
<v-list-group
color="primary"
v-model="menuItemOpened"
:disabled="disabled"
:prepend-icon="icon"
no-action
>
<template v-slot:activator>
<v-list-item-content>
<v-list-item-title v-text="name"></v-list-item-title>
</v-list-item-content>
</template>
<v-list-item
v-for="child in children"
:key="child.name"
:to="child.path"
:disabled="child.meta.disabled"
>
<v-list-item-icon>
<v-icon
v-text="child.meta.icon"
:disabled="child.meta.disabled"
/>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title v-text="child.name"></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-group>
</template>
<script>
export default {
name: "SideBarMenuItemExpandable",
props: {
name: {
type: String,
required: true,
},
icon: {
type: String,
required: true,
},
disabled: {
type: Boolean,
required: true,
},
children: {
type: Array,
required: true,
},
},
data: () => ({
opened: false,
}),
computed: {
menuItemOpened: {
get: function () {
return this.opened;
},
set: function () {
return !this.opened;
},
},
},
};
</script>

25
ErsatzTV/client-app/src/components/Navigation/SideBarVersion.vue

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<template>
<span v-if="!isNavigationMini" class="text-center">
<v-divider inset></v-divider>
<h4 class="pt-2">ErsatzTV Version</h4>
<p>{{ currentServerVersion }}</p>
</span>
</template>
<script>
import { mapState } from "pinia";
import { applicationState } from "@/stores/applicationState";
export default {
name: "SideBarVersion",
computed: {
...mapState(applicationState, [
"currentServerVersion",
"isNavigationMini",
]),
},
};
</script>
<style scoped></style>

49
ErsatzTV/client-app/src/components/Navigation/ToolBar.vue

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
<template>
<nav>
<v-app-bar flat app dense absolute>
<v-app-bar-nav-icon @click.stop="toggleMiniNavigation()" />
<ToolBarSearch />
<v-spacer />
<ToolBarLinks />
</v-app-bar>
<v-navigation-drawer app :mini-variant="isNavigationMini" permanent>
<template v-slot:prepend>
<SideBarLogo />
</template>
<SideBarMenu />
<template v-slot:append>
<SideBarVersion />
</template>
</v-navigation-drawer>
</nav>
</template>
<script>
import SideBarLogo from "./SideBarLogo.vue";
import SideBarMenu from "./SideBarMenu.vue";
import SideBarVersion from "./SideBarVersion";
import ToolBarLinks from "./ToolBarLinks";
import ToolBarSearch from "./ToolBarSearch";
import { mapState } from "pinia";
import { applicationState } from "@/stores/applicationState";
export default {
name: "NavToolbar",
components: {
SideBarMenu,
SideBarLogo,
SideBarVersion,
ToolBarLinks,
ToolBarSearch,
},
computed: {
...mapState(applicationState, [
"isNavigationMini",
"toggleMiniNavigation",
]),
},
};
</script>

97
ErsatzTV/client-app/src/components/Navigation/ToolBarLinks.vue

@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
<template>
<span>
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-btn
class="ma-2"
outlined
color="primary"
@click="copyTextToClipboard(navBarURLs.m3uURL)"
v-bind="attrs"
v-on="on"
small
>
<v-icon>mdi-playlist-play</v-icon> M3U
</v-btn>
</template>
<span>{{ clickToCopyText }}</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-btn
class="ma-2"
outlined
color="primary"
@click="copyTextToClipboard(navBarURLs.xmlURL)"
v-bind="attrs"
v-on="on"
small
>
<v-icon>mdi-xml</v-icon> XML
</v-btn>
</template>
<span>{{ clickToCopyText }}</span>
</v-tooltip>
<v-btn
icon
color="secondary"
:href="navBarURLs.documentationURL"
target="_blank"
>
<v-icon>mdi-file-document</v-icon>
</v-btn>
<v-btn
icon
color="secondary"
:href="navBarURLs.discordURL"
target="_blank"
>
<v-icon>mdi-discord</v-icon>
</v-btn>
<v-btn
icon
color="secondary"
:href="navBarURLs.githubURL"
target="_blank"
>
<v-icon>mdi-github</v-icon>
</v-btn>
</span>
</template>
<script>
import { mapState } from "pinia";
import { applicationState } from "@/stores/applicationState";
import { snackbarState } from "@/stores/snackbarState";
export default {
name: "ToolBarLinks.vue",
data: () => ({
toast: false,
clickToCopyText: "Click to copy to clipboard!",
successCopyText: "Successfully copied text to clipboard!",
failedCopyText: "Failed to copy text to clipboard! Error: ",
}),
computed: {
...mapState(applicationState, ["navBarURLs"]),
...mapState(snackbarState, ["showSnackbar"]),
},
methods: {
copyTextToClipboard(text) {
try {
navigator.clipboard.writeText(text);
this.showSnackbar(this.successCopyText);
} catch (e) {
console.error(e);
this.showSnackbar(`${this.failedCopyText}${e}`);
}
},
},
};
</script>
<style scoped></style>

18
ErsatzTV/client-app/src/components/Navigation/ToolBarSearch.vue

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
<template>
<v-text-field
outlined
class="ml-5"
label="Search..."
prepend-inner-icon="mdi-magnify"
hide-details
dense
></v-text-field>
</template>
<script>
export default {
name: "ToolBarSearch",
};
</script>
<style scoped></style>

35
ErsatzTV/client-app/src/components/PopUps/SnackBar.vue

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<template>
<v-snackbar v-model="snackbar" :timeout="timeout">
{{ currentMessage }}
<v-btn text color="primary" @click.native="closeSnackbar()"
>Close</v-btn
>
</v-snackbar>
</template>
<script>
import { mapState } from "pinia";
import { snackbarState } from "@/stores/snackbarState";
export default {
data: () => ({
timeout: 4000,
}),
computed: {
...mapState(snackbarState, [
"currentMessage",
"isVisible",
"closeSnackbar",
"openSnackbar",
]),
snackbar: {
get() {
return this.isVisible;
},
set() {
this.closeSnackbar();
},
},
},
};
</script>

23
ErsatzTV/client-app/src/main.js

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
import Vue from "vue";
import App from "./App.vue";
import vuetify from "./plugins/vuetify";
import router from "./router";
import { createPinia, PiniaVuePlugin } from "pinia";
import autoPageTitleMixin from "./mixins/autoPageTitle";
import "roboto-fontface/css/roboto/roboto-fontface.css";
import "@mdi/font/css/materialdesignicons.css";
Vue.config.productionTip = false;
Vue.use(PiniaVuePlugin);
const pinia = createPinia();
// Mixin to automate the page title when navigating... Will default to "ErsatzTV if no title value exported from page.
Vue.mixin(autoPageTitleMixin);
new Vue({
vuetify,
router,
pinia,
render: (h) => h(App),
}).$mount("#app");

15
ErsatzTV/client-app/src/mixins/autoPageTitle.js

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
function getTitle(vm) {
const { title } = vm.$options;
if (title) {
return typeof title === "function" ? title.call(vm) : title;
}
}
export default {
created() {
const title = getTitle(this);
if (title) {
document.title = `ErsatzTV | ${title}`;
}
},
};

28
ErsatzTV/client-app/src/plugins/vuetify.js

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
import Vue from "vue";
import Vuetify from "vuetify/lib/framework";
import colors from "vuetify/lib/util/colors";
Vue.use(Vuetify);
export default new Vuetify({
icons: {
iconfont: "mdi", // default - only for display purposes
},
theme: {
themes: {
dark: {
primary: colors.yellow,
secondary: colors.teal.accent3,
accent: colors.yellow.accent2,
error: colors.red,
warning: colors.orange,
info: colors.lightBlue,
success: colors.green,
},
},
options: {
customProperties: true,
},
dark: true,
},
});

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

@ -0,0 +1,223 @@ @@ -0,0 +1,223 @@
import Vue from "vue";
import VueRouter from "vue-router";
import HomePage from "../views/HomePage.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "Home",
component: HomePage,
meta: {
icon: "mdi-home",
disabled: false,
},
},
{
path: "/channels",
name: "Channels",
meta: {
icon: "mdi-broadcast",
disabled: true,
},
},
{
path: "/ffmpeg-profiles",
name: "FFmpeg Profiles",
meta: {
icon: "mdi-video-input-component",
disabled: true,
},
},
{
path: "/watermarks",
name: "Watermarks",
meta: {
icon: "mdi-watermark",
disabled: true,
},
},
{
path: "/sources",
name: "Media Sources",
meta: {
icon: "mdi-server-network",
disabled: false,
},
children: [
{
path: "/sources/local",
name: "Local",
meta: {
icon: "mdi-folder",
disabled: true,
},
},
{
path: "/sources/emby",
name: "Emby",
meta: {
icon: "mdi-emby",
disabled: true,
},
},
{
path: "/sources/jellyfin",
name: "Jellyfin",
meta: {
icon: "mdi-jellyfish",
disabled: true,
},
},
{
path: "/sources/plex",
name: "Plex",
meta: {
icon: "mdi-plex",
disabled: true,
},
},
],
},
{
path: "/media",
name: "Media",
meta: {
icon: "mdi-cog",
disabled: false,
},
children: [
{
path: "/media/libraries",
name: "Libraries",
meta: {
icon: "mdi-library",
disabled: true,
},
},
{
path: "/media/trash",
name: "Trash",
meta: {
icon: "mdi-trash-can",
disabled: true,
},
},
{
path: "/media/tv-shows",
name: "TV Shows",
meta: {
icon: "mdi-television-classic",
disabled: true,
},
},
{
path: "/media/movies",
name: "Movies",
meta: {
icon: "mdi-movie",
disabled: true,
},
},
{
path: "/media/music-videos",
name: "Music Videos",
meta: {
icon: "mdi-music-circle",
disabled: true,
},
},
{
path: "/media/other-videos",
name: "Other Videos",
meta: {
icon: "mdi-video",
disabled: true,
},
},
{
path: "/media/songs",
name: "Songs",
meta: {
icon: "mdi-album",
disabled: true,
},
},
],
},
{
path: "/lists",
name: "Lists",
meta: {
icon: "mdi-format-list-bulleted",
disabled: false,
},
children: [
{
path: "/lists/collections",
name: "Collections",
meta: {
icon: "mdi-collage",
disabled: true,
},
},
{
path: "/lists/trakt-lists",
name: "Trakt Lists",
meta: {
icon: "mdi-hammer",
disabled: true,
},
},
{
path: "/lists/filler-presets",
name: "Filler Presets",
meta: {
icon: "mdi-tune-vertical",
disabled: true,
},
},
],
},
{
path: "/schedules",
name: "Schedules",
meta: {
icon: "mdi-calendar",
disabled: true,
},
},
{
path: "/playouts",
name: "Playouts",
meta: {
icon: "mdi-clipboard-play-multiple",
disabled: true,
},
},
{
path: "/settings",
name: "Settings",
meta: {
icon: "mdi-cog",
disabled: true,
},
},
{
path: "/Logs",
name: "Logs",
meta: {
icon: "mdi-card-text",
disabled: true,
},
},
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});
export default router;

42
ErsatzTV/client-app/src/stores/applicationState.js

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
import { defineStore } from "pinia";
const pageURL = `${window.location}`;
export const applicationState = defineStore("appState", {
state: () => {
return {
miniMenu: false,
currentVersion: "0.4.3-7cd2f9a-docker-nvidia", // Needs to be pulled from API with an action when ready
m3uURL: pageURL + "iptv/channels.m3u",
xmlURL: pageURL + "iptv/xmltv.xml",
documentationURL: "https://ersatztv.org/",
githubURL: "https://github.com/jasongdove/ErsatzTV",
discordURL: "https://discord.gg/hHaJm3yGy6",
};
},
getters: {
isNavigationMini(state) {
return state.miniMenu;
},
currentServerVersion(state) {
return state.currentVersion;
},
navBarURLs(state) {
return {
m3uURL: state.m3uURL,
xmlURL: state.xmlURL,
documentationURL: state.documentationURL,
githubURL: state.githubURL,
discordURL: state.discordURL,
};
},
},
actions: {
toggleMiniNavigation() {
this.miniMenu = !this.miniMenu;
},
disableMiniNavigation() {
this.miniMenu = false;
},
},
});

27
ErsatzTV/client-app/src/stores/snackbarState.js

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
import { defineStore } from "pinia";
export const snackbarState = defineStore("snackbarState", {
state: () => {
return {
visible: false,
message: "",
};
},
getters: {
isVisible(state) {
return state.visible;
},
currentMessage(state) {
return state.message;
},
},
actions: {
showSnackbar(message) {
this.message = message;
this.visible = true;
},
closeSnackbar() {
this.visible = false;
},
},
});

13
ErsatzTV/client-app/src/views/HomePage.vue

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
<template>
<div>
<h1>{{ this.$route.name }}</h1>
</div>
</template>
<script>
export default {
title() {
return `Home`;
},
};
</script>

13
ErsatzTV/client-app/vue.config.js

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: ["vuetify"],
runtimeCompiler: true,
pwa: {
name: "ErsatzTV",
},
publicPath: "/v2/",
outputDir: "../wwwroot/v2",
filenameHashing: false
});

92
api/ersatztv-mock-api.json

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
{
"uuid": "49d843b2-cad8-4e26-a7f7-e2081683ee0e",
"lastMigration": 19,
"name": "ErsatzTV (copy)",
"endpointPrefix": "",
"latency": 0,
"port": 3000,
"hostname": "0.0.0.0",
"routes": [
{
"uuid": "56cb284d-6911-4695-a8ca-eecffbfcbd57",
"documentation": "get all channels",
"method": "get",
"endpoint": "api/channels",
"responses": [
{
"uuid": "4fc6ab6f-6a1b-4084-8bb2-94f00d62840c",
"body": "[\n {{#repeat (faker 'datatype.number' min=1 max=5)}}\n { \n \"id\": {{@index}},\n \"number\": {{faker 'datatype.number' min=1 max=25}},\n \"name\": \"{{faker 'hacker.adjective'}} {{faker 'hacker.noun'}}\",\n \"group\": \"\",\n \"categories\": [],\n \"ffmpegProfileId\": 1,\n \"logo\": \"\",\n \"language\": \"{{oneOf (array '' 'eng')}}\",\n \"streamingMode\": \"{{oneOf (array 'HLS Segmenter' 'MPEG-TS')}}\"\n }\n {{/repeat}}\n]",
"latency": 0,
"statusCode": 200,
"label": "",
"headers": [],
"filePath": "",
"sendFileAsBody": false,
"rules": [],
"rulesOperator": "OR",
"disableTemplating": false,
"fallbackTo404": false
}
],
"enabled": true,
"randomResponse": false,
"sequentialResponse": false
},
{
"uuid": "a87f888a-2038-495e-9f89-2ae32b854004",
"documentation": "get all ffmpeg profiles",
"method": "get",
"endpoint": "api/ffmpeg/profiles",
"responses": [
{
"uuid": "2f42cd38-2591-475f-a4bf-e5fb3455a8b3",
"body": "[\n {{#repeat (faker 'datatype.number' min=2 max=3)}}\n { \n \"id\": {{@index}},\n \"name\": \"{{faker 'hacker.adjective'}} {{faker 'hacker.noun'}}\",\n \"transcode\": {{faker 'datatype.boolean'}},\n \"resolution\": \"{{oneOf (array '1920x1080' '1280x720' '720x480')}}\",\n \"videoCodec\": \"{{oneOf (array 'hevc_nvenc' 'h264_nvenc')}}\",\n \"audioCodec\": \"{{oneOf (array 'aac' 'ac3')}}\"\n }\n {{/repeat}}\n]",
"latency": 0,
"statusCode": 200,
"label": "",
"headers": [],
"filePath": "",
"sendFileAsBody": false,
"rules": [],
"rulesOperator": "OR",
"disableTemplating": false,
"fallbackTo404": false
}
],
"enabled": true,
"randomResponse": false,
"sequentialResponse": false
}
],
"proxyMode": false,
"proxyHost": "",
"proxyRemovePrefix": false,
"tlsOptions": {
"enabled": false,
"type": "CERT",
"pfxPath": "",
"certPath": "",
"keyPath": "",
"caPath": "",
"passphrase": ""
},
"cors": true,
"headers": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"proxyReqHeaders": [
{
"key": "",
"value": ""
}
],
"proxyResHeaders": [
{
"key": "",
"value": ""
}
]
}

11
docker/Dockerfile

@ -7,6 +7,8 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu @@ -7,6 +7,8 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
RUN apt-get update && apt-get install -y ca-certificates
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get install -y nodejs
WORKDIR /source
# copy csproj and restore as distinct layers
@ -15,25 +17,22 @@ COPY nuget.config . @@ -15,25 +17,22 @@ COPY nuget.config .
COPY lib/nuget/* ./lib/nuget/
COPY artwork/* ./artwork/
COPY ErsatzTV/*.csproj ./ErsatzTV/
COPY ErsatzTV/client-app/package*.json ./ErsatzTV/client-app/
COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/
COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/
COPY ErsatzTV.Core.Tests/*.csproj ./ErsatzTV.Core.Tests/
COPY ErsatzTV.FFmpeg/*.csproj ./ErsatzTV.FFmpeg/
COPY ErsatzTV.FFmpeg.Tests/*.csproj ./ErsatzTV.FFmpeg.Tests/
COPY ErsatzTV.Infrastructure/*.csproj ./ErsatzTV.Infrastructure/
RUN dotnet restore -r linux-x64
RUN dotnet restore -r linux-x64 ErsatzTV/
# copy everything else and build app
COPY ErsatzTV/. ./ErsatzTV/
COPY ErsatzTV.Application/. ./ErsatzTV.Application/
COPY ErsatzTV.Core/. ./ErsatzTV.Core/
COPY ErsatzTV.Core.Tests/. ./ErsatzTV.Core.Tests/
COPY ErsatzTV.FFmpeg/. ./ErsatzTV.FFmpeg/
COPY ErsatzTV.FFmpeg.Tests/. ./ErsatzTV.FFmpeg.Tests/
COPY ErsatzTV.Infrastructure/. ./ErsatzTV.Infrastructure/
WORKDIR /source/ErsatzTV
ARG INFO_VERSION="unknown"
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
RUN dotnet publish ErsatzTV/ErsatzTV.csproj -c release -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
# final stage/image
FROM runtime-base

12
docker/nvidia/Dockerfile

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
RUN apt-get update && apt-get install -y ca-certificates
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get install -y nodejs
WORKDIR /source
# copy csproj and restore as distinct layers
@ -9,25 +11,21 @@ COPY nuget.config . @@ -9,25 +11,21 @@ COPY nuget.config .
COPY lib/nuget/* ./lib/nuget/
COPY artwork/* ./artwork/
COPY ErsatzTV/*.csproj ./ErsatzTV/
COPY ErsatzTV/client-app/package*.json ./ErsatzTV/client-app/
COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/
COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/
COPY ErsatzTV.Core.Tests/*.csproj ./ErsatzTV.Core.Tests/
COPY ErsatzTV.FFmpeg/*.csproj ./ErsatzTV.FFmpeg/
COPY ErsatzTV.FFmpeg.Tests/*.csproj ./ErsatzTV.FFmpeg.Tests/
COPY ErsatzTV.Infrastructure/*.csproj ./ErsatzTV.Infrastructure/
RUN dotnet restore -r linux-x64
RUN dotnet restore -r linux-x64 ErsatzTV/
# copy everything else and build app
COPY ErsatzTV/. ./ErsatzTV/
COPY ErsatzTV.Application/. ./ErsatzTV.Application/
COPY ErsatzTV.Core/. ./ErsatzTV.Core/
COPY ErsatzTV.Core.Tests/. ./ErsatzTV.Core.Tests/
COPY ErsatzTV.FFmpeg/. ./ErsatzTV.FFmpeg/
COPY ErsatzTV.FFmpeg.Tests/. ./ErsatzTV.FFmpeg.Tests/
COPY ErsatzTV.Infrastructure/. ./ErsatzTV.Infrastructure/
WORKDIR /source/ErsatzTV
ARG INFO_VERSION="unknown"
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
RUN dotnet publish ErsatzTV/ErsatzTV.csproj -c release -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
# final stage/image
FROM jasongdove/ffmpeg:5.0-nvidia2004 AS runtime-base

11
docker/vaapi/Dockerfile

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
RUN apt-get update && apt-get install -y ca-certificates
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get install -y nodejs
WORKDIR /source
# copy csproj and restore as distinct layers
@ -9,25 +11,22 @@ COPY nuget.config . @@ -9,25 +11,22 @@ COPY nuget.config .
COPY lib/nuget/* ./lib/nuget/
COPY artwork/* ./artwork/
COPY ErsatzTV/*.csproj ./ErsatzTV/
COPY ErsatzTV/client-app/package*.json ./ErsatzTV/client-app/
COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/
COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/
COPY ErsatzTV.Core.Tests/*.csproj ./ErsatzTV.Core.Tests/
COPY ErsatzTV.FFmpeg/*.csproj ./ErsatzTV.FFmpeg/
COPY ErsatzTV.FFmpeg.Tests/*.csproj ./ErsatzTV.FFmpeg.Tests/
COPY ErsatzTV.Infrastructure/*.csproj ./ErsatzTV.Infrastructure/
RUN dotnet restore -r linux-x64
RUN dotnet restore -r linux-x64 ErsatzTV/
# copy everything else and build app
COPY ErsatzTV/. ./ErsatzTV/
COPY ErsatzTV.Application/. ./ErsatzTV.Application/
COPY ErsatzTV.Core/. ./ErsatzTV.Core/
COPY ErsatzTV.Core.Tests/. ./ErsatzTV.Core.Tests/
COPY ErsatzTV.FFmpeg/. ./ErsatzTV.FFmpeg/
COPY ErsatzTV.FFmpeg.Tests/. ./ErsatzTV.FFmpeg.Tests/
COPY ErsatzTV.Infrastructure/. ./ErsatzTV.Infrastructure/
WORKDIR /source/ErsatzTV
ARG INFO_VERSION="unknown"
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
RUN dotnet publish ErsatzTV/ErsatzTV.csproj -c release -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
# final stage/image
FROM jasongdove/ffmpeg:5.0-vaapi2004 AS runtime-base

Loading…
Cancel
Save