mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* 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
36 changed files with 21600 additions and 22 deletions
@ -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 |
||||||
@ -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? |
||||||
@ -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/). |
||||||
|
|
||||||
@ -0,0 +1,3 @@ |
|||||||
|
module.exports = { |
||||||
|
presets: ["@vue/cli-plugin-babel/preset"], |
||||||
|
}; |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"target": "es5", |
||||||
|
"module": "esnext", |
||||||
|
"baseUrl": "./", |
||||||
|
"moduleResolution": "node", |
||||||
|
"paths": { |
||||||
|
"@/*": [ |
||||||
|
"src/*" |
||||||
|
] |
||||||
|
}, |
||||||
|
"lib": [ |
||||||
|
"esnext", |
||||||
|
"dom", |
||||||
|
"dom.iterable", |
||||||
|
"scripthost" |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -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" |
||||||
|
] |
||||||
|
} |
||||||
|
After Width: | Height: | Size: 15 KiB |
@ -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> |
||||||
@ -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> |
||||||
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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"); |
||||||
@ -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}`; |
||||||
|
} |
||||||
|
}, |
||||||
|
}; |
||||||
@ -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, |
||||||
|
}, |
||||||
|
}); |
||||||
@ -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; |
||||||
@ -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; |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
@ -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; |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
<template> |
||||||
|
<div> |
||||||
|
<h1>{{ this.$route.name }}</h1> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
title() { |
||||||
|
return `Home`; |
||||||
|
}, |
||||||
|
}; |
||||||
|
</script> |
||||||
@ -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 |
||||||
|
}); |
||||||
@ -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": "" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
Loading…
Reference in new issue