Finalizing The WebUI for alpha/beta testing next week.

The WebUI is mostly done for now, The only missing piece is backend view, which isn't something we will include in the first release.
This commit is contained in:
abdulmohsen
2024-05-10 18:56:17 +03:00
parent fbd95471ec
commit 5f0874d64b
15 changed files with 403 additions and 212 deletions

View File

@@ -183,3 +183,32 @@ hr {
border: var(--bulma-control-border-width) solid rgba(56, 56, 56, 0.38);
}
@media screen and (min-width: 769px), print {
.field.is-grouped-tablet {
display: flex;
gap: 0.75rem;
justify-content: flex-start;
}
.field.is-grouped-tablet > .control {
flex-shrink: 0;
}
.field.is-grouped-tablet > .control.is-expanded {
flex-grow: 1;
flex-shrink: 1;
}
.field.is-grouped-tablet.is-grouped-centered {
justify-content: center;
}
.field.is-grouped-tablet.is-grouped-right {
justify-content: flex-end;
}
.field.is-grouped-tablet.is-grouped-multiline {
flex-wrap: wrap;
}
}

View File

@@ -12,7 +12,7 @@
</option>
</select>
</div>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-server"></i>
</div>
<p class="help">
@@ -25,7 +25,7 @@
<label class="label">Name</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.name" required>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-user"></i>
</div>
<p class="help">
@@ -42,13 +42,15 @@
</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.token" required>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-key"></i>
</div>
<p class="help">
<template v-if="'plex'===backend.type">
Enter the <code>X-Plex-Token</code>.<a target="_blank" href="https://support.plex.tv/articles/204059436">
Visit This article for more information</a>.
Enter the <code>X-Plex-Token</code>.
<NuxtLink target="_blank" href="https://support.plex.tv/articles/204059436">
Visit This article for more information.
</NuxtLink>
</template>
<template v-else>
Generate a new API token from <code>Dashboard > Settings > API Keys</code>.
@@ -68,7 +70,7 @@
</select>
</div>
<input class="input" type="text" v-model="backend.url" v-else required>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-link" v-if="!serversLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
@@ -86,7 +88,7 @@
<label class="label">URL</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.url" required>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-link"></i>
</div>
<p class="help">
@@ -100,7 +102,7 @@
<label class="label">Unique Identifier</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.uuid" required>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-server" v-if="!uuidLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
@@ -125,7 +127,7 @@
</select>
</div>
<input class="input" type="text" v-model="backend.user" v-else>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-user-tie" v-if="!usersLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
@@ -188,7 +190,7 @@
<div class="field has-text-right">
<div class="control">
<button class="button is-primary" type="submit">
<span class="icon is-small"><i class="fas fa-plus"></i></span>
<span class="icon"><i class="fas fa-plus"></i></span>
<span>Add Backend</span>
</button>
</div>
@@ -213,7 +215,7 @@ const props = defineProps({
const backend = ref({
name: '',
type: '',
type: 'plex',
url: '',
token: '',
uuid: '',

View File

@@ -72,22 +72,16 @@
<div class="navbar-item">
<button class="button is-dark" @click="selectedTheme = 'light'" v-if="'dark' === selectedTheme"
v-tooltip="'Switch to light theme'">
<span class="icon is-small has-text-warning">
<i class="fas fa-sun"></i>
</span>
<span class="icon has-text-warning"><i class="fas fa-sun"></i></span>
</button>
<button class="button is-dark" @click="selectedTheme = 'dark'" v-if="'light' === selectedTheme"
v-tooltip="'Switch to dark theme'">
<span class="icon is-small">
<i class="fas fa-moon"></i>
</span>
<span class="icon"><i class="fas fa-moon"></i></span>
</button>
</div>
<div class="navbar-item">
<button class="button is-dark" @click="showConnection = !showConnection" v-tooltip="'Configure connection'">
<span class="icon is-small">
<i class="fas fa-cog"></i>
</span>
<span class="icon"><i class="fas fa-cog"></i></span>
</button>
</div>
</div>
@@ -120,7 +114,10 @@
</button>
</div>
</div>
<p class="help">Can be obtained by using the <code>system:apikey</code> command.</p>
<p class="help">
You can obtain the <code>API TOKEN</code> by using the <code>system:apikey</code> command or by
viewing the <code>/config/config/.env</code> and looking for the <code>WS_API_KEY=</code> key.
</p>
</div>
</div>
</div>
@@ -160,14 +157,14 @@
placeholder="API Path... /v1/api"
@keyup="api_status = false; api_response = ''">
<p class="help">
Use <a href="javascript:void(0)" @click="api_path = '/v1/api'">Set default API</a>.
Use <a href="javascript:void(0)" @click="api_path = '/v1/api'">Set default API Path</a>.
</p>
</div>
</div>
</div>
</div>
<div class="field is-grouped has-addons-right">
<div class="field">
<div class="field-body">
<div class="field">
<div class="field has-addons">
@@ -184,7 +181,13 @@
</button>
</div>
</div>
<p class="help">These settings are stored locally in your browser.</p>
<p class="help">
<span class="icon-text">
<span class="icon has-text-danger"><i class="fas fa-info"></i></span>
<span>These settings are stored locally in your browser. You need to re-add them if you access the
<code>WebUI</code> from different browser.</span>
</span>
</p>
</div>
</div>
</div>
@@ -200,30 +203,40 @@
</template>
<div class="columns is-multiline mt-3">
<div class="column is-12">
<div class="column is-12 is-hidden-mobile">
<div class="content">
If you have question, want clarification on something, or just want to chat with other users, you are welcome
to join our <a href="https://discord.gg/haUXHJyj6Y" rel="noreferrer,nofollow,noopener" target="_blank">
<span class="icon-text">
<span class="icon"><i class="fas fa-brands fa-discord"></i></span>
<span>Discord server</span>
</span>
</a>. For real bug reports, feature requests, or contributions, please visit the <a
href="https://github.com/arabcoders/watchstate/issues/new/choose" rel="noreferrer,nofollow,noopener">
<span class="icon-text">
<span class="icon"><i class="fas fa-brands fa-github"></i></span>
<span>GitHub repository</span>
</span>
</a>.
<Message v-if="show_page_info" title="Information">
<button class="delete" @click="show_page_info = false"></button>
If you have question, or want clarification on something, or just want to chat with other users, you are
welcome to join our
<NuxtLink href="https://discord.gg/haUXHJyj6Y" target="_blank">
<span class="icon-text is-underlined">
<span class="icon"><i class="fas fa-brands fa-discord"></i></span>
<span>Discord server</span>
</span>
</NuxtLink>
. For real bug reports, feature requests, or contributions, please visit the
<NuxtLink href="https://github.com/arabcoders/watchstate/issues/new/choose" target="_blank">
<span class="icon-text is-underlined">
<span class="icon"><i class="fas fa-brands fa-github"></i></span>
<span>GitHub repository</span>
</span>
</NuxtLink>
.
</Message>
</div>
</div>
<div class="column is-6 is-12-mobile has-text-left">
{{ api_version }} - <a href="https://github.com/arabcoders/watchstate" target="_blank">WatchState</a>
<template v-if="!show_page_info">
<span class="is-hidden-mobile">
- <a href="javascript:void(0)" @click="show_page_info=true">Show Info</a>
</span>
</template>
</div>
</div>
<NuxtNotifications position="top right" :speed="800" :ignoreDuplicates="true" :width="340" :pauseOnHover="true"/>
</div>
</template>
@@ -241,15 +254,15 @@ const showConnection = ref(false)
const api_url = useStorage('api_url', window.location.origin)
const api_path = useStorage('api_path', '/v1/api')
const api_token = useStorage('api_token', '')
const show_page_info = useStorage('show_page_info', true)
const api_status = ref(false)
const api_response = ref('Status: Unknown')
const api_version = useStorage('api_version', 'dev-master')
const Year = ref(new Date().getFullYear())
const showMenu = ref(false)
const exposeToken = ref(false)
const applyPreferredColorScheme = (scheme) => {
const applyPreferredColorScheme = scheme => {
for (let s = 0; s < document.styleSheets.length; s++) {
for (let i = 0; i < document.styleSheets[s].cssRules.length; i++) {
try {
@@ -298,7 +311,7 @@ onMounted(async () => {
}
})
watch(selectedTheme, (value) => {
watch(selectedTheme, value => {
try {
applyPreferredColorScheme(value)
} catch (e) {

View File

@@ -12,12 +12,13 @@
"dependencies": {
"@vueuse/core": "^10.9.0",
"@vueuse/nuxt": "^10.9.0",
"cronstrue": "^2.49.0",
"floating-vue": "^5.2.2",
"moment": "^2.30.1",
"nuxt": "^3.11.2",
"nuxt3-notifications": "^1.2.0",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"cronstrue": "^2.49.0"
}
"vue-router": "^4.3.0"
},
"devDependencies": {}
}

View File

@@ -26,7 +26,7 @@
<label class="label">Name</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.name" required readonly disabled>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-user"></i>
</div>
<p class="help">
@@ -40,7 +40,7 @@
<label class="label">Type</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.type" readonly disabled>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-globe"></i>
</div>
</div>
@@ -50,7 +50,7 @@
<label class="label">URL</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.url" required>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-link"></i>
</div>
<p class="help">
@@ -67,7 +67,7 @@
</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.token" required>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-key"></i>
</div>
<p class="help">
@@ -88,7 +88,7 @@
<label class="label">Backend Unique ID</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.uuid" required>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-server" v-if="!uuidLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
@@ -110,7 +110,7 @@
</select>
</div>
<input class="input" type="text" v-model="backend.user" v-else>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-user-tie" v-if="!usersLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
@@ -170,13 +170,13 @@
</div>
<div class="field">
<label class="label" @click="showOptions = !showOptions">
<label class="label is-clickable" @click="showOptions = !showOptions">
<span class="icon-text">
<span class="icon">
<i v-if="showOptions" class="fas fa-arrow-up"></i>
<i v-else class="fas fa-arrow-down"></i>
</span>
<span>Optional options</span>
<span>Additional options...</span>
</span>
</label>
<div class="columns is-multiline is-mobile" v-if="showOptions && backend.options">
@@ -189,7 +189,7 @@
</div>
<div class="column is-1">
<button class="button is-danger" @click.prevent="removeOption(key)">
<span class="icon is-small">
<span class="icon">
<i class="fas fa-trash"></i>
</span>
</button>
@@ -201,7 +201,7 @@
<div class="field has-text-right">
<div class="control">
<button class="button is-primary" type="submit">
<span class="icon is-small">
<span class="icon">
<i class="fas fa-save"></i>
</span>
<span>Save Settings</span>

View File

@@ -11,10 +11,13 @@
</div>
</div>
</div>
<div class="column is-12">
This page is not yet implemented. It will be used to display the details of a specific backend.
</div>
</template>
<script setup>
const backend = useRoute().params.backend
useHead({title: `Backends: ${backend}`})
</script>

View File

@@ -16,7 +16,7 @@
</p>
<p class="control">
<button class="button is-primary" @click.prevent="loadContent">
<span class="icon is-small">
<span class="icon">
<i class="fas fa-sync"></i>
</span>
</button>
@@ -24,6 +24,9 @@
</div>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">This page contains all the backends that are currently configured.</span>
</div>
</div>
<div class="column is-12" v-if="toggleForm">
@@ -33,8 +36,8 @@
<div v-for="backend in backends" :key="backend.name" class="column is-6-tablet is-12-mobile">
<div class="card">
<header class="card-header">
<p class="card-header-title is-centered is-word-break">
<NuxtLink :href="'/backend/' + backend.name">
<p class="card-header-title">
<NuxtLink :href="`/backend/${backend.name}`">
{{ backend.name }}
</NuxtLink>
</p>
@@ -45,12 +48,12 @@
</span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-6-mobile" v-if="backend.export.enabled">
<div class="columns is-multiline has-text-centered">
<div class="column is-6 has-text-left-mobile" v-if="backend.export.enabled">
<strong>Last Export:</strong>
{{ backend.export.lastSync ? moment(backend.export.lastSync).fromNow() : 'None' }}
</div>
<div class="column is-hidden-mobile" v-if="backend.import.enabled">
<div class="column is-6 has-text-left-mobile" v-if="backend.import.enabled">
<strong>Last Import:</strong>
{{ backend.import.lastSync ? moment(backend.import.lastSync).fromNow() : 'None' }}
</div>
@@ -63,7 +66,7 @@
:checked="backend.export.enabled"
@change="updateValue(backend, 'export.enabled', !backend.export.enabled)">
<label :for="backend.name+'_export'">
Export {{ backend.export.enabled ? 'Enabled' : 'Disabled' }}
Export <span class="is-hidden-mobile">&nbsp;{{ backend.export.enabled ? 'Enabled' : 'Disabled' }}</span>
</label>
</div>
</div>
@@ -73,7 +76,7 @@
:checked="backend.import.enabled"
@change="updateValue(backend, 'import.enabled',!backend.import.enabled)">
<label :for="backend.name+'_import'">
Import {{ backend.import.enabled ? 'Enabled' : 'Disabled' }}
Import <span class="is-hidden-mobile">&nbsp;{{ backend.import.enabled ? 'Enabled' : 'Disabled' }}</span>
</label>
</div>
</div>
@@ -88,6 +91,7 @@ import 'assets/css/bulma-switch.css'
import moment from 'moment'
import request from '~/utils/request.js'
import BackendAdd from '~/components/BackendAdd.vue'
import {notification} from '~/utils/index.js'
useHead({title: 'Backends'})

View File

@@ -14,37 +14,51 @@
<div class="column is-12">
<form @submit.prevent="RunCommand">
<div class="field is-grouped">
<p class="control is-expanded has-icons-left">
<input type="text" class="input" v-model="command" placeholder="system:view" autocomplete="off" autofocus
:disabled="isLoading">
<span class="icon is-small is-left">
<i class="fas fa-terminal"></i>
</span>
</p>
<p class="control">
<button class="button is-primary" type="submit" :disabled="isLoading" :class="{'is-loading':isLoading}">
<span class="icon-text">
<span class="icon">
<i class="fa fa-server"></i>
<div class="field">
<div class="field-body">
<div class="field is-grouped-tablet">
<p class="control is-expanded has-icons-left">
<input type="text" class="input" v-model="command" placeholder="system:view" autocomplete="off"
autofocus
:disabled="isLoading">
<span class="icon is-left">
<i class="fas fa-terminal"></i>
</span>
<span>Run</span>
</span>
</button>
</p>
<p class="control">
<button class="button is-info" type="button" v-tooltip="'Clear output'" @click="response = []">
<span class="icon">
<i class="fa fa-broom"></i>
</span>
</button>
</p>
<p class="control" v-if="isLoading">
<button class="button is-danger" type="button" @click="finished" v-tooltip="'Close connection.'">
<span class="icon">
<i class="fa fa-power-off"></i>
</span>
</button>
</p>
<p class="control">
<button class="button is-primary" type="submit" :disabled="isLoading" :class="{'is-loading':isLoading}">
<span class="icon-text">
<span class="icon">
<i class="fa fa-server"></i>
</span>
<span>Run</span>
</span>
</button>
</p>
<p class="control">
<button class="button is-info" type="button" v-tooltip="'Clear output'" @click="response = []">
<span class="icon-text">
<span class="icon"><i class="fa fa-broom"></i></span>
<span>Clear</span>
</span>
</button>
</p>
<p class="control" v-if="isLoading">
<button class="button is-danger" type="button" @click="finished" v-tooltip="'Close connection.'">
<span class="icon-text">
<span class="icon"><i class="fa fa-power-off"></i></span>
<span>Close Connection</span>
</span>
</button>
</p>
</div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fa fa-info"></i></span>
<span>Please beware, clicking close connection does not stop the command. It only stops the output from
being displayed. The command will continue to run until it finishes.</span>
</span>
</p>
</div>
</form>

View File

@@ -30,7 +30,7 @@
<div class="column is-12" v-if="toggleForm">
<form id="env_add_form" @submit.prevent="addVariable">
<div class="field is-grouped">
<div class="field is-grouped-tablet">
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="form_key" id="form_key" @change="keyChanged">
@@ -40,7 +40,7 @@
</option>
</select>
</div>
<div class="icon is-small is-left">
<div class="icon is-left">
<i class="fas fa-key"></i>
</div>
</div>

View File

@@ -20,17 +20,20 @@
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">This page has the latest history entries. Sorted by the most recent event.</span>
</div>
</div>
<div class="column is-12" v-if="total && last_page > 1">
<div class="field is-grouped">
<div class="control">
<button rel="first" class="button" v-if="page !== 1" @click="loadContent(1)">
<div class="control" v-if="page !== 1">
<button rel="first" class="button" @click="loadContent(1)">
<span><<</span>
</button>
</div>
<div class="control">
<button rel="prev" class="button" v-if="page > 1 && (page-1) !== 1" @click="loadContent(page-1)">
<div class="control" v-if="page > 1 && (page-1) !== 1">
<button rel="prev" class="button" @click="loadContent(page-1)">
<span><</span>
</button>
</div>
@@ -43,14 +46,13 @@
</select>
</div>
</div>
<div class="control">
<button rel="next" class="button" v-if="page !== last_page && (page+1) !== last_page"
@click="loadContent(page+1)">
<div class="control" v-if="page !== last_page && (page+1) !== last_page">
<button rel="next" class="button" @click="loadContent(page+1)">
<span>></span>
</button>
</div>
<div class="control">
<button rel="last" class="button" v-if="page !== last_page" @click="loadContent(last_page)">
<div class="control" v-if="page !== last_page">
<button rel="last" class="button" @click="loadContent(last_page)">
<span>>></span>
</button>
</div>
@@ -59,45 +61,53 @@
<div class="column is-12" v-if="searchForm">
<form @submit.prevent="loadContent(1)">
<div class="field has-addons">
<div class="control has-icons-left">
<div class="select">
<select v-model="searchField" class="is-capitalized">
<option value="">Select Field</option>
<option v-for="field in searchable" :key="field" :value="field">
{{ field }}
</option>
</select>
</div>
<div class="icon is-small is-left">
<i class="fas fa-folder-tree"></i>
<div class="field">
<div class="field-body">
<div class="field is-grouped-tablet">
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="searchField" class="is-capitalized" :disabled="isLoading">
<option value="">Select Field</option>
<option v-for="field in searchable" :key="'search-' + field.key" :value="field.key">
{{ field.key }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-folder-tree"></i>
</div>
</div>
<div class="control is-expanded has-icons-left">
<input class="input" type="search" placeholder="Search..." v-model="query"
:disabled="'' === searchField || isLoading">
<div class="icon is-left">
<i class="fas fa-search"></i>
</div>
</div>
<div class="control">
<button class="button is-warning" type="button" @click="clearSearch" :disabled="isLoading">
<span class="icon-text">
<span class="icon"><i class="fas fa-cancel"></i></span>
<span>Reset</span>
</span>
</button>
</div>
<div class="control">
<button class="button is-primary" type="submit" :disabled="!query || '' === searchField || isLoading"
:class="{'is-loading':isLoading}">
<span class="icon-text">
<span class="icon"><i class="fas fa-search"></i></span>
<span>Search</span>
</span>
</button>
</div>
</div>
</div>
<div class="control is-expanded has-icons-left">
<input class="input" type="search" placeholder="Search..." v-model="query" :disabled="'' === searchField">
<div class="icon is-small is-left">
<i class="fas fa-search"></i>
</div>
<p class="help" v-if="[ 'metadata', 'extra' ].includes(searchField)">
<span class="icon has-text-danger"><i class="fas fa-exclamation"></i></span>
<span>Searching using <code>metadata</code> or <code>extra</code> fields is not currently supported via
WebUI.</span>
</p>
</div>
<div class="control">
<button class="button is-primary" type="submit" :disabled="!query || '' === searchField">
<span class="icon">
<i class="fas fa-search"></i>
</span>
</button>
</div>
<div class="control">
<button class="button is-danger" type="button" v-tooltip="'Reset search'" @click="clearSearch">
<span class="icon">
<i class="fas fa-cancel"></i>
</span>
</button>
</div>
<p class="help" v-html="getHelp(searchField)"></p>
</div>
</form>
</div>
@@ -172,73 +182,86 @@
import request from '~/utils/request.js'
import moment from 'moment'
import Message from '~/components/Message.vue'
import {notification} from '~/utils/index.js'
const route = useRoute()
useHead({title: 'History'})
const items = ref([]);
const searchable = ref(['id', 'via', 'year', 'type', 'title', 'season', 'episode', 'parent', 'guid']);
const jsonFields = ref(['metadata', 'extra'])
const items = ref([])
const searchable = ref([{key: 'id'}, {key: 'via'}, {key: 'year'}, {key: 'type'}, {key: 'title'}, {key: 'season'}, {key: 'episode'}, {key: 'parent'}, {key: 'guid'}])
const error = ref('')
const page = ref(route.query.page ?? 1);
const perpage = ref(route.query.perpage ?? 50);
const total = ref(0);
const last_page = computed(() => Math.ceil(total.value / perpage.value));
const page = ref(route.query.page ?? 1)
const perpage = ref(route.query.perpage ?? 50)
const total = ref(0)
const last_page = computed(() => Math.ceil(total.value / perpage.value))
const query = ref(route.query.q ?? '');
const searchField = ref(route.query.key ?? '');
const isLoading = ref(false);
const searchForm = ref(false);
const query = ref(route.query.q ?? '')
const searchField = ref(route.query.key ?? '')
const isLoading = ref(false)
const searchForm = ref(false)
const loadContent = async (pageNumber, fromPopState = false) => {
pageNumber = parseInt(pageNumber);
pageNumber = parseInt(pageNumber)
if (isNaN(pageNumber) || pageNumber < 1) {
pageNumber = 1;
pageNumber = 1
}
let title = `Links: Page #${pageNumber}`;
let title = `Links: Page #${pageNumber}`
let search = new URLSearchParams();
search.set('perpage', perpage.value);
search.set('page', pageNumber);
let search = new URLSearchParams()
search.set('perpage', perpage.value)
search.set('page', pageNumber)
if (searchField.value && query.value) {
search.set('q', query.value);
search.set('key', searchField.value);
title += `. (Search: ${query.value})`;
search.set('q', query.value)
search.set('key', searchField.value)
title += `. (Search: ${query.value})`
}
useHead({title})
let newUrl = window.location.pathname + '?' + search.toString();
isLoading.value = true;
items.value = [];
let newUrl = window.location.pathname + '?' + search.toString()
try {
if (searchField.value && query.value) {
search.delete('q');
search.delete('key');
search.set(searchField.value, query.value)
search.delete('q')
search.delete('key')
if (jsonFields.value.includes(searchField.value)) {
search.set(searchField.value, `1`)
let [field, value] = splitQuery(query.value, '://')
if (-1 === query.value.indexOf('://') || !value || !field) {
notification('error', 'Error', `Invalid search format for '${searchField.value}'.`)
return
}
search.set('key', field)
search.set('value', value)
} else {
search.set(searchField.value, query.value)
}
}
isLoading.value = true
items.value = []
const response = await request(`/history/?${search.toString()}`)
const json = await response.json();
const json = await response.json()
if (!fromPopState && window.location.href !== newUrl) {
window.history.pushState({
page: pageNumber,
query: query.value,
key: searchField.value
}, '', newUrl);
}, '', newUrl)
}
if ('paging' in json) {
page.value = json.paging.current_page;
perpage.value = json.paging.perpage;
total.value = json.paging.total;
page.value = json.paging.current_page
perpage.value = json.paging.perpage
total.value = json.paging.total
}
if (json.history) {
@@ -253,18 +276,18 @@ const loadContent = async (pageNumber, fromPopState = false) => {
error.value = json.error
}
isLoading.value = false;
isLoading.value = false
} catch (e) {
}
};
}
const makePagination = () => {
let pagination = [];
let pages = Math.ceil(total.value / perpage.value);
let pagination = []
let pages = Math.ceil(total.value / perpage.value)
if (pages < 2) {
return pagination;
return pagination
}
for (let i = 1; i <= pages; i++) {
@@ -272,18 +295,45 @@ const makePagination = () => {
page: i,
text: `Page #${i}`,
selected: parseInt(page.value) === i,
});
})
}
return pagination;
return pagination
}
const clearSearch = () => {
query.value = '';
searchField.value = '';
searchForm.value = false;
loadContent(1);
query.value = ''
searchField.value = ''
searchForm.value = false
loadContent(1)
}
const splitQuery = (str, delimiter) => {
const index = str.indexOf(delimiter)
return (-1 === index) ? [str] : [str.slice(0, index), str.slice(index + delimiter.length)]
}
const getHelp = (key) => {
if (!key) {
return ''
}
let data = searchable.value.filter(i => i.key === key)
if (data.length === 0) {
return ''
}
if (!data[0].description) {
return '';
}
let text = `${data[0].description}`;
if (data[0].type) {
text += ` Expected value: <code>${typeof data[0].type === 'object' ? data[0].type.join(' or ') : data[0].type}</code>`
}
return `<span class="icon-text"><span class="icon"><i class="fas fa-info"></i></span><span>${text}</span></span>`
}
onMounted(async () => loadContent(page.value ?? 1))
</script>

View File

@@ -3,7 +3,7 @@
<div class="column is-12">
<h1 class="title is-4">
<NuxtLink href="/history">Recent History</NuxtLink>
<NuxtLink href="/history">Latest History Entries</NuxtLink>
</h1>
</div>
@@ -58,7 +58,7 @@
<div class="column is-12" v-for="log in logs" :key="log.filename">
<h1 class="title is-4">
<NuxtLink :href="`/logs/${log.filename}`">Today {{ ucFirst(log.type) }} log</NuxtLink>
<NuxtLink :href="`/logs/${log.filename}`">Latest {{ log.type }} logs</NuxtLink>
</h1>
<code class="box logs-container">
<span class="is-block" v-for="(item, index) in log.lines" :key="log.filename + '-' + index">
@@ -82,7 +82,6 @@
import request from '~/utils/request.js'
import moment from 'moment'
import Message from '~/components/Message.vue'
import {ucFirst} from '../utils/index.js'
useHead({title: 'Index'})

View File

@@ -13,6 +13,11 @@
</div>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
This page contains all the stored log files. The naming convention is <code>type.YYYYMMDD.log</code>.
</span>
</div>
</div>
<div class="column is-4-tablet" v-for="(item, index) in logs" :key="'log-'+index">

View File

@@ -17,16 +17,31 @@
</div>
<div class="column is-12">
<Message message_class="is-warning">
<Message message_class="is-warning" title="Warning" v-if="show_report_warning">
<p>Beware, while we try to make sure no sensitive information is leaked via the report, it's possible that
something might be missed. Please review the report before posting it. If you notice
any sensitive information, please report it to the developers. so we can fix it.</p>
<div class="mt-4">
<button class="button is-block is-fullwidth is-primary" @click="show_report_warning = false">
<span class="icon-text">
<span class="icon"><i class="fas fa-exclamation"></i></span>
<span>I Understand, show report.</span>
</span>
</button>
</div>
</Message>
</div>
<div class="column is-12" v-if="data.length>0">
<pre style="min-height: 60vh;max-height:80vh; overflow-y: scroll"
><code><span v-for="(item, index) in data" :key="index" class="is-block">{{ item }}</span></code></pre>
<Message message_class="is-info" v-if="!show_report_warning && data.length<1">
<span class="icon"><i class="fas fa-spinner fa-pulse"></i></span>
<span>Generating the report. Please wait...</span>
</Message>
<template v-if="!show_report_warning && data.length>0">
<pre style="min-height: 60vh;max-height:80vh; overflow-y: scroll"
><code><span v-for="(item, index) in data" :key="index" class="is-block">{{ item }}</span></code></pre>
</template>
</div>
</div>
</template>
@@ -35,12 +50,17 @@
useHead({title: `System Report`})
const data = ref([])
const show_report_warning = ref(true)
onMounted(async () => {
const response = await request(`/system/report`);
watch(show_report_warning, async (value) => {
if (false !== value) {
return
}
const response = await request(`/system/report`)
data.value = await response.json()
});
})
const copyAPI = navigator.clipboard
const copyContent = () => navigator.clipboard.writeText(data.value.join('\n'));
</script>

View File

@@ -8,16 +8,19 @@
<div class="field is-grouped">
<p class="control">
<button class="button is-primary" @click.prevent="loadContent(true)">
<span class="icon is-small">
<i class="fas fa-sync"></i>
</span>
<span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</div>
</div>
</div>
<div class="subtitle is-hidden-mobile" v-if="queued.length > 0">
<p>The following tasks <code>{{ queued.join(', ') }}</code> are queued to be run in background soon.</p>
<div class="is-hidden-mobile">
<span class="subtitle">
This page contains all the tasks that are currently configured.
<template v-if="queued.length > 0">
<p>The following tasks <code>{{ queued.join(', ') }}</code> are queued to be run in background soon.</p>
</template>
</span>
</div>
</div>
@@ -87,7 +90,8 @@
<button class="button is-warning" @click="confirmRun(task)">
<span class="icon-text">
<span class="icon"><i class="fas fa-terminal"></i></span>
<span>Run via console</span>
<span class="is-hidden-mobile">Run via console</span>
<span class="is-hidden-tablet">Run now</span>
</span>
</button>
</div>

View File

@@ -286,17 +286,64 @@ final class Index
'last_url' => $lastUrl,
],
'searchable' => [
'id',
'via',
'year',
'type',
'title',
'season',
'episode',
'parent',
'guid',
'metadata',
'extra',
[
'key' => 'id',
'description' => 'Search using local history id.',
'type' => 'int',
],
[
'key' => 'via',
'description' => 'Search using the backend name.',
'type' => 'string',
],
[
'key' => 'year',
'description' => 'Search using the year.',
'type' => 'int',
],
[
'key' => 'type',
'description' => 'Search using the content type.',
'type' => [
'movie',
'episode',
],
],
[
'key' => 'title',
'description' => 'Search using the title.',
'type' => 'string',
],
[
'key' => 'season',
'description' => 'Search using the season number.',
'type' => 'int',
],
[
'key' => 'episode',
'description' => 'Search using the episode number.',
'type' => 'int',
],
[
'key' => 'parent',
'description' => 'Search using the parent GUID.',
'type' => 'guid://id',
],
[
'key' => 'guid',
'description' => 'Search using the GUID.',
'type' => 'guid://id',
],
[
'key' => 'metadata',
'description' => 'Search using the metadata JSON field. Searching this field might be slow.',
'type' => 'backend.field://value',
],
[
'key' => 'extra',
'description' => 'Search using the extra JSON field. Searching this field might be slow.',
'type' => 'backend.field://value',
],
],
];