-
-
-
-
-
-
-
-
-
- {{ folder }}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- Create
-
-
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
- Overall ({{ currentNumber }}/{{ inputFiles.length }}):
-
-
-
-
-
-
- Current:
-
-
-
-
-
-
- Uploading:
-
-
- {{ uploadTask }}
-
-
-
-
-
+
+
-
-
diff --git a/pages/message.vue b/pages/message.vue
index c6742021..b484fd45 100644
--- a/pages/message.vue
+++ b/pages/message.vue
@@ -1,433 +1,492 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
- Send
-
-
-
-
- Sending success...
-
-
- Sending failed...
-
-
-
-
-
-
-
-
-
- Delete: "{{ selected }}"?
-
+
+
+
+
+
+
+
+
+
Are you sure that you want to delete preset: "{{ selected }}"?
+
+
+
+
-
-
diff --git a/plugins/README.md b/plugins/README.md
deleted file mode 100644
index ca1f9d8a..00000000
--- a/plugins/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# PLUGINS
-
-**This directory is not required, you can delete it if you don't want to use it.**
-
-This directory contains Javascript plugins that you want to run before mounting the root Vue.js application.
-
-More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).
diff --git a/plugins/axios.js b/plugins/axios.js
deleted file mode 100644
index 4e036c3b..00000000
--- a/plugins/axios.js
+++ /dev/null
@@ -1,49 +0,0 @@
-export default function ({ $axios, store, redirect, route }) {
- $axios.onRequest((config) => {
- const token = store.state.auth.jwtToken
- if (token) {
- config.headers.common.Authorization = `Bearer ${token}`
- }
-
- // disable progress on auth
- if (config.url.includes('auth') || config.url.includes('process') || config.url.includes('current')) {
- config.progress = false
- }
- })
-
- $axios.interceptors.response.use((response) => {
- return response
- }, (error) => {
- const originalRequest = error.config
-
- // prevent infinite loop
- if (error.response.status === 401 && route.path !== '/') {
- store.commit('auth/REMOVE_TOKEN')
- redirect('/')
- return Promise.reject(error)
- }
-
- if (error.response.status === 401 && !originalRequest._retry && !originalRequest.url.includes('auth/token')) {
- originalRequest._retry = true
-
- store.commit('auth/REMOVE_TOKEN')
- store.commit('auth/UPDATE_IS_LOGIN', false)
- redirect('/')
- }
- return Promise.reject(error)
- })
-
- $axios.onError((error) => {
- const code = parseInt(error.response && error.response.status)
-
- if (code === 401 && route.path !== '/') {
- redirect('/')
- } else if (code !== 401 && code !== 409 && code !== 503 !== (code === 408 && route.path.includes('/media/current'))) {
- store.commit('UPDATE_VARIANT', 'danger')
- store.commit('UPDATE_SHOW_ERROR_ALERT', true)
- store.commit('UPDATE_ERROR_ALERT_MESSAGE', error)
- }
-
- return error.response
- })
-}
diff --git a/plugins/bootstrap.client.js b/plugins/bootstrap.client.js
new file mode 100644
index 00000000..ca506d13
--- /dev/null
+++ b/plugins/bootstrap.client.js
@@ -0,0 +1,6 @@
+import bootstrap from 'bootstrap/dist/js/bootstrap.bundle.js'
+import "bootstrap-icons/font/bootstrap-icons.css";
+
+export default defineNuxtPlugin((nuxtApp) => {
+ nuxtApp.provide('bootstrap', bootstrap)
+})
diff --git a/plugins/dayjs.ts b/plugins/dayjs.ts
new file mode 100644
index 00000000..20a65c6e
--- /dev/null
+++ b/plugins/dayjs.ts
@@ -0,0 +1,21 @@
+import * as dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc.js'
+import timezone from 'dayjs/plugin/timezone.js'
+
+export default defineNuxtPlugin((nuxtApp) => {
+ dayjs.extend(utc)
+ dayjs.extend(timezone)
+ nuxtApp.provide('dayjs', dayjs)
+})
+
+// declare module '#app' {
+// interface NuxtApp {
+// $dayjs: dayjs.Dayjs
+// }
+// }
+
+// declare module '@vue/runtime-core' {
+// interface ComponentCustomProperties {
+// $dayjs(date?: dayjs.ConfigType): dayjs.Dayjs
+// }
+// }
diff --git a/plugins/draggable.js b/plugins/draggable.js
deleted file mode 100644
index 145e2cbb..00000000
--- a/plugins/draggable.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import Vue from 'vue'
-import draggable from 'vuedraggable'
-
-Vue.use(draggable)
-/* eslint-disable-next-line */
-Vue.component('draggable', draggable)
diff --git a/plugins/filters.js b/plugins/filters.js
deleted file mode 100644
index 4b813ce3..00000000
--- a/plugins/filters.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import Vue from 'vue'
-
-Vue.filter('toMin', function (sec) {
- if (sec) {
- const minutes = Math.floor(sec / 60)
- const seconds = Math.round(sec - minutes * 60)
- return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')} min`
- } else {
- return ''
- }
-})
-
-Vue.filter('filename', function (path) {
- if (path) {
- const pathArr = path.split('/')
- return pathArr[pathArr.length - 1]
- } else {
- return ''
- }
-})
diff --git a/plugins/helpers.js b/plugins/helpers.js
deleted file mode 100644
index ab240688..00000000
--- a/plugins/helpers.js
+++ /dev/null
@@ -1,62 +0,0 @@
-export default ({ app }, inject) => {
- inject('processPlaylist', (dayStart, length, list, forSave) => {
- if (!dayStart) {
- dayStart = 0
- }
-
- let begin = dayStart
- const newList = []
-
- for (const item of list) {
- item.begin = begin
-
- if (!item.audio) {
- delete item.audio
- }
-
- if (!item.category) {
- delete item.category
- }
-
- if (!item.custom_filter) {
- delete item.custom_filter
- }
-
- if (begin + (item.out - item.in) > length + dayStart) {
- item.class = 'overLength'
-
- if (forSave) {
- item.out = (length + dayStart) - begin
- }
- }
-
- if (forSave && begin >= length + dayStart) {
- break
- }
-
- newList.push(item)
-
- begin += (item.out - item.in)
- }
-
- return newList
- })
-
- // convert time (00:00:00) string to seconds
- inject('timeToSeconds', (time) => {
- const t = time.split(':')
- return parseInt(t[0]) * 3600 + parseInt(t[1]) * 60 + parseInt(t[2])
- })
-
- inject('secToHMS', (sec) => {
- let hours = Math.floor(sec / 3600)
- sec %= 3600
- let minutes = Math.floor(sec / 60)
- let seconds = sec % 60
-
- minutes = String(minutes).padStart(2, '0')
- hours = String(hours).padStart(2, '0')
- seconds = String(parseInt(seconds)).padStart(2, '0')
- return hours + ':' + minutes + ':' + seconds
- })
-}
diff --git a/plugins/loading.js b/plugins/loading.js
deleted file mode 100644
index 3399b3c1..00000000
--- a/plugins/loading.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import Vue from 'vue'
-import Loading from 'vue-loading-overlay'
-import 'vue-loading-overlay/dist/vue-loading.css'
-
-Vue.use(Loading)
-/* eslint-disable-next-line */
-Vue.component('loading', Loading)
diff --git a/plugins/lodash.client.js b/plugins/lodash.client.js
new file mode 100644
index 00000000..0211d9c6
--- /dev/null
+++ b/plugins/lodash.client.js
@@ -0,0 +1,5 @@
+import _ from 'lodash'
+
+export default defineNuxtPlugin((nuxtApp) => {
+ nuxtApp.provide('_', _)
+})
diff --git a/plugins/lodash.js b/plugins/lodash.js
deleted file mode 100644
index 268dd06a..00000000
--- a/plugins/lodash.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import Vue from 'vue'
-import _ from 'lodash'
-
-Object.defineProperty(Vue.prototype, '$_', { value: _ })
diff --git a/plugins/nuxt-client-init.js b/plugins/nuxt-client-init.js
deleted file mode 100644
index 489a6c55..00000000
--- a/plugins/nuxt-client-init.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default async (context) => {
- await context.store.dispatch('config/nuxtClientInit', context)
-}
diff --git a/plugins/sortable.client.ts b/plugins/sortable.client.ts
new file mode 100644
index 00000000..beae3657
--- /dev/null
+++ b/plugins/sortable.client.ts
@@ -0,0 +1,6 @@
+import { defineNuxtPlugin } from '#app'
+import { Sortable } from 'sortablejs-vue3'
+
+export default defineNuxtPlugin((nuxtApp) => {
+ nuxtApp.vueApp.component('Sortable', Sortable)
+})
diff --git a/plugins/splitpanes.js b/plugins/splitpanes.js
deleted file mode 100644
index ca9269ed..00000000
--- a/plugins/splitpanes.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import Vue from 'vue'
-import { Splitpanes, Pane } from 'splitpanes'
-import 'splitpanes/dist/splitpanes.css'
-
-/* eslint-disable */
-Vue.component('splitpanes', Splitpanes)
-Vue.component('pane', Pane)
diff --git a/plugins/video.js b/plugins/video.js
deleted file mode 100644
index 0d135c56..00000000
--- a/plugins/video.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import Vue from 'vue'
-import VideoPlayer from '@/components/VideoPlayer.vue'
-
-/* eslint-disable-next-line */
-Vue.component('video-player', VideoPlayer)
diff --git a/static/favicon.ico b/public/favicon.ico
similarity index 100%
rename from static/favicon.ico
rename to public/favicon.ico
diff --git a/static/robots.txt b/public/robots.txt
similarity index 100%
rename from static/robots.txt
rename to public/robots.txt
diff --git a/static/live/README.md b/static/live/README.md
deleted file mode 100644
index 9262f50f..00000000
--- a/static/live/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-This folder can be used for HLS streaming playlist.
-
-In production it is recommend to serve ***.m3u8** and ***.ts** with nginx or another web server.
diff --git a/store/README.md b/store/README.md
deleted file mode 100644
index 1972d277..00000000
--- a/store/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# STORE
-
-**This directory is not required, you can delete it if you don't want to use it.**
-
-This directory contains your Vuex Store files.
-Vuex Store option is implemented in the Nuxt.js framework.
-
-Creating a file in this directory automatically activates the option in the framework.
-
-More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
diff --git a/store/auth.js b/store/auth.js
deleted file mode 100644
index 8cfef072..00000000
--- a/store/auth.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/* eslint-disable camelcase */
-import jwt_decode from 'jwt-decode'
-
-export const state = () => ({
- jwtToken: '',
- isLogin: false
-})
-
-// mutate values in state
-export const mutations = {
- UPDATE_TOKEN (state, obj) {
- state.jwtToken = obj.token
- this.$cookies.set('token', obj.token, {
- path: '/',
- maxAge: 60 * 60 * 24 * 365,
- sameSite: 'lax'
- })
- },
- UPDATE_IS_LOGIN (state, bool) {
- state.isLogin = bool
- },
- REMOVE_TOKEN (state) {
- this.$cookies.remove('token')
- state.jwtToken = null
- }
-}
-
-export const actions = {
- async obtainToken ({ commit }, { username, password }) {
- const payload = {
- username,
- password
- }
- let code = null
- await this.$axios.post('auth/login/', payload)
- .then((response) => {
- commit('UPDATE_TOKEN', { token: response.data.user.token })
- commit('UPDATE_IS_LOGIN', true)
- code = response.status
- })
- .catch((error) => {
- code = error.response.status
- })
-
- return code
- },
-
- inspectToken ({ commit, state }) {
- const token = this.$cookies.get('token')
-
- if (token) {
- commit('UPDATE_TOKEN', { token })
- const decoded_token = jwt_decode(token)
- const timestamp = Date.now() / 1000
- const expire_token = decoded_token.exp
-
- if (state.jwtToken && expire_token - timestamp > 15) {
- commit('UPDATE_IS_LOGIN', true)
- } else {
- // PROMPT USER TO RE-LOGIN, THIS ELSE CLAUSE COVERS THE CONDITION WHERE A TOKEN IS EXPIRED AS WELL
- commit('UPDATE_IS_LOGIN', false)
- }
- } else {
- commit('UPDATE_IS_LOGIN', false)
- }
- }
-}
diff --git a/store/config.js b/store/config.js
deleted file mode 100644
index 1c080602..00000000
--- a/store/config.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import _ from 'lodash'
-
-export const state = () => ({
- configID: 0,
- configCount: 0,
- configGui: null,
- configGuiRaw: null,
- startInSec: 0,
- playlistLength: 86400.0,
- configPlayout: {},
- currentUser: null,
- configUser: null,
- utcOffset: 0
-})
-
-export const mutations = {
- UPDATE_CONFIG_ID (state, id) {
- state.configID = id
- },
- UPDATE_CONFIG_COUNT (state, count) {
- state.configCount = count
- },
- UPDATE_GUI_CONFIG (state, config) {
- state.configGui = config
- },
- UPDATE_GUI_CONFIG_RAW (state, config) {
- state.configGuiRaw = config
- },
- UPDATE_START_TIME (state, sec) {
- state.startInSec = sec
- },
- UPDATE_PLAYLIST_LENGTH (state, sec) {
- state.playlistLength = sec
- },
- UPDATE_PLAYOUT_CONFIG (state, config) {
- state.configPlayout = config
- },
- SET_CURRENT_USER (state, user) {
- state.currentUser = user
- },
- UPDATE_USER_CONFIG (state, config) {
- state.configUser = config
- },
- UPDATE_UTC_OFFSET (state, offset) {
- state.utcOffset = offset
- }
-}
-
-export const actions = {
- async nuxtClientInit ({ dispatch, rootState }) {
- await dispatch('auth/inspectToken', null, { root: true })
- if (rootState.auth.isLogin) {
- await dispatch('getGuiConfig')
- await dispatch('getPlayoutConfig')
- await dispatch('getUserConfig')
- }
- },
-
- async getGuiConfig ({ commit }) {
- const response = await this.$axios.get('api/channels')
-
- if (response.data) {
- for (const data of response.data) {
- if (data.extra_extensions) {
- data.extra_extensions = data.extra_extensions.split(',')
- } else {
- data.extra_extensions = []
- }
- }
-
- commit('UPDATE_UTC_OFFSET', response.data[0].utc_offset)
- commit('UPDATE_GUI_CONFIG', response.data)
- commit('UPDATE_GUI_CONFIG_RAW', _.cloneDeep(response.data))
- commit('UPDATE_CONFIG_COUNT', response.data.length)
- } else {
- commit('UPDATE_GUI_CONFIG', [{
- id: 1,
- channel: '',
- preview_url: '',
- playout_config: '',
- extra_extensions: [],
- utc_offset: 0
- }])
- }
- },
-
- async setGuiConfig ({ commit, state, dispatch }, obj) {
- const stringObj = _.cloneDeep(obj)
- stringObj.extra_extensions = stringObj.extra_extensions.join(',')
- let response
-
- if (state.configGuiRaw.some(e => e.id === stringObj.id)) {
- response = await this.$axios.patch(`api/channel/${obj.id}`, stringObj)
- } else {
- response = await this.$axios.post('api/channel/', stringObj)
- const guiConfigs = []
-
- for (const obj of state.configGui) {
- if (obj.name === stringObj.name) {
- response.data.extra_extensions = response.data.extra_extensions.split(',')
- guiConfigs.push(response.data)
- } else {
- guiConfigs.push(obj)
- }
- }
-
- commit('UPDATE_GUI_CONFIG', guiConfigs)
- commit('UPDATE_GUI_CONFIG_RAW', _.cloneDeep(guiConfigs))
- commit('UPDATE_CONFIG_COUNT', guiConfigs.length)
-
- await dispatch('getPlayoutConfig')
- }
-
- return response
- },
-
- async getPlayoutConfig ({ commit, state, rootState }) {
- const channel = state.configGui[state.configID].id
- const response = await this.$axios.get(`api/playout/config/${channel}`)
-
- if (response.data) {
- if (response.data.playlist.day_start) {
- commit('UPDATE_START_TIME', this.$timeToSeconds(response.data.playlist.day_start))
- }
-
- if (response.data.playlist.length) {
- commit('UPDATE_PLAYLIST_LENGTH', this.$timeToSeconds(response.data.playlist.length))
- }
-
- commit('UPDATE_PLAYOUT_CONFIG', response.data)
- } else {
- rootState.showErrorAlert = true
- rootState.ErrorAlertMessage = 'No playout config found!'
- }
- },
-
- async setPlayoutConfig ({ state }, obj) {
- const channel = state.configGui[state.configID].id
- const update = await this.$axios.put(`api/playout/config/${channel}`, obj)
- return update
- },
-
- async getUserConfig ({ commit }) {
- const user = await this.$axios.get('api/user')
-
- if (user.data) {
- commit('SET_CURRENT_USER', user.data.username)
- }
- if (user.data) {
- commit('UPDATE_USER_CONFIG', user.data)
- }
- },
-
- async setUserConfig (obj) {
- const update = await this.$axios.put(`api/user/${obj.id}`, obj)
- return update
- }
-}
diff --git a/store/index.js b/store/index.js
deleted file mode 100644
index ab0cd4ec..00000000
--- a/store/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export const strict = false
-
-export const state = () => ({
- showErrorAlert: false,
- variant: 'danger',
- ErrorAlertMessage: ''
-})
-
-export const mutations = {
- UPDATE_SHOW_ERROR_ALERT (state, show) {
- state.showErrorAlert = show
- },
- UPDATE_VARIANT (state, variant) {
- state.variant = variant
- },
- UPDATE_ERROR_ALERT_MESSAGE (state, message) {
- state.ErrorAlertMessage = message
- }
-}
diff --git a/store/media.js b/store/media.js
deleted file mode 100644
index c4e2e36a..00000000
--- a/store/media.js
+++ /dev/null
@@ -1,49 +0,0 @@
-export const state = () => ({
- currentPath: null,
- crumbs: [],
- folderTree: {}
-})
-
-export const mutations = {
- UPDATE_CURRENT_PATH (state, path) {
- state.currentPath = path
- },
- UPDATE_CRUMBS (state, crumbs) {
- state.crumbs = crumbs
- },
- UPDATE_FOLDER_TREE (state, tree) {
- state.folderTree = tree
- }
-}
-
-export const actions = {
- async getTree ({ commit, rootState }, { path }) {
- const crumbs = []
- let root = '/'
- const channel = rootState.config.configGui[rootState.config.configID].id
- const response = await this.$axios.post(
- `api/file/${channel}/browse/`, { source: path })
-
- if (response.data) {
- const pathStr = 'Home/' + response.data.source
- const pathArr = pathStr.split('/')
-
- if (path) {
- for (const crumb of pathArr) {
- if (crumb === 'Home') {
- crumbs.push({ text: crumb, path: root })
- } else if (crumb) {
- root += crumb + '/'
- crumbs.push({ text: crumb, path: root })
- }
- }
- } else {
- crumbs.push({ text: 'Home', path: '' })
- }
-
- commit('UPDATE_CURRENT_PATH', path)
- commit('UPDATE_CRUMBS', crumbs)
- commit('UPDATE_FOLDER_TREE', response.data)
- }
- }
-}
diff --git a/store/playlist.js b/store/playlist.js
deleted file mode 100644
index 6804f19d..00000000
--- a/store/playlist.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import _ from 'lodash'
-
-export const state = () => ({
- playlist: null,
- playlistToday: [],
- progressValue: 0,
- currentClip: 'No clips is playing',
- currentClipIndex: null,
- currentClipStart: null,
- currentClipDuration: null,
- currentClipIn: null,
- currentClipOut: null,
- timeStr: '00:00:00',
- timeLeft: '00:00:00'
-})
-
-export const mutations = {
- UPDATE_PLAYLIST (state, list) {
- state.playlist = list
- },
- UPDATE_TODAYS_PLAYLIST (state, list) {
- state.playlistToday = list
- },
- SET_PROGRESS_VALUE (state, value) {
- state.progressValue = value
- },
- SET_CURRENT_CLIP (state, clip) {
- state.currentClip = clip
- },
- SET_CURRENT_CLIP_INDEX (state, index) {
- state.currentClipIndex = index
- },
- SET_CURRENT_CLIP_START (state, start) {
- state.currentClipStart = start
- },
- SET_CURRENT_CLIP_DURATION (state, dur) {
- state.currentClipDuration = dur
- },
- SET_CURRENT_CLIP_IN (state, _in) {
- state.currentClipIn = _in
- },
- SET_CURRENT_CLIP_OUT (state, out) {
- state.currentClipOut = out
- },
- SET_TIME (state, time) {
- state.timeStr = time
- },
- SET_TIME_LEFT (state, time) {
- state.timeLeft = time
- }
-}
-
-export const actions = {
- async getPlaylist ({ commit, rootState }, { date }) {
- const timeInSec = this.$timeToSeconds(this.$dayjs().utcOffset(rootState.config.utcOffset).format('HH:mm:ss'))
- const channel = rootState.config.configGui[rootState.config.configID].id
- let dateToday = this.$dayjs().utcOffset(rootState.config.utcOffset).format('YYYY-MM-DD')
-
- if (rootState.config.startInSec > timeInSec) {
- dateToday = this.$dayjs(dateToday).utcOffset(rootState.config.utcOffset).subtract(1, 'day').format('YYYY-MM-DD')
- }
-
- const response = await this.$axios.get(`api/playlist/${channel}?date=${date}`)
-
- if (response.data && response.data.program) {
- commit('UPDATE_PLAYLIST', this.$processPlaylist(rootState.config.startInSec, rootState.config.playlistLength, response.data.program, false))
-
- if (date === dateToday) {
- commit('UPDATE_TODAYS_PLAYLIST', _.cloneDeep(response.data.program))
- } else {
- commit('SET_CURRENT_CLIP_INDEX', null)
- }
- } else {
- commit('UPDATE_PLAYLIST', [])
- }
- },
-
- async playoutStat ({ commit, state, rootState }) {
- const channel = rootState.config.configGui[rootState.config.configID].id
- const time = this.$dayjs().utcOffset(rootState.config.utcOffset).format('HH:mm:ss')
- let timeSec = this.$timeToSeconds(time)
-
- commit('SET_TIME', time)
-
- if (timeSec < rootState.config.startInSec) {
- timeSec += rootState.config.playlistLength
- }
-
- if (timeSec < state.currentClipStart) {
- return
- }
-
- const response = await this.$axios.get(`api/control/${channel}/media/current`)
-
- if (response.data && response.data.result && response.data.result.played_sec) {
- const obj = response.data.result
- const progValue = obj.played_sec * 100 / obj.current_media.out
- commit('SET_PROGRESS_VALUE', progValue)
- commit('SET_CURRENT_CLIP', obj.current_media.source)
- commit('SET_CURRENT_CLIP_INDEX', obj.index)
- commit('SET_CURRENT_CLIP_START', obj.start_sec)
- commit('SET_CURRENT_CLIP_DURATION', obj.current_media.duration)
- commit('SET_CURRENT_CLIP_IN', obj.current_media.seek)
- commit('SET_CURRENT_CLIP_OUT', obj.current_media.out)
- commit('SET_TIME_LEFT', this.$secToHMS(obj.remaining_sec))
- }
- }
-}
diff --git a/stores/auth.ts b/stores/auth.ts
new file mode 100644
index 00000000..3ff2a7cf
--- /dev/null
+++ b/stores/auth.ts
@@ -0,0 +1,90 @@
+import { defineStore } from 'pinia'
+import jwtDecode, { JwtPayload } from 'jwt-decode'
+
+export const useAuth = defineStore('auth', {
+ state: () => ({
+ isLogin: false,
+ jwtToken: '',
+ authHeader: {},
+ }),
+
+ getters: {},
+ actions: {
+ updateToken(token: string) {
+ const cookie = useCookie('token', {
+ path: '/',
+ maxAge: 60 * 60 * 24 * 365,
+ sameSite: 'lax',
+ })
+
+ cookie.value = token
+ this.jwtToken = token
+ this.authHeader = { Authorization: `Bearer ${token}` }
+ },
+
+ updateIsLogin(bool: boolean) {
+ this.isLogin = bool
+ },
+
+ removeToken() {
+ const cookie = useCookie('token')
+ cookie.value = null
+ this.jwtToken = ''
+ this.authHeader = {}
+ },
+
+ async obtainToken(username: string, password: string) {
+ let code = 0
+ const payload = {
+ username,
+ password,
+ }
+
+ await fetch('auth/login/', {
+ method: 'POST',
+ headers: new Headers([['content-type', 'application/json;charset=UTF-8']]),
+ body: JSON.stringify(payload),
+ })
+ .then((response) => {
+ code = response.status
+ return response
+ })
+ .then((response) => response.json())
+ .then((response) => {
+ this.updateToken(response.user.token)
+ this.updateIsLogin(true)
+ })
+ .catch((error) => {
+ if (error.status) {
+ code = error.status
+ }
+ })
+
+ return code
+ },
+
+ inspectToken() {
+ let token = useCookie('token').value
+
+ if (token === null) {
+ token = ''
+ }
+
+ if (token) {
+ this.updateToken(token)
+ const decodedToken = jwtDecode
(token)
+ const timestamp = Date.now() / 1000
+ const expireToken = decodedToken.exp
+
+ if (expireToken && this.jwtToken && expireToken - timestamp > 15) {
+ this.updateIsLogin(true)
+ } else {
+ // Prompt user to re login.
+ this.updateIsLogin(false)
+ }
+ } else {
+ this.updateIsLogin(false)
+ }
+ },
+ },
+})
diff --git a/stores/config.ts b/stores/config.ts
new file mode 100644
index 00000000..124c4e04
--- /dev/null
+++ b/stores/config.ts
@@ -0,0 +1,240 @@
+import _ from 'lodash'
+import { defineStore } from 'pinia'
+
+const { timeToSeconds } = stringFormatter()
+
+import { useAuth } from '~/stores/auth'
+import { useIndex } from '~/stores/index'
+const authStore = useAuth()
+const indexStore = useIndex()
+
+interface GuiConfig {
+ id: number
+ config_path: string
+ extra_extensions: string
+ name: string
+ preview_url: string
+ service: string
+ uts_offset?: number
+}
+
+interface User {
+ username: string
+ mail: string
+ password?: string
+}
+
+export const useConfig = defineStore('config', {
+ state: () => ({
+ configID: 0,
+ configCount: 0,
+ configGui: [] as GuiConfig[],
+ configGuiRaw: [] as GuiConfig[],
+ startInSec: 0,
+ playlistLength: 86400.0,
+ configPlayout: {} as any,
+ currentUser: '',
+ configUser: {} as User,
+ utcOffset: 0,
+ }),
+
+ getters: {},
+ actions: {
+ updateConfigID(id: number) {
+ this.configID = id
+ },
+
+ updateConfigCount(count: number) {
+ this.configCount = count
+ },
+
+ updateGuiConfig(config: GuiConfig[]) {
+ this.configGui = config
+ },
+
+ updateGuiConfigRaw(config: GuiConfig[]) {
+ this.configGuiRaw = config
+ },
+
+ updateStartTime(sec: number) {
+ this.startInSec = sec
+ },
+
+ updatePlaylistLength(sec: number) {
+ this.playlistLength = sec
+ },
+
+ updatePlayoutConfig(config: any) {
+ this.configPlayout = config
+ },
+
+ setCurrentUser(user: string) {
+ this.currentUser = user
+ },
+
+ updateUserConfig(config: User) {
+ this.configUser = config
+ },
+
+ updateUtcOffset(offset: number) {
+ this.utcOffset = offset
+ },
+
+ async nuxtClientInit() {
+ authStore.inspectToken()
+
+ if (authStore.isLogin) {
+ await this.getGuiConfig()
+ await this.getPlayoutConfig()
+ await this.getUserConfig()
+ }
+ },
+
+ async getGuiConfig() {
+ let statusCode = 0
+ await fetch('api/channels', {
+ method: 'GET',
+ headers: authStore.authHeader,
+ })
+ .then(response => {
+ statusCode = response.status
+
+ return response
+ })
+ .then((response) => response.json())
+ .then((objs) => {
+ this.updateUtcOffset(objs[0].utc_offset)
+ this.updateGuiConfig(objs)
+ this.updateGuiConfigRaw(_.cloneDeep(objs))
+ this.updateConfigCount(objs.length)
+ })
+ .catch((e) => {
+ if (statusCode === 401) {
+ const cookie = useCookie('token')
+ cookie.value = null
+ authStore.isLogin = false
+
+ navigateTo('/')
+ }
+
+ this.updateGuiConfig([
+ {
+ id: 1,
+ config_path: '',
+ extra_extensions: '',
+ name: 'Channel 1',
+ preview_url: '',
+ service: '',
+ uts_offset: 0,
+ },
+ ])
+
+ indexStore.errorAlertMessage = e
+ indexStore.showErrorAlert = true
+ })
+ },
+
+ async setGuiConfig(obj: GuiConfig): Promise {
+ const stringObj = _.cloneDeep(obj)
+ const contentType = { 'content-type': 'application/json;charset=UTF-8' }
+ let response
+
+ if (this.configGuiRaw.some((e) => e.id === stringObj.id)) {
+ response = await fetch(`api/channel/${obj.id}`, {
+ method: 'PATCH',
+ headers: { ...contentType, ...authStore.authHeader },
+ body: JSON.stringify(stringObj),
+ })
+ } else {
+ response = await fetch('api/channel/', {
+ method: 'POST',
+ headers: { ...contentType, ...authStore.authHeader },
+ body: JSON.stringify(stringObj),
+ })
+
+ const json = await response.json()
+ const guiConfigs = []
+
+ for (const obj of this.configGui) {
+ if (obj.name === stringObj.name) {
+ guiConfigs.push(json)
+ } else {
+ guiConfigs.push(obj)
+ }
+ }
+
+ this.updateGuiConfig(guiConfigs)
+ this.updateGuiConfigRaw(_.cloneDeep(guiConfigs))
+ this.updateConfigCount(guiConfigs.length)
+
+ await this.getPlayoutConfig()
+ }
+
+ return response
+ },
+
+ async getPlayoutConfig() {
+ const channel = this.configGui[this.configID].id
+
+ await fetch(`api/playout/config/${channel}`, {
+ method: 'GET',
+ headers: authStore.authHeader,
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.playlist.day_start) {
+ const start = timeToSeconds(data.playlist.day_start)
+ this.updateStartTime(start)
+ }
+
+ if (data.playlist.length) {
+ const length = timeToSeconds(data.playlist.length)
+ this.updatePlaylistLength(length)
+ }
+
+ this.updatePlayoutConfig(data)
+ })
+ .catch(() => {
+ indexStore.showErrorAlert = true
+ indexStore.errorAlertMessage = 'No playout config found!'
+ })
+ },
+
+ async setPlayoutConfig(obj: any) {
+ const channel = this.configGui[this.configID].id
+ const contentType = { 'content-type': 'application/json;charset=UTF-8' }
+
+ const update = await fetch(`api/playout/config/${channel}`, {
+ method: 'PUT',
+ headers: { ...contentType, ...authStore.authHeader },
+ body: JSON.stringify(obj),
+ })
+
+ return update
+ },
+
+ async getUserConfig() {
+ await fetch('api/user', {
+ method: 'GET',
+ headers: authStore.authHeader,
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ this.setCurrentUser(data.username)
+ this.updateUserConfig(data)
+ })
+ },
+
+ async setUserConfig(obj: any) {
+ const contentType = { 'content-type': 'application/json;charset=UTF-8' }
+
+ const update = await fetch(`api/user/${obj.id}`, {
+ method: 'PUT',
+ headers: { ...contentType, ...authStore.authHeader },
+ body: JSON.stringify(obj),
+ })
+
+ return update
+ },
+ },
+})
diff --git a/stores/index.ts b/stores/index.ts
new file mode 100644
index 00000000..36f93648
--- /dev/null
+++ b/stores/index.ts
@@ -0,0 +1,18 @@
+import { defineStore } from 'pinia'
+
+export const useIndex = defineStore('index', {
+ state: () => ({
+ showAlert: false,
+ alertVariant: 'success',
+ alertMsg: '',
+ }),
+
+ getters: {},
+ actions: {
+ resetAlert() {
+ this.showAlert = false
+ this.alertVariant = 'success'
+ this.alertMsg = ''
+ },
+ },
+})
diff --git a/stores/media.ts b/stores/media.ts
new file mode 100644
index 00000000..e0971184
--- /dev/null
+++ b/stores/media.ts
@@ -0,0 +1,52 @@
+import { defineStore } from 'pinia'
+
+import { useAuth } from '~/stores/auth'
+import { useConfig } from '~/stores/config'
+const authStore = useAuth()
+const configStore = useConfig()
+
+export const useMedia = defineStore('media', {
+ state: () => ({
+ currentPath: '',
+ crumbs: [] as Crumb[],
+ folderTree: {} as FolderObject,
+ }),
+
+ getters: {},
+ actions: {
+ async getTree(path: string) {
+ const contentType = { 'content-type': 'application/json;charset=UTF-8' }
+ const channel = configStore.configGui[configStore.configID].id
+ const crumbs: Crumb[] = []
+ let root = '/'
+
+ await fetch(`api/file/${channel}/browse/`, {
+ method: 'POST',
+ headers: { ...contentType, ...authStore.authHeader },
+ body: JSON.stringify({ source: path }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ const pathStr = 'Home/' + data.source
+ const pathArr = pathStr.split('/')
+
+ if (path && path !== '/') {
+ for (const crumb of pathArr) {
+ if (crumb === 'Home') {
+ crumbs.push({ text: crumb, path: root })
+ } else if (crumb) {
+ root += crumb + '/'
+ crumbs.push({ text: crumb, path: root })
+ }
+ }
+ } else {
+ crumbs.push({ text: 'Home', path: '' })
+ }
+
+ this.currentPath = path
+ this.crumbs = crumbs
+ this.folderTree = data
+ })
+ },
+ },
+})
diff --git a/stores/playlist.ts b/stores/playlist.ts
new file mode 100644
index 00000000..2c31230a
--- /dev/null
+++ b/stores/playlist.ts
@@ -0,0 +1,96 @@
+import * as dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc.js'
+import timezone from 'dayjs/plugin/timezone.js'
+
+import { defineStore } from 'pinia'
+
+dayjs.extend(utc)
+dayjs.extend(timezone)
+
+import { useAuth } from '~/stores/auth'
+import { useConfig } from '~/stores/config'
+
+const authStore = useAuth()
+const configStore = useConfig()
+const { timeToSeconds, secToHMS } = stringFormatter()
+const { processPlaylist } = playlistOperations()
+
+export const usePlaylist = defineStore('playlist', {
+ state: () => ({
+ playlist: [] as PlaylistItem[],
+ progressValue: 0,
+ currentClip: 'No clip is playing',
+ currentClipIndex: -1,
+ currentClipStart: 0,
+ currentClipDuration: 0,
+ currentClipIn: 0,
+ currentClipOut: 0,
+ timeStr: '00:00:00',
+ remainingSec: 0,
+ playoutIsRunning: true,
+ }),
+
+ getters: {},
+ actions: {
+ updatePlaylist(list: any) {
+ this.playlist = list
+ },
+
+ async getPlaylist(date: string) {
+ const timeInSec = timeToSeconds(dayjs().utcOffset(configStore.utcOffset).format('HH:mm:ss'))
+ const channel = configStore.configGui[configStore.configID].id
+ let dateToday = dayjs().utcOffset(configStore.utcOffset).format('YYYY-MM-DD')
+
+ if (configStore.startInSec > timeInSec) {
+ dateToday = dayjs(dateToday).utcOffset(configStore.utcOffset).subtract(1, 'day').format('YYYY-MM-DD')
+ }
+
+ await fetch(`api/playlist/${channel}?date=${date}`, {
+ method: 'GET',
+ headers: authStore.authHeader,
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.program) {
+ this.updatePlaylist(
+ processPlaylist(configStore.startInSec, configStore.playlistLength, data.program, false)
+ )
+ }
+ })
+ .catch(() => {
+ this.updatePlaylist([])
+ })
+ },
+
+ async playoutStat() {
+ const channel = configStore.configGui[configStore.configID].id
+
+ await fetch(`api/control/${channel}/media/current`, {
+ method: 'GET',
+ headers: authStore.authHeader,
+ })
+ .then((response) => {
+ if (response.status === 503) {
+ this.playoutIsRunning = false
+ }
+
+ return response.json()
+ })
+ .then((data) => {
+ if (data.result && data.result.played_sec) {
+ this.playoutIsRunning = true
+ const obj = data.result
+ this.currentClip = obj.current_media.source
+ this.currentClipIndex = obj.index
+ this.currentClipStart = obj.start_sec
+ this.currentClipDuration = obj.current_media.duration
+ this.currentClipIn = obj.current_media.seek
+ this.currentClipOut = obj.current_media.out
+ }
+ })
+ .catch(() => {
+ this.playoutIsRunning = false
+ })
+ },
+ },
+})
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..80406628
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "exclude": [
+ "./public"
+ ],
+ // https://nuxt.com/docs/guide/concepts/typescript
+ "extends": "./.nuxt/tsconfig.json"
+}
diff --git a/types/intex.ts b/types/intex.ts
new file mode 100644
index 00000000..ff9ca926
--- /dev/null
+++ b/types/intex.ts
@@ -0,0 +1,38 @@
+export {}
+
+declare global {
+ interface Crumb {
+ text: string
+ path: string
+ }
+
+ interface PlaylistItem {
+ uid: string
+ begin: number
+ source: string
+ duration: number
+ in: number
+ out: number
+ audio?: string
+ category?: string
+ custom_filter?: string
+ class?: string
+ }
+
+ interface FileObject {
+ name: string
+ duration: number
+ }
+
+ interface FolderObject {
+ source: string
+ parent: string
+ folders: string[]
+ files: FileObject[]
+ }
+
+ interface SourceObject {
+ type: string
+ src: string
+ }
+}