Added simple page transition.

This commit is contained in:
Abdulmhsen B. A. A.
2024-07-22 21:31:46 +03:00
parent 109d133837
commit 2fb9435367
28 changed files with 3807 additions and 3735 deletions

View File

@@ -180,3 +180,11 @@ body {
.is-text-contents { .is-text-contents {
display: contents; display: contents;
} }
.page-enter-active, .page-leave-active {
transition: opacity 0.2s ease;
}
.page-enter-from, .page-leave-to {
opacity: 0;
}

View File

@@ -286,13 +286,11 @@
</div> </div>
</div> </div>
<template v-if="!hasAPISettings"> <div>
<no-api/> <NuxtPage v-if="!showConnection && hasAPISettings"/>
</template> <no-api v-else/>
<template v-else> </div>
<slot v-if="!showConnection && hasAPISettings"/>
</template>
<div class="columns is-multiline is-mobile mt-3"> <div class="columns is-multiline is-mobile mt-3">
<div class="column is-12 is-hidden-tablet has-text-centered"> <div class="column is-12 is-hidden-tablet has-text-centered">
<a href="#top" id="bottom" class="button"> <a href="#top" id="bottom" class="button">

View File

@@ -18,7 +18,7 @@ export default defineNuxtConfig({
], ],
}, },
buildAssetsDir: "assets", buildAssetsDir: "assets",
pageTransition: {name: 'page', mode: 'out-in'}
}, },
router: { router: {
options: { options: {

View File

@@ -1,66 +1,69 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span> <span class="title is-4">
<NuxtLink to="/backends" v-text="'Backends'"/> <span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
- <NuxtLink to="/backends" v-text="'Backends'"/>
<NuxtLink :to="'/backend/' + id" v-text="id"/> -
: Delete <NuxtLink :to="'/backend/' + id" v-text="id"/>
</span> : Delete
</span>
<div class="is-pulled-right"> <div class="is-pulled-right">
<div class="field is-grouped"></div> <div class="field is-grouped"></div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">Delete backend configuration and records.</span>
</div>
</div> </div>
<div class="is-hidden-mobile"> <template v-if="isDeleting">
<span class="subtitle">Delete backend configuration and records.</span> <div class="column is-12">
</div> <Message message_class="has-background-warning-90 has-text-dark" title="Deleting..."
</div> icon="fas fa-spin fa-exclamation-triangle"
message="Delete operation is in progress. Please wait..."/>
</div>
</template>
<template v-else-if="isLoading">
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
</div>
</template>
<template v-else>
<div class="column is-12" v-if="error">
<Message message_class="is-background-warning-80 has-text-dark" title="Error" icon="fas fa-exclamation-circle"
:use-close="true" @close="navigateTo('/backends')"
:message="`${error.error.code}: ${error.error.message}`"/>
</div>
<div class="column is-12" v-else>
<Message message_class="is-background-warning-80 has-text-dark" title="Confirmation is required"
icon="fas fa-exclamation-triangle">
<p>Are you sure you want to delete the backend <code>{{ type }}: {{ id }}</code> configuration and all its
records?</p>
<template v-if="isDeleting"> <h5 class="has-text-dark">This operation will do the following</h5>
<div class="column is-12">
<Message message_class="has-background-warning-90 has-text-dark" title="Deleting..."
icon="fas fa-spin fa-exclamation-triangle" message="Delete operation is in progress. Please wait..."/>
</div>
</template>
<template v-else-if="isLoading">
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
</div>
</template>
<template v-else>
<div class="column is-12" v-if="error">
<Message message_class="is-background-warning-80 has-text-dark" title="Error" icon="fas fa-exclamation-circle"
:use-close="true" @close="navigateTo('/backends')"
:message="`${error.error.code}: ${error.error.message}`"/>
</div>
<div class="column is-12" v-else>
<Message message_class="is-background-warning-80 has-text-dark" title="Confirmation is required"
icon="fas fa-exclamation-triangle">
<p>Are you sure you want to delete the backend <code>{{ type }}: {{ id }}</code> configuration and all its
records?</p>
<h5 class="has-text-dark">This operation will do the following</h5> <ul>
<li>Remove records metadata that references the given backend.</li>
<li>Run data integrity check to remove no longer used records.</li>
<li>Update <code>servers.yaml</code> file and remove backend configuration.</li>
</ul>
<ul> <p>There is no undo operation. This action is irreversible.</p>
<li>Remove records metadata that references the given backend.</li> </Message>
<li>Run data integrity check to remove no longer used records.</li>
<li>Update <code>servers.yaml</code> file and remove backend configuration.</li>
</ul>
<p>There is no undo operation. This action is irreversible.</p> <Confirm @confirmed="deleteBackend()"
</Message> title="Delete backend"
title-icon="fa-trash"
<Confirm @confirmed="deleteBackend()" warning="Depending on your hardware speed, the delete operation might take long time. do not interrupt the
title="Delete backend"
title-icon="fa-trash"
warning="Depending on your hardware speed, the delete operation might take long time. do not interrupt the
process, or close the browser tab. You will be redirected to the backends page automatically once the process, or close the browser tab. You will be redirected to the backends page automatically once the
process is complete. Otherwise, you might end up with a corrupted database."/> process is complete. Otherwise, you might end up with a corrupted database."/>
</div> </div>
</template> </template>
</div>
</div> </div>
</template> </template>

View File

@@ -1,350 +1,354 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span> <span class="title is-4">
<NuxtLink to="/backends" v-text="'Backends'"/> <span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
- <NuxtLink to="/backends" v-text="'Backends'"/>
<NuxtLink :to="'/backend/' + id" v-text="id"/> -
: Edit <NuxtLink :to="'/backend/' + id" v-text="id"/>
</span> : Edit
</span>
<div class="is-pulled-right"> <div class="is-pulled-right">
<div class="field is-grouped"></div> <div class="field is-grouped"></div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">Edit the backend settings.</span>
</div>
</div> </div>
<div class="is-hidden-mobile"> <div class="column is-12" v-if="isLimitedToken">
<span class="subtitle">Edit the backend settings.</span> <Message title="For your information" message_class="has-background-warning-90 has-text-dark"
icon="fas fa-info-circle">
<p>
This backend is using accesstoken instead of API keys, And this method untested and may not work as
expected.
Please make sure you know what you are doing. Simple operations like <code>Import</code>,
<code>Export</code>
should work fine.
</p>
<p>
How the access token interact with the rest of the API is undefined and untested by us. Please use with
caution. If you notice any issue, please report it to us.
</p>
</Message>
</div> </div>
</div>
<div class="column is-12" v-if="isLimitedToken"> <div class="column is-12" v-if="isLoading">
<Message title="For your information" message_class="has-background-warning-90 has-text-dark" <Message message_class="is-background-info-90 has-text-dark" title="Loading"
icon="fas fa-info-circle"> icon="fas fa-spinner fa-spin" message="Loading backend settings. Please wait..."/>
<p> </div>
This backend is using accesstoken instead of API keys, And this method untested and may not work as expected.
Please make sure you know what you are doing. Simple operations like <code>Import</code>, <code>Export</code>
should work fine.
</p>
<p>
How the access token interact with the rest of the API is undefined and untested by us. Please use with
caution. If you notice any issue, please report it to us.
</p>
</Message>
</div>
<div class="column is-12" v-if="isLoading"> <div v-else class="column is-12">
<Message message_class="is-background-info-90 has-text-dark" title="Loading" <form id="backend_edit_form" @submit.prevent="saveContent">
icon="fas fa-spinner fa-spin" message="Loading backend settings. Please wait..."/> <div class="card">
</div> <header class="card-header">
<p class="card-header-title is-justify-center">Edit Backend - {{ backend.name }}</p>
</header>
<div v-else class="column is-12"> <div class="card-content">
<form id="backend_edit_form" @submit.prevent="saveContent"> <div class="field">
<div class="card"> <label class="label">Name</label>
<header class="card-header"> <div class="control has-icons-left">
<p class="card-header-title is-justify-center">Edit Backend - {{ backend.name }}</p> <input class="input" type="text" v-model="backend.name" required readonly disabled>
</header> <div class="icon is-left">
<i class="fas fa-id-badge"></i>
<div class="card-content">
<div class="field">
<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-left">
<i class="fas fa-id-badge"></i>
</div>
<p class="help">
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
case a-z, 0-9 and _</code> only.
</p>
</div>
</div>
<div class="field">
<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-left">
<i class="fas fa-server"></i>
</div>
</div>
</div>
<div class="field">
<label class="label">URL</label>
<div class="control has-icons-left">
<div class="select is-fullwidth" v-if="servers.length > 0">
<select v-model="backend.url" class="is-capital" @change="updateIdentifier" required>
<option value="" disabled>Select Server</option>
<option v-for="server in servers" :key="server.uuid" :value="server.uri">
{{ server.name }} - {{ server.uri }}
</option>
</select>
</div>
<input class="input" type="text" v-model="backend.url" v-else required>
<div class="icon is-left">
<i class="fas fa-link"></i>
</div>
<p class="help">
<template v-if="servers.length<1">
Enter the URL of the backend. For example
<code v-if="'plex' === backend.type">http://192.168.8.11:32400</code>
<code v-else>http://192.168.8.100:8096</code>
.
</template>
<template v-else>
Those are the servers associated with the Plex Token. Select the server you want to use.
</template>
</p>
</div>
</div>
<div class="field">
<label class="label">
<template v-if="'plex' !== backend.type">API Key</template>
<template v-else>X-Plex-Token</template>
</label>
<div class="field-body">
<div class="field">
<div class="field has-addons">
<div class="control is-expanded has-icons-left">
<input class="input" v-model="backend.token" required
:type="false === exposeToken ? 'password' : 'text'">
<div class="icon is-left">
<i class="fas fa-key"></i>
</div>
</div>
<div class="control">
<button type="button" class="button is-primary" @click="exposeToken = !exposeToken"
v-tooltip="'Toggle token'">
<span class="icon" v-if="!exposeToken"><i class="fas fa-eye"></i></span>
<span class="icon" v-else><i class="fas fa-eye-slash"></i></span>
</button>
</div>
</div> </div>
<p class="help"> <p class="help">
<template v-if="'plex'===backend.type"> Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
Enter the <code>X-Plex-Token</code>. case a-z, 0-9 and _</code> only.
<NuxtLink target="_blank" </p>
to="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/" </div>
v-text="'Visit This article for more information.'"/> </div>
<div class="field">
<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-left">
<i class="fas fa-server"></i>
</div>
</div>
</div>
<div class="field">
<label class="label">URL</label>
<div class="control has-icons-left">
<div class="select is-fullwidth" v-if="servers.length > 0">
<select v-model="backend.url" class="is-capital" @change="updateIdentifier" required>
<option value="" disabled>Select Server</option>
<option v-for="server in servers" :key="server.uuid" :value="server.uri">
{{ server.name }} - {{ server.uri }}
</option>
</select>
</div>
<input class="input" type="text" v-model="backend.url" v-else required>
<div class="icon is-left">
<i class="fas fa-link"></i>
</div>
<p class="help">
<template v-if="servers.length<1">
Enter the URL of the backend. For example
<code v-if="'plex' === backend.type">http://192.168.8.11:32400</code>
<code v-else>http://192.168.8.100:8096</code>
.
</template> </template>
<template v-else> <template v-else>
You can generate a new API key from <code>Dashboard > Settings > API Keys</code>. Those are the servers associated with the Plex Token. Select the server you want to use.
</template> </template>
</p> </p>
</div> </div>
</div> </div>
</div>
<div class="field"> <div class="field">
<label class="label">Backend Unique ID</label> <label class="label">
<div class="control has-icons-left"> <template v-if="'plex' !== backend.type">API Key</template>
<input class="input" type="text" v-model="backend.uuid" required :disabled="isLimitedToken"> <template v-else>X-Plex-Token</template>
<div class="icon is-left"> </label>
<i class="fas fa-cloud" v-if="!uuidLoading"></i> <div class="field-body">
<i class="fas fa-spinner fa-pulse" v-else></i> <div class="field">
<div class="field has-addons">
<div class="control is-expanded has-icons-left">
<input class="input" v-model="backend.token" required
:type="false === exposeToken ? 'password' : 'text'">
<div class="icon is-left">
<i class="fas fa-key"></i>
</div>
</div>
<div class="control">
<button type="button" class="button is-primary" @click="exposeToken = !exposeToken"
v-tooltip="'Toggle token'">
<span class="icon" v-if="!exposeToken"><i class="fas fa-eye"></i></span>
<span class="icon" v-else><i class="fas fa-eye-slash"></i></span>
</button>
</div>
</div>
<p class="help">
<template v-if="'plex'===backend.type">
Enter the <code>X-Plex-Token</code>.
<NuxtLink target="_blank"
to="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/"
v-text="'Visit This article for more information.'"/>
</template>
<template v-else>
You can generate a new API key from <code>Dashboard > Settings > API Keys</code>.
</template>
</p>
</div>
</div> </div>
<p class="help">
<span v-if="'plex' === backend.type">
The backend unique ID is random string generated on server setup, In Plex case it used to inquiry
about the users associated with the server to generate limited <code>X-Plex-Token</code> for them.
It
used by webhooks as a filter to match the backend. in-case you are member of multiple servers.
</span>
<span v-else>
The backend unique ID is a random string generated on server setup. It is used to identify the
backend
uniquely. This is used for webhook matching and filtering.
</span>
<NuxtLink @click="getUUid" v-if="!isLimitedToken" v-text="'Get from the backend.'"/>
</p>
</div> </div>
</div>
<div class="field"> <div class="field">
<label class="label"> <label class="label">Backend Unique ID</label>
<template v-if="users.length>0">Associated User</template> <div class="control has-icons-left">
<template v-else>User ID</template> <input class="input" type="text" v-model="backend.uuid" required :disabled="isLimitedToken">
</label> <div class="icon is-left">
<div class="control has-icons-left"> <i class="fas fa-cloud" v-if="!uuidLoading"></i>
<div class="select is-fullwidth" v-if="users.length>0"> <i class="fas fa-spinner fa-pulse" v-else></i>
<select v-model="backend.user" class="is-capitalized" :disabled="isLimitedToken"> </div>
<option v-for="user in users" :key="'uid-'+user.id" :value="user.id"> <p class="help">
{{ user.name }} <span v-if="'plex' === backend.type">
</option> The backend unique ID is random string generated on server setup, In Plex case it used to inquiry
</select> about the users associated with the server to generate limited <code>X-Plex-Token</code> for them.
It
used by webhooks as a filter to match the backend. in-case you are member of multiple servers.
</span>
<span v-else>
The backend unique ID is a random string generated on server setup. It is used to identify the
backend
uniquely. This is used for webhook matching and filtering.
</span>
<NuxtLink @click="getUUid" v-if="!isLimitedToken" v-text="'Get from the backend.'"/>
</p>
</div> </div>
<input class="input" type="text" v-model="backend.user" v-else> </div>
<div class="icon is-left">
<i class="fas fa-user-tie" v-if="!usersLoading"></i> <div class="field">
<i class="fas fa-spinner fa-pulse" v-else></i> <label class="label">
<template v-if="users.length>0">Associated User</template>
<template v-else>User ID</template>
</label>
<div class="control has-icons-left">
<div class="select is-fullwidth" v-if="users.length>0">
<select v-model="backend.user" class="is-capitalized" :disabled="isLimitedToken">
<option v-for="user in users" :key="'uid-'+user.id" :value="user.id">
{{ user.name }}
</option>
</select>
</div>
<input class="input" type="text" v-model="backend.user" v-else>
<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>
<p class="help">
<span v-if="'plex' === backend.type">
Plex doesn't use standard API practice for identifying users. They use <code>X-Plex-Token</code>
to
identify the user. The user selected here will only be used for webhook matching and filtering.
</span>
<span v-else>
Which <code>{{ ucFirst(backend.type) }}</code> user should this backend use? The User ID will
determine the
data we get from the backend. And for webhook matching and filtering.
</span>
This tool is meant for single user use.
<a href="javascript:void(0)" @click="getUsers" v-if="!isLimitedToken">
Retrieve User ids from backend.
</a>
</p>
</div> </div>
<p class="help"> </div>
<span v-if="'plex' === backend.type">
Plex doesn't use standard API practice for identifying users. They use <code>X-Plex-Token</code> to <div class="field" v-if="backend.import">
identify the user. The user selected here will only be used for webhook matching and filtering. <label class="label" for="backend_import">Import data from this backend?</label>
<div class="control">
<input id="backend_import" type="checkbox" class="switch is-success" v-model="backend.import.enabled">
<label for="backend_import">Enable</label>
<p class="help">
Import means to get the data from the backend and store it in the database.
</p>
</div>
</div>
<div class="field" v-if="backend.import && !backend.import.enabled">
<label class="label" for="backend_import_metadata">Import metadata only from this backend?</label>
<div class="control">
<input id="backend_import_metadata" type="checkbox" class="switch is-success"
v-model="backend.options.IMPORT_METADATA_ONLY">
<label for="backend_import_metadata">Enable</label>
<p class="help has-text-danger">
To efficiently push changes to the backend we need relation map and this require
us to get metadata from the backend. You have Importing disabled, as such this option
allow us to import this backend metadata without altering your play state.
</p>
</div>
</div>
<div class="field" v-if="backend.export">
<label class="label" for="backend_export">Export data to this backend?</label>
<div class="control">
<input id="backend_export" type="checkbox" class="switch is-success" v-model="backend.export.enabled">
<label for="backend_export">Enable</label>
<p class="help">
Export means to send the data from the database to this backend.
</p>
</div>
</div>
<div class="field" v-if="backend.webhook">
<label class="label" for="webhook_match_user">Webhook match user</label>
<div class="control">
<input id="webhook_match_user" type="checkbox" class="switch is-success"
v-model="backend.webhook.match.user">
<label for="webhook_match_user">Enable</label>
<p class="help">
Check webhook payload for user id match. if it does not match, the payload will be ignored.
</p>
</div>
</div>
<div class="field" v-if="backend.webhook">
<label class="label" for="webhook_match_uuid">Webhook match backend id</label>
<div class="control">
<input id="webhook_match_uuid" type="checkbox" class="switch is-success"
v-model="backend.webhook.match.uuid">
<label for="webhook_match_uuid">Enable</label>
<p class="help">
Check webhook payload for backend unique id. if it does not match, the payload will be ignored.
</p>
</div>
</div>
<div class="field">
<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>Additional options...</span>
</span> </span>
<span v-else> </label>
Which <code>{{ ucFirst(backend.type) }}</code> user should this backend use? The User ID will <template v-if="showOptions">
determine the <div class="columns is-multiline is-mobile">
data we get from the backend. And for webhook matching and filtering. <template v-for="(val, key) in backend?.options" :key="'bo-'+key">
</span> <div class="column is-5">
This tool is meant for single user use. <input type="text" class="input" :value="key" readonly disabled>
<a href="javascript:void(0)" @click="getUsers" v-if="!isLimitedToken"> <p class="help is-unselectable">
Retrieve User ids from backend. <span class="icon has-text-info">
</a> <i class="fas fa-info-circle" :class="{'fa-bounce': newOptions[key]}"></i>
</p> </span>
</div> {{ optionsList.find(v => v.key === key)?.description }}
</div> </p>
</div>
<div class="field" v-if="backend.import"> <div class="column is-6">
<label class="label" for="backend_import">Import data from this backend?</label> <input type="text" class="input" v-model="backend.options[key]" required>
<div class="control"> </div>
<input id="backend_import" type="checkbox" class="switch is-success" v-model="backend.import.enabled"> <div class="column is-1">
<label for="backend_import">Enable</label> <button class="button is-danger" @click.prevent="removeOption(key)">
<p class="help"> <span class="icon">
Import means to get the data from the backend and store it in the database. <i class="fas fa-trash"></i>
</p> </span>
</div> </button>
</div> </div>
</template>
<div class="field" v-if="backend.import && !backend.import.enabled"> </div>
<label class="label" for="backend_import_metadata">Import metadata only from this backend?</label> <div class="columns is-multiline is-mobile">
<div class="control"> <div class="column is-12">
<input id="backend_import_metadata" type="checkbox" class="switch is-success" <span class="icon-text">
v-model="backend.options.IMPORT_METADATA_ONLY"> <span class="icon"><i class="fas fa-plus"></i></span>
<label for="backend_import_metadata">Enable</label> <span>Add new option</span>
<p class="help has-text-danger"> </span>
To efficiently push changes to the backend we need relation map and this require </div>
us to get metadata from the backend. You have Importing disabled, as such this option
allow us to import this backend metadata without altering your play state.
</p>
</div>
</div>
<div class="field" v-if="backend.export">
<label class="label" for="backend_export">Export data to this backend?</label>
<div class="control">
<input id="backend_export" type="checkbox" class="switch is-success" v-model="backend.export.enabled">
<label for="backend_export">Enable</label>
<p class="help">
Export means to send the data from the database to this backend.
</p>
</div>
</div>
<div class="field" v-if="backend.webhook">
<label class="label" for="webhook_match_user">Webhook match user</label>
<div class="control">
<input id="webhook_match_user" type="checkbox" class="switch is-success"
v-model="backend.webhook.match.user">
<label for="webhook_match_user">Enable</label>
<p class="help">
Check webhook payload for user id match. if it does not match, the payload will be ignored.
</p>
</div>
</div>
<div class="field" v-if="backend.webhook">
<label class="label" for="webhook_match_uuid">Webhook match backend id</label>
<div class="control">
<input id="webhook_match_uuid" type="checkbox" class="switch is-success"
v-model="backend.webhook.match.uuid">
<label for="webhook_match_uuid">Enable</label>
<p class="help">
Check webhook payload for backend unique id. if it does not match, the payload will be ignored.
</p>
</div>
</div>
<div class="field">
<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>Additional options...</span>
</span>
</label>
<template v-if="showOptions">
<div class="columns is-multiline is-mobile">
<template v-for="(val, key) in backend?.options" :key="'bo-'+key">
<div class="column is-5"> <div class="column is-5">
<input type="text" class="input" :value="key" readonly disabled> <div class="select is-fullwidth">
<p class="help is-unselectable"> <select v-model="selectedOption">
<span class="icon has-text-info"> <option value="">Select Option</option>
<i class="fas fa-info-circle" :class="{'fa-bounce': newOptions[key]}"></i> <option v-for="option in filteredOptions(optionsList)"
</span> :key="'opt-'+option.key" :value="option.key">
{{ optionsList.find(v => v.key === key)?.description }} {{ option.key }}
</p> </option>
</select>
</div>
</div> </div>
<div class="column is-6"> <div class="column is-6">
<input type="text" class="input" v-model="backend.options[key]" required> {{ selectedOptionHelp }}
</div> </div>
<div class="column is-1"> <div class="column is-1">
<button class="button is-danger" @click.prevent="removeOption(key)"> <button class="button is-primary" @click.prevent="addOption">
<span class="icon"> <span class="icon">
<i class="fas fa-trash"></i> <i class="fas fa-add"></i>
</span> </span>
</button> </button>
</div> </div>
</template>
</div>
<div class="columns is-multiline is-mobile">
<div class="column is-12">
<span class="icon-text">
<span class="icon"><i class="fas fa-plus"></i></span>
<span>Add new option</span>
</span>
</div> </div>
<div class="column is-5"> </template>
<div class="select is-fullwidth"> </div>
<select v-model="selectedOption"> </div>
<option value="">Select Option</option> <div class="card-footer">
<option v-for="option in filteredOptions(optionsList)" <button class="button card-footer-item is-fullwidth is-primary" type="submit">
:key="'opt-'+option.key" :value="option.key"> <span class="icon"><i class="fas fa-save"></i></span>
{{ option.key }} <span>Save Settings</span>
</option> </button>
</select> <NuxtLink class="card-footer-item button is-fullwidth is-danger" :to="`/backend/${id}`">
</div> <span class="icon"><i class="fas fa-cancel"></i></span>
</div> <span>Cancel changes</span>
<div class="column is-6"> </NuxtLink>
{{ selectedOptionHelp }}
</div>
<div class="column is-1">
<button class="button is-primary" @click.prevent="addOption">
<span class="icon">
<i class="fas fa-add"></i>
</span>
</button>
</div>
</div>
</template>
</div> </div>
</div> </div>
<div class="card-footer"> </form>
<button class="button card-footer-item is-fullwidth is-primary" type="submit"> </div>
<span class="icon"><i class="fas fa-save"></i></span>
<span>Save Settings</span>
</button>
<NuxtLink class="card-footer-item button is-fullwidth is-danger" :to="`/backend/${id}`">
<span class="icon"><i class="fas fa-cancel"></i></span>
<span>Cancel changes</span>
</NuxtLink>
</div>
</div>
</form>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import 'assets/css/bulma-switch.css' import 'assets/css/bulma-switch.css'
import {notification, ucFirst} from '~/utils/index.js' import {notification, ucFirst} from '~/utils/index'
import {ref} from "vue"; import Message from '~/components/Message'
import Message from "~/components/Message.vue";
const id = useRoute().params.backend const id = useRoute().params.backend
const redirect = useRoute().query?.redirect ?? `/backend/${id}` const redirect = useRoute().query?.redirect ?? `/backend/${id}`

View File

@@ -1,163 +1,167 @@
<template> <template>
<div class="columns is-multiline" v-if="isLoading || isError"> <div>
<div class="column is-12"> <div class="columns is-multiline" v-if="isLoading || isError">
<Message message_class="is-background-info-90 has-text-dark" title="Loading" v-if="isLoading" <div class="column is-12">
icon="fas fa-spinner fa-spin" message="Loading backend settings. Please wait..."/> <Message message_class="is-background-info-90 has-text-dark" title="Loading" v-if="isLoading"
icon="fas fa-spinner fa-spin" message="Loading backend settings. Please wait..."/>
<Message message_class="has-background-warning-80 has-text-dark" icon="fas fa-exclamation-triangle" <Message message_class="has-background-warning-80 has-text-dark" icon="fas fa-exclamation-triangle"
title="Warning" v-if="!isLoading && isError"> title="Warning" v-if="!isLoading && isError">
<p> <p>
<span class="icon"><i class="fas fa-exclamation"></i></span> <span class="icon"><i class="fas fa-exclamation"></i></span>
There was error loading your backend data. Please try again later. There was error loading your backend data. Please try again later.
</p> </p>
<div v-if="error"> <div v-if="error">
<pre><code>{{ error }}</code></pre> <pre><code>{{ error }}</code></pre>
</div>
</Message>
</div>
</div>
<template v-if="!isLoading && !isError">
<div class="columns is-multiline">
<div class="column is-12 is-clearfix is-unselectable">
<span class="title is-4">
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
<NuxtLink to="/backends" v-text="'Backends'"/>
: {{ backend }}
</span>
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<NuxtLink class="button is-danger" v-tooltip.bottom="'Delete Backend'" :to="`/backend/${backend}/delete`">
<span class="icon"><i class="fas fa-trash"></i></span>
</NuxtLink>
</p>
<p class="control">
<NuxtLink class="button is-primary" v-tooltip.bottom="'Edit Backend'" :to="`/backend/${backend}/edit`">
<span class="icon"><i class="fas fa-edit"></i></span>
</NuxtLink>
</p>
</div> </div>
</div> </Message>
<div class="is-hidden-mobile">
<span class="subtitle">Basic information about backend activity.</span>
</div>
</div> </div>
</div> </div>
<div class="columns is-multiline"> <template v-if="!isLoading && !isError">
<div class="column is-12"> <div class="columns is-multiline">
<div class="content"> <div class="column is-12 is-clearfix is-unselectable">
<h1 class="title is-4">Useful Tools</h1> <span class="title is-4">
<ul> <span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
<li> <NuxtLink to="/backends" v-text="'Backends'"/>
<NuxtLink :to="`/backend/${backend}/mismatched`" v-text="'Find possible mis-identified content.'"/> : {{ backend }}
</li>
<li>
<NuxtLink :to="`/backend/${backend}/unmatched`" v-text="'Find unmatched content.'"/>
</li>
<li>
<NuxtLink :to="`/backend/${backend}/libraries`" v-text="'View backend libraries.'"/>
</li>
<li>
<NuxtLink :to="`/backend/${backend}/users`" v-text="'View backend users.'"/>
</li>
<li>
<NuxtLink :to="`/backend/${backend}/sessions`" v-text="'View active sessions.'"/>
</li>
<li>
<NuxtLink :to="`/backend/${backend}/search`" v-text="'Search backend content.'"/>
</li>
</ul>
</div>
</div>
</div>
<div class="columns" v-if="bHistory.length<1">
<div class="column is-12">
<Message message_class="is-background-warning-80 has-text-dark" title="Warning" icon="fas fa-exclamation-circle"
message="No items were found. There are probably no items in the local database yet."/>
</div>
</div>
<div class="columns is-multiline" v-else>
<div class="column is-12">
<h1 class="title is-4">Recent History</h1>
</div>
<div class="column is-6-tablet" v-for="history in bHistory" :key="history.id">
<div class="card" :class="{ 'is-success': history.watched }">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<NuxtLink :to="`/history/${history.id}`" v-text="makeName(history)"/>
</p>
<span class="card-header-icon">
<span class="icon" v-if="'episode' === history.type"><i class="fas fa-tv"></i></span>
<span class="icon" v-else><i class="fas fa-film"></i></span>
</span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-4-tablet is-6-mobile has-text-left-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`Updated at: ${moment.unix(history.updated_at ?? history.updated).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(history.updated_at ?? history.updated).fromNow() }}
</span>
</span>
</div>
<div class="column is-4-tablet is-6-mobile has-text-right-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-server"></i></span>
<span>
<NuxtLink :to="'/backend/'+history.via" v-text="history.via"/>
</span>
</span>
</div>
<div class="column is-4-tablet is-12-mobile has-text-left-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-envelope"></i></span>
<span>{{ history.event }}</span>
</span>
</div>
</div>
</div>
<div class="card-footer" v-if="history.progress">
<div class="card-footer-item">
<span class="has-text-success" v-if="history.watched">Played</span>
<span class="has-text-danger" v-else>Unplayed</span>
</div>
<div class="card-footer-item">{{ formatDuration(history.progress) }}</div>
</div>
</div>
</div>
<div class="column is-12">
<NuxtLink :to="`/history/?perpage=50&page=1&q=${backend}.via://${backend}&key=metadata`">
<span class="icon-text">
<span class="icon"><i class="fas fa-history"></i></span>
<span>View all history related to this backend</span>
</span> </span>
</NuxtLink> <div class="is-pulled-right">
</div> <div class="field is-grouped">
</div> <p class="control">
<NuxtLink class="button is-danger" v-tooltip.bottom="'Delete Backend'"
<div class="columns is-multiline" v-if="info"> :to="`/backend/${backend}/delete`">
<div class="column is-12"> <span class="icon"><i class="fas fa-trash"></i></span>
<h1 class="title is-4">Basic info</h1> </NuxtLink>
</div> </p>
<div class="column is-12"> <p class="control">
<div class="content"> <NuxtLink class="button is-primary" v-tooltip.bottom="'Edit Backend'" :to="`/backend/${backend}/edit`">
<code class="is-block is-pre-wrap" v-text="info"></code> <span class="icon"><i class="fas fa-edit"></i></span>
</NuxtLink>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">Basic information about backend activity.</span>
</div>
</div> </div>
</div> </div>
</div>
</template> <div class="columns is-multiline">
<div class="column is-12">
<div class="content">
<h1 class="title is-4">Useful Tools</h1>
<ul>
<li>
<NuxtLink :to="`/backend/${backend}/mismatched`" v-text="'Find possible mis-identified content.'"/>
</li>
<li>
<NuxtLink :to="`/backend/${backend}/unmatched`" v-text="'Find unmatched content.'"/>
</li>
<li>
<NuxtLink :to="`/backend/${backend}/libraries`" v-text="'View backend libraries.'"/>
</li>
<li>
<NuxtLink :to="`/backend/${backend}/users`" v-text="'View backend users.'"/>
</li>
<li>
<NuxtLink :to="`/backend/${backend}/sessions`" v-text="'View active sessions.'"/>
</li>
<li>
<NuxtLink :to="`/backend/${backend}/search`" v-text="'Search backend content.'"/>
</li>
</ul>
</div>
</div>
</div>
<div class="columns" v-if="bHistory.length<1">
<div class="column is-12">
<Message message_class="is-background-warning-80 has-text-dark" title="Warning"
icon="fas fa-exclamation-circle"
message="No items were found. There are probably no items in the local database yet."/>
</div>
</div>
<div class="columns is-multiline" v-else>
<div class="column is-12">
<h1 class="title is-4">Recent History</h1>
</div>
<div class="column is-6-tablet" v-for="history in bHistory" :key="history.id">
<div class="card" :class="{ 'is-success': history.watched }">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<NuxtLink :to="`/history/${history.id}`" v-text="makeName(history)"/>
</p>
<span class="card-header-icon">
<span class="icon" v-if="'episode' === history.type"><i class="fas fa-tv"></i></span>
<span class="icon" v-else><i class="fas fa-film"></i></span>
</span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-4-tablet is-6-mobile has-text-left-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`Updated at: ${moment.unix(history.updated_at ?? history.updated).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(history.updated_at ?? history.updated).fromNow() }}
</span>
</span>
</div>
<div class="column is-4-tablet is-6-mobile has-text-right-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-server"></i></span>
<span>
<NuxtLink :to="'/backend/'+history.via" v-text="history.via"/>
</span>
</span>
</div>
<div class="column is-4-tablet is-12-mobile has-text-left-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-envelope"></i></span>
<span>{{ history.event }}</span>
</span>
</div>
</div>
</div>
<div class="card-footer" v-if="history.progress">
<div class="card-footer-item">
<span class="has-text-success" v-if="history.watched">Played</span>
<span class="has-text-danger" v-else>Unplayed</span>
</div>
<div class="card-footer-item">{{ formatDuration(history.progress) }}</div>
</div>
</div>
</div>
<div class="column is-12">
<NuxtLink :to="`/history/?perpage=50&page=1&q=${backend}.via://${backend}&key=metadata`">
<span class="icon-text">
<span class="icon"><i class="fas fa-history"></i></span>
<span>View all history related to this backend</span>
</span>
</NuxtLink>
</div>
</div>
<div class="columns is-multiline" v-if="info">
<div class="column is-12">
<h1 class="title is-4">Basic info</h1>
</div>
<div class="column is-12">
<div class="content">
<code class="is-block is-pre-wrap" v-text="info"></code>
</div>
</div>
</div>
</template>
</div>
</template> </template>
<script setup> <script setup>
import moment from 'moment' import moment from 'moment'
import Message from '~/components/Message.vue' import Message from '~/components/Message'
import {formatDuration, makeName, notification, TOOLTIP_DATE_FORMAT} from "~/utils/index.js"; import {formatDuration, makeName, notification, TOOLTIP_DATE_FORMAT} from '~/utils/index'
const backend = ref(useRoute().params.backend) const backend = ref(useRoute().params.backend)
const bHistory = ref([]) const bHistory = ref([])

View File

@@ -1,99 +1,102 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span> <span class="title is-4">
<NuxtLink to="/backends" v-text="'Backends'"/> <span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
- <NuxtLink to="/backends" v-text="'Backends'"/>
<NuxtLink :to="'/backend/' + backend" v-text="backend"/> -
: Libraries <NuxtLink :to="'/backend/' + backend" v-text="backend"/>
</span> : Libraries
</span>
<div class="is-pulled-right"> <div class="is-pulled-right">
<div class="field is-grouped"> <div class="field is-grouped">
<p class="control"> <p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading" :class="{'is-loading':isLoading}"> <button class="button is-info" @click="loadContent" :disabled="isLoading"
<span class="icon"><i class="fas fa-sync"></i></span> :class="{'is-loading':isLoading}">
</button> <span class="icon"><i class="fas fa-sync"></i></span>
</p> </button>
</p>
</div>
</div>
<div class="subtitle is-hidden-mobile">
This page will show all the libraries that are available in the backend.
</div> </div>
</div> </div>
<div class="subtitle is-hidden-mobile"> <div class="column is-12" v-if="items.length < 1">
This page will show all the libraries that are available in the backend. <Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
message="Loading libraries list. Please wait..." v-if="isLoading"/>
<Message v-else message_class="has-background-warning-80 has-text-dark" title="Warning"
icon="fas fa-exclamation-circle"
message="WatchState was unable to get any libraries from the backend."/>
</div> </div>
</div>
<div class="column is-12" v-if="items.length < 1"> <div class="column is-6" v-for="item in items" :key="`library-${item.id}`">
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin" <div class="card">
message="Loading libraries list. Please wait..." v-if="isLoading"/> <header class="card-header">
<Message v-else message_class="has-background-warning-80 has-text-dark" title="Warning" <p class="card-header-title is-text-overflow">
icon="fas fa-exclamation-circle" <NuxtLink target="_blank" :to="item.webUrl" v-text="item.title" v-if="item?.webUrl"/>
message="WatchState was unable to get any libraries from the backend."/> <span v-else v-text="item.title"/>
</div> </p>
<div class="card-header-icon">
<div class="column is-6" v-for="item in items" :key="`library-${item.id}`"> <span class="icon">
<div class="card"> <i class="fas fa-film" :class="{'fa-film': 'Movie' === item.type, 'fa-tv': 'Movie' !== item.type}"></i>
<header class="card-header"> </span>
<p class="card-header-title is-text-overflow">
<NuxtLink target="_blank" :to="item.webUrl" v-text="item.title" v-if="item?.webUrl"/>
<span v-else v-text="item.title"/>
</p>
<div class="card-header-icon">
<span class="icon">
<i class="fas fa-film" :class="{'fa-film': 'Movie' === item.type, 'fa-tv': 'Movie' !== item.type}"></i>
</span>
</div>
</header>
<div class="card-content">
<div class="columns is-mobile is-multiline">
<div class="column is-6">
<strong>Type:</strong> {{ item.type }}
</div> </div>
<div class="column is-6 has-text-right"> </header>
<strong>Supported:</strong> {{ item.supported ? 'Yes' : 'No' }} <div class="card-content">
</div> <div class="columns is-mobile is-multiline">
<div class="column is-6" v-if="item?.agent"> <div class="column is-6">
<div class="is-text-overflow"> <strong>Type:</strong> {{ item.type }}
<strong>Agent:</strong> {{ item.agent }}
</div> </div>
</div> <div class="column is-6 has-text-right">
<div class="column is-6 has-text-right" v-if="item?.scanner"> <strong>Supported:</strong> {{ item.supported ? 'Yes' : 'No' }}
<div class="is-text-overflow"> </div>
<strong>Scanner:</strong> {{ item.scanner }} <div class="column is-6" v-if="item?.agent">
<div class="is-text-overflow">
<strong>Agent:</strong> {{ item.agent }}
</div>
</div>
<div class="column is-6 has-text-right" v-if="item?.scanner">
<div class="is-text-overflow">
<strong>Scanner:</strong> {{ item.scanner }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="card-footer">
<div class="card-footer"> <div class="card-footer-item is-justify-content-start">
<div class="card-footer-item is-justify-content-start"> <input :id="`ignore-${item.id}`" type="checkbox" class="switch is-success" :checked="item.ignored"
<input :id="`ignore-${item.id}`" type="checkbox" class="switch is-success" :checked="item.ignored" @change="toggleIgnore(item)">
@change="toggleIgnore(item)"> <label :for="`ignore-${item.id}`"></label>
<label :for="`ignore-${item.id}`"></label> <span>Ignore content from this library.</span>
<span>Ignore content from this library.</span> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle" <Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle"
:toggle="show_page_tips" @toggle="show_page_tips = !show_page_tips" :use-toggle="true"> :toggle="show_page_tips" @toggle="show_page_tips = !show_page_tips" :use-toggle="true">
<ul> <ul>
<li>Ignoring library will prevent any content from being added to the local database from the library <li>Ignoring library will prevent any content from being added to the local database from the library
during import process, and webhook events handling. during import process, and webhook events handling.
</li> </li>
<li>Libraries that shows <code>Supported: No</code> will not be processed by <code>WatchState</code>.</li> <li>Libraries that shows <code>Supported: No</code> will not be processed by <code>WatchState</code>.</li>
</ul> </ul>
</Message> </Message>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {notification} from '~/utils/index.js' import {notification} from '~/utils/index'
import {useStorage} from '@vueuse/core' import {useStorage} from '@vueuse/core'
import request from '~/utils/request.js' import request from '~/utils/request'
const backend = useRoute().params.backend const backend = useRoute().params.backend
const items = ref([]) const items = ref([])

View File

@@ -1,141 +1,144 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<NuxtLink to="/backends" v-text="'Backends'"/> <span class="title is-4">
- <NuxtLink to="/backends" v-text="'Backends'"/>
<NuxtLink :to="'/backend/' + backend" v-text="backend"/> -
: Misidentified <NuxtLink :to="'/backend/' + backend" v-text="backend"/>
</span> : Misidentified
<div class="is-pulled-right" v-if="hasLooked"> </span>
<div class="field is-grouped"> <div class="is-pulled-right" v-if="hasLooked">
<p class="control"> <div class="field is-grouped">
<button class="button is-info" @click="loadContent" :disabled="isLoading" :class="{'is-loading':isLoading}"> <p class="control">
<span class="icon"><i class="fas fa-sync"></i></span> <button class="button is-info" @click="loadContent" :disabled="isLoading"
</button> :class="{'is-loading':isLoading}">
</p> <span class="icon"><i class="fas fa-sync"></i></span>
</div> </button>
</div> </p>
<div class="subtitle is-hidden-mobile">
This page will show items that <code>WatchState</code> thinks are possibly mis-identified in your backend.
</div>
</div>
<div class="column is-12" v-if="false === hasLooked">
<div class="card">
<header class="card-header">
<p class="card-header-title is-justify-center">Request Analyze</p>
</header>
<div class="card-content">
<div class="content">
<ul>
<li>
Checking the items will take time, you will see the spinner while <code>WatchState</code> is analyzing
the entire backend libraries content. Do not reload the page.
</li>
</ul>
</div> </div>
</div> </div>
<div class="control"> <div class="subtitle is-hidden-mobile">
<button class="button is-fullwidth is-primary" @click="loadContent" :disabled="isLoading"> This page will show items that <code>WatchState</code> thinks are possibly mis-identified in your backend.
<span class="icon"><i class="fas fa-check"></i></span>
<span>Initiate The process</span>
</button>
</div> </div>
</div> </div>
</div>
<div class="column is-12" v-if="items.length < 1">
<Message v-if="isLoading" message_class="is-background-info-90 has-text-dark"
title="Analyzing" icon="fas fa-spinner fa-spin"
message="Analyzing the backend content. Please wait. It will take a while..."/>
<Message v-else-if="!isLoading && hasLooked" message_class="has-background-success-90 has-text-dark"
title="Success!" icon="fas fa-check"
message="WatchState did not find any possible mismatched items in the libraries we looked at."/>
</div>
<template v-if="items.length > 1"> <div class="column is-12" v-if="false === hasLooked">
<div class="column is-12">
<Message class="has-background-warning-80 has-text-dark" title="Warning" icon="fas fa-exclamation-triangle"
message="WatchState found some items that might be mis-identified in your backend. Please review the results."/>
</div>
<div class="column is-6" v-for="item in items">
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title is-text-overflow"> <p class="card-header-title is-justify-center">Request Analyze</p>
<NuxtLink target="_blank" :to="item.webUrl" v-if="item.webUrl" v-text="item.title"/>
<span v-else>{{ item.title }}</span>
</p>
<div class="card-header-icon" @click="item.showItem = !item.showItem">
<span class="icon has-tooltip">
<i class="fas fa-film" :class="{'fa-film': 'Movie' === item.type,'fa-tv': 'Movie' !== item.type}"></i>
</span>
</div>
</header> </header>
<div class="card-content"> <div class="card-content">
<div class="columns is-mobile is-multiline"> <div class="content">
<div class="column is-6"> <ul>
<strong class="is-unselectable">Library:</strong> {{ item.library }} <li>
Checking the items will take time, you will see the spinner while <code>WatchState</code> is analyzing
the entire backend libraries content. Do not reload the page.
</li>
</ul>
</div>
</div>
<div class="control">
<button class="button is-fullwidth is-primary" @click="loadContent" :disabled="isLoading">
<span class="icon"><i class="fas fa-check"></i></span>
<span>Initiate The process</span>
</button>
</div>
</div>
</div>
<div class="column is-12" v-if="items.length < 1">
<Message v-if="isLoading" message_class="is-background-info-90 has-text-dark"
title="Analyzing" icon="fas fa-spinner fa-spin"
message="Analyzing the backend content. Please wait. It will take a while..."/>
<Message v-else-if="!isLoading && hasLooked" message_class="has-background-success-90 has-text-dark"
title="Success!" icon="fas fa-check"
message="WatchState did not find any possible mismatched items in the libraries we looked at."/>
</div>
<template v-if="items.length > 1">
<div class="column is-12">
<Message class="has-background-warning-80 has-text-dark" title="Warning" icon="fas fa-exclamation-triangle"
message="WatchState found some items that might be mis-identified in your backend. Please review the results."/>
</div>
<div class="column is-6" v-for="item in items">
<div class="card">
<header class="card-header">
<p class="card-header-title is-text-overflow">
<NuxtLink target="_blank" :to="item.webUrl" v-if="item.webUrl" v-text="item.title"/>
<span v-else>{{ item.title }}</span>
</p>
<div class="card-header-icon" @click="item.showItem = !item.showItem">
<span class="icon has-tooltip">
<i class="fas fa-film" :class="{'fa-film': 'Movie' === item.type,'fa-tv': 'Movie' !== item.type}"></i>
</span>
</div> </div>
<div class="column is-6 has-text-right"> </header>
<strong class="is-unselectable">Type:</strong> {{ item.type }} <div class="card-content">
</div> <div class="columns is-mobile is-multiline">
<div class="column is-6"> <div class="column is-6">
<strong class="is-unselectable">Year:</strong> {{ item.year ?? '???' }} <strong class="is-unselectable">Library:</strong> {{ item.library }}
</div> </div>
<div class="column is-6 has-text-right"> <div class="column is-6 has-text-right">
<strong class="is-unselectable">Percent:</strong> <span :class="percentColor(item.percent)"> <strong class="is-unselectable">Type:</strong> {{ item.type }}
{{ item.percent.toFixed(2) }}% </div>
</span> <div class="column is-6">
</div> <strong class="is-unselectable">Year:</strong> {{ item.year ?? '???' }}
<div class="column is-12" v-if="item.path" </div>
@click="(e) => e.target.firstChild?.classList?.toggle('is-text-overflow')"> <div class="column is-6 has-text-right">
<div class="is-text-overflow"> <strong class="is-unselectable">Percent:</strong> <span :class="percentColor(item.percent)">
<strong class="is-unselectable">Path:&nbsp;</strong> {{ item.percent.toFixed(2) }}%
<NuxtLink :to="makeSearchLink('path',item.path)" v-text="item.path"/> </span>
</div>
<div class="column is-12" v-if="item.path"
@click="(e) => e.target.firstChild?.classList?.toggle('is-text-overflow')">
<div class="is-text-overflow">
<strong class="is-unselectable">Path:&nbsp;</strong>
<NuxtLink :to="makeSearchLink('path',item.path)" v-text="item.path"/>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="card-content p-0 m-0" v-if="item?.showItem">
<div class="card-content p-0 m-0" v-if="item?.showItem"> <pre><code>{{ JSON.stringify(item, null, 2) }}</code></pre>
<pre><code>{{ JSON.stringify(item, null, 2) }}</code></pre> </div>
</div> </div>
</div> </div>
</div> </template>
</template>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> @toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul> <ul>
<li> <li>
This service expects standard plex naming conventions This service expects standard plex naming conventions
<NuxtLink target="_blank" to="https://support.plex.tv/articles/naming-and-organizing-your-tv-show-files/" <NuxtLink target="_blank" to="https://support.plex.tv/articles/naming-and-organizing-your-tv-show-files/"
v-text="'for series'"/> v-text="'for series'"/>
, and , and
<NuxtLink target="_blank" <NuxtLink target="_blank"
to="https://support.plex.tv/articles/naming-and-organizing-your-movie-media-files/" to="https://support.plex.tv/articles/naming-and-organizing-your-movie-media-files/"
v-text="'for movies'"/> v-text="'for movies'"/>
. So if you libraries doesn't follow the same conventions, you will see a lot of items being reported as . So if you libraries doesn't follow the same conventions, you will see a lot of items being reported as
misidentified. misidentified.
</li> </li>
<li> <li>
If you see a lot of misidentified items, you might want to check the that the source directory matches the If you see a lot of misidentified items, you might want to check the that the source directory matches the
item. item.
</li> </li>
<li> <li>
Clicking on the icon next to the title will show you the raw data that was used to generate the report. Clicking on the icon next to the title will show you the raw data that was used to generate the report.
</li> </li>
</ul> </ul>
</Message> </Message>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {makeSearchLink, notification} from "~/utils/index.js"; import {makeSearchLink, notification} from '~/utils/index'
import {useStorage} from "@vueuse/core"; import {useStorage} from '@vueuse/core'
import Message from "~/components/Message.vue"; import Message from '~/components/Message'
const backend = useRoute().params.backend const backend = useRoute().params.backend
const items = ref([]) const items = ref([])

View File

@@ -1,205 +1,208 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<NuxtLink to="/backends" v-text="'Backends'"/> <span class="title is-4">
- <NuxtLink to="/backends" v-text="'Backends'"/>
<NuxtLink :to="'/backend/' + backend" v-text="backend"/> -
: Search <NuxtLink :to="'/backend/' + backend" v-text="backend"/>
</span> : Search
<div class="is-pulled-right"> </span>
<div class="field is-grouped"> <div class="is-pulled-right">
<p class="control"> <div class="field is-grouped">
<button class="button is-info" @click="searchContent(true)" :disabled="isLoading || !searchField || !query" <p class="control">
:class="{'is-loading':isLoading}"> <button class="button is-info" @click="searchContent(true)"
<span class="icon"><i class="fas fa-sync"></i></span> :disabled="isLoading || !searchField || !query"
</button> :class="{'is-loading':isLoading}">
</p> <span class="icon"><i class="fas fa-sync"></i></span>
</div> </button>
</div> </p>
<div class="is-hidden-mobile">
<span class="subtitle">This page search the remote backend data not the locally stored data.</span>
</div>
</div>
<div class="column is-12">
<form @submit.prevent="searchContent(false)">
<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="" disabled>Select Field</option>
<option v-for="field in searchable" :key="'search-' + field" :value="field">
{{ field }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-folder-tree"></i>
</div>
</div>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="limit" :disabled="isLoading">
<option value="" disabled>Select Limit</option>
<option v-for="limiter in limits" :key="'search-' + limiter" :value="limiter">
{{ limiter }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-sitemap"></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-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 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>
</div> </div>
</div> </div>
</form> <div class="is-hidden-mobile">
</div> <span class="subtitle">This page search the remote backend data not the locally stored data.</span>
</div>
</div>
<div class="column is-12" v-if="items?.length<1 && hasSearched"> <div class="column is-12">
<Message v-if="isLoading" message_class="is-background-info-90 has-text-dark" icon="fas fa-spinner fa-spin" <form @submit.prevent="searchContent(false)">
title="Loading" message="Loading data please wait..."/> <div class="field">
<Message v-else class="has-background-warning-80 has-text-dark" title="Warning" <div class="field-body">
icon="fas fa-exclamation-triangle" :use-close="true" @close="clearSearch"> <div class="field is-grouped-tablet">
<span v-if="error?.message" v-text="error.message"></span> <div class="control has-icons-left">
<template v-else> <div class="select is-fullwidth">
<span>No items found.</span> <select v-model="searchField" class="is-capitalized" :disabled="isLoading">
<span v-if="query"> <option value="" disabled>Select Field</option>
Search query <code><strong>{{ searchField }}</strong> : <strong>{{ query }}</strong></code> <option v-for="field in searchable" :key="'search-' + field" :value="field">
</span> {{ field }}
</template> </option>
</Message> </select>
</div> </div>
<div class="icon is-left">
<i class="fas fa-folder-tree"></i>
</div>
</div>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="limit" :disabled="isLoading">
<option value="" disabled>Select Limit</option>
<option v-for="limiter in limits" :key="'search-' + limiter" :value="limiter">
{{ limiter }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-sitemap"></i>
</div>
</div>
<div class="column is-12"> <div class="control is-expanded has-icons-left">
<div class="columns is-multiline" v-if="items?.length>0"> <input class="input" type="search" placeholder="Search..." v-model="query"
<div class="column is-6-tablet" v-for="item in items" :key="item.id"> :disabled="'' === searchField || isLoading">
<div class="card" :class="{ 'is-success': item.watched }"> <div class="icon is-left">
<header class="card-header"> <i class="fas fa-search"></i>
<p class="card-header-title is-text-overflow"> </div>
<NuxtLink :to="item.webUrl" v-text="makeName(item)" target="_blank"/> </div>
</p>
<span class="card-header-icon"> <div class="control">
<span class="icon"> <button class="button is-primary" type="submit" :disabled="!query || '' === searchField || isLoading"
<i class="fas" :class="{'is-loading':isLoading}">
:class="{'fa-folder': 'show' === item.type, 'fa-tv': 'episode' === item.type, 'fa-film': 'movie' === item.type}"></i> <span class="icon-text">
<span class="icon"><i class="fas fa-search"></i></span>
<span>Search</span>
</span>
</button>
</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>
</div>
</div>
</form>
</div>
<div class="column is-12" v-if="items?.length<1 && hasSearched">
<Message v-if="isLoading" message_class="is-background-info-90 has-text-dark" icon="fas fa-spinner fa-spin"
title="Loading" message="Loading data please wait..."/>
<Message v-else class="has-background-warning-80 has-text-dark" title="Warning"
icon="fas fa-exclamation-triangle" :use-close="true" @close="clearSearch">
<span v-if="error?.message" v-text="error.message"></span>
<template v-else>
<span>No items found.</span>
<span v-if="query">
Search query <code><strong>{{ searchField }}</strong> : <strong>{{ query }}</strong></code>
</span>
</template>
</Message>
</div>
<div class="column is-12">
<div class="columns is-multiline" v-if="items?.length>0">
<div class="column is-6-tablet" v-for="item in items" :key="item.id">
<div class="card" :class="{ 'is-success': item.watched }">
<header class="card-header">
<p class="card-header-title is-text-overflow">
<NuxtLink :to="item.webUrl" v-text="makeName(item)" target="_blank"/>
</p>
<span class="card-header-icon">
<span class="icon">
<i class="fas"
:class="{'fa-folder': 'show' === item.type, 'fa-tv': 'episode' === item.type, 'fa-film': 'movie' === item.type}"></i>
</span>
</span> </span>
</span> </header>
</header> <div class="card-content">
<div class="card-content"> <div class="columns is-multiline is-mobile has-text-centered">
<div class="columns is-multiline is-mobile has-text-centered"> <div class="column is-12 has-text-left" v-if="item?.title">
<div class="column is-12 has-text-left" v-if="item?.title"> <div class="is-text-overflow is-clickable"
<div class="is-text-overflow is-clickable" @click="(e) => e.target.classList.toggle('is-text-overflow')">
@click="(e) => e.target.classList.toggle('is-text-overflow')"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i>&nbsp;</span>
<NuxtLink :to="makeSearchLink('title',item.title)" v-text="item.title"/>
</div>
</div>
</div>
<div class="column is-12 is-clickable has-text-left" v-if="item?.content_path"
@click="(e) => e.target.firstChild?.classList?.toggle('is-text-overflow')">
<div class="is-text-overflow"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i>&nbsp;</span> <span class="icon"><i class="fas fa-file"></i></span>
<NuxtLink :to="makeSearchLink('title',item.title)" v-text="item.title"/> <NuxtLink :to="makeSearchLink('path',item.content_path)" v-text="item.content_path"/>
</div> </div>
</div> </div>
</div> </div>
<div class="column is-12 is-clickable has-text-left" v-if="item?.content_path"
@click="(e) => e.target.firstChild?.classList?.toggle('is-text-overflow')">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i></span>
<NuxtLink :to="makeSearchLink('path',item.content_path)" v-text="item.content_path"/>
</div>
</div>
</div> </div>
</div> <div class="card-footer">
<div class="card-footer"> <div class="card-footer-item">
<div class="card-footer-item"> <span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span> <span class="has-tooltip"
<span class="has-tooltip" v-tooltip="moment.unix(item.updated_at ?? item.updated).format(TOOLTIP_DATE_FORMAT)">
v-tooltip="moment.unix(item.updated_at ?? item.updated).format(TOOLTIP_DATE_FORMAT)"> {{ moment.unix(item.updated_at ?? item.updated).fromNow() }}
{{ moment.unix(item.updated_at ?? item.updated).fromNow() }}
</span>
</div>
<div class="card-footer-item">
<span class="icon">
<i class="fas"
:class="{'fa-folder': 'show' === item.type, 'fa-tv': 'episode' === item.type, 'fa-film': 'movie' === item.type}"></i>
&nbsp;
</span>
<span class="is-capitalized">{{ item.type }}</span>
</div>
<div class="card-footer-item">
<span class="icon"><i class="fas fa-database"></i>&nbsp;</span>
<span>
<NuxtLink
:to="makeSearchLink(`metadata`,`${item.via}.show://${ag(item,`metadata.${item.via}.id`)}`)"
v-if="'show' === item.type">
View linked items
</NuxtLink>
<NuxtLink :to="`/history/${item.id}`" v-else-if="item.id">
View local item
</NuxtLink>
<span v-else class="has-text-danger">
Not imported
</span> </span>
</span> </div>
<div class="card-footer-item">
<span class="icon">
<i class="fas"
:class="{'fa-folder': 'show' === item.type, 'fa-tv': 'episode' === item.type, 'fa-film': 'movie' === item.type}"></i>
&nbsp;
</span>
<span class="is-capitalized">{{ item.type }}</span>
</div>
<div class="card-footer-item">
<span class="icon"><i class="fas fa-database"></i>&nbsp;</span>
<span>
<NuxtLink
:to="makeSearchLink(`metadata`,`${item.via}.show://${ag(item,`metadata.${item.via}.id`)}`)"
v-if="'show' === item.type">
View linked items
</NuxtLink>
<NuxtLink :to="`/history/${item.id}`" v-else-if="item.id">
View local item
</NuxtLink>
<span v-else class="has-text-danger">
Not imported
</span>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle" <Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle"
:toggle="show_page_tips" @toggle="show_page_tips = !show_page_tips" :use-toggle="true"> :toggle="show_page_tips" @toggle="show_page_tips = !show_page_tips" :use-toggle="true">
<ul> <ul>
<li> <li>
items with <code>Not imported</code> text are items not yet imported to local database. items with <code>Not imported</code> text are items not yet imported to local database.
</li> </li>
<li> <li>
The items shown here are from the remote backend data queried directly. The items shown here are from the remote backend data queried directly.
</li> </li>
<li>Clicking directly on the <code>item title</code> will take you to the page associated with that link in <li>Clicking directly on the <code>item title</code> will take you to the page associated with that link in
the backend. the backend.
</li> </li>
</ul> </ul>
</Message> </Message>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import request from '~/utils/request.js' import request from '~/utils/request'
import moment from 'moment' import moment from 'moment'
import {makeName, makeSearchLink, notification, TOOLTIP_DATE_FORMAT} from '~/utils/index.js' import {makeName, makeSearchLink, notification, TOOLTIP_DATE_FORMAT} from '~/utils/index'
import Message from "~/components/Message.vue"; import Message from '~/components/Message'
import {useStorage} from "@vueuse/core"; import {useStorage} from '@vueuse/core'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -1,68 +1,71 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<NuxtLink to="/backends" v-text="'Backends'"/> <span class="title is-4">
- <NuxtLink to="/backends" v-text="'Backends'"/>
<NuxtLink :to="'/backend/' + backend" v-text="backend"/> -
: Sessions <NuxtLink :to="'/backend/' + backend" v-text="backend"/>
</span> : Sessions
</span>
<div class="is-pulled-right"> <div class="is-pulled-right">
<div class="field is-grouped"> <div class="field is-grouped">
<p class="control"> <p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading" :class="{'is-loading':isLoading}"> <button class="button is-info" @click="loadContent" :disabled="isLoading"
<span class="icon"><i class="fas fa-sync"></i></span> :class="{'is-loading':isLoading}">
</button> <span class="icon"><i class="fas fa-sync"></i></span>
</p> </button>
</p>
</div>
</div>
<div class="subtitle is-hidden-mobile">
Show backend's sessions that are currently active.
</div> </div>
</div> </div>
<div class="subtitle is-hidden-mobile"> <div class="column is-12" v-if="items.length < 1">
Show backend's sessions that are currently active. <Message message_class="is-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
v-if="isLoading" message="Requesting active play sessions. Please wait..."/>
<Message v-else message_class="has-background-success-90 has-text-dark" title="Information"
icon="fa fa-info-circle" message="There are no active play sessions currently running."/>
</div> </div>
</div> <template v-else>
<div class="column is-12">
<div class="column is-12" v-if="items.length < 1"> <div class="content">
<Message message_class="is-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin" <h1 class="title is-4">Active Sessions</h1>
v-if="isLoading" message="Requesting active play sessions. Please wait..."/> </div>
<Message v-else message_class="has-background-success-90 has-text-dark" title="Information"
icon="fa fa-info-circle" message="There are no active play sessions currently running."/>
</div>
<template v-else>
<div class="column is-12">
<div class="content">
<h1 class="title is-4">Active Sessions</h1>
</div> </div>
</div> <div class="column is-12">
<div class="column is-12"> <table class="table is-fullwidth is-hoverable is-striped">
<table class="table is-fullwidth is-hoverable is-striped"> <thead>
<thead> <tr>
<tr> <th>User</th>
<th>User</th> <th>Title</th>
<th>Title</th> <th>State</th>
<th>State</th> <th>Progress at</th>
<th>Progress at</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> <tr v-for="item in items" :key="item.id">
<tr v-for="item in items" :key="item.id"> <td>{{ item.user_name }}</td>
<td>{{ item.user_name }}</td> <td>
<td> <NuxtLink :to="makeItemLink(item)" v-text="item.item_title"/>
<NuxtLink :to="makeItemLink(item)" v-text="item.item_title"/> </td>
</td> <td>{{ item.session_state }}</td>
<td>{{ item.session_state }}</td> <td>{{ formatDuration(item.item_offset_at) }}</td>
<td>{{ formatDuration(item.item_offset_at) }}</td> </tr>
</tr> </tbody>
</tbody> </table>
</table> </div>
</div> </template>
</template> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {formatDuration, notification} from "~/utils/index.js"; import {formatDuration, notification} from "~/utils/index";
const backend = useRoute().params.backend const backend = useRoute().params.backend
const items = ref([]) const items = ref([])

View File

@@ -1,124 +1,126 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<NuxtLink to="/backends" v-text="'Backends'"/> <span class="title is-4">
- <NuxtLink to="/backends" v-text="'Backends'"/>
<NuxtLink :to="'/backend/' + backend" v-text="backend"/> -
: Unmatched <NuxtLink :to="'/backend/' + backend" v-text="backend"/>
</span> : Unmatched
<div class="is-pulled-right" v-if="hasLooked">
<div class="field is-grouped">
<p class="control">
<button class="button is-info" @click.prevent="loadContent" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</div>
</div>
<div class="subtitle is-hidden-mobile">
In this page you will find items <code>WatchState</code> knows that are un-matched in your backend.
</div>
</div>
<div class="column is-12" v-if="false === hasLooked">
<div class="card">
<header class="card-header">
<p class="card-header-title is-justify-center">Request Analyze</p>
</header>
<div class="card-content">
<div class="content">
<ul>
<li>
Checking the items will take time, you will see the spinner while <code>WatchState</code> is analyzing
the entire backend libraries content. Do not reload the page.
</li>
</ul>
</div>
</div>
<div class="control">
<button class="button is-fullwidth is-primary" @click="loadContent" :disabled="isLoading">
<span class="icon"><i class="fas fa-check"></i></span>
<span>Initiate The process</span>
</button>
</div>
</div>
</div>
<div class="column is-12" v-if="items.length < 1">
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark"
title="Analyzing" icon="fas fa-spinner fa-spin"
message="Analyzing the backend content. Please wait. It will take a while..."/>
<Message v-if="!isLoading && hasLooked" message_class="has-background-success-90 has-text-dark"
title="Success!" icon="fas fa-check"
message="WatchState did not find any unmatched content in the libraries we looked at."/>
</div>
<div class="column is-12" v-if="items.length > 1">
<h1 class="title is-4">
<span class="icon-text">
<span class="icon has-text-danger"><i class="fas fa-exclamation-triangle"></i></span>
<span>Unmatched Content</span>
</span> </span>
</h1> <div class="is-pulled-right" v-if="hasLooked">
</div> <div class="field is-grouped">
<p class="control">
<div class="column is-6" v-for="item in items"> <button class="button is-info" @click.prevent="loadContent" :disabled="isLoading"
<div class="card"> :class="{'is-loading':isLoading}">
<header class="card-header"> <span class="icon"><i class="fas fa-sync"></i></span>
<p class="card-header-title is-text-overflow"> </button>
<NuxtLink target="_blank" :to="item.webUrl ?? item.url">{{ item.title }}</NuxtLink> </p>
</p>
<div class="card-header-icon" @click="item.showItem = !item.showItem">
<span class="icon has-tooltip" v-tooltip="'Toggle raw data'">
<i class="fas fa-film" :class="{'fa-film': 'Movie' === item.type,'fa-tv': 'Movie' !== item.type}"></i>
</span>
</div> </div>
</header> </div>
<div class="card-content"> <div class="subtitle is-hidden-mobile">
<div class="columns is-mobile is-multiline"> In this page you will find items <code>WatchState</code> knows that are un-matched in your backend.
<div class="column is-6"> </div>
<strong class="is-unselectable">Library:</strong> {{ item.library ?? 'Unknown' }} </div>
<div class="column is-12" v-if="false === hasLooked">
<div class="card">
<header class="card-header">
<p class="card-header-title is-justify-center">Request Analyze</p>
</header>
<div class="card-content">
<div class="content">
<ul>
<li>
Checking the items will take time, you will see the spinner while <code>WatchState</code> is analyzing
the entire backend libraries content. Do not reload the page.
</li>
</ul>
</div> </div>
<div class="column is-6 has-text-right"> </div>
<strong class="is-unselectable">Type:</strong> <span class="is-capitalized">{{ <div class="control">
item.type ?? 'Unknown' <button class="button is-fullwidth is-primary" @click="loadContent" :disabled="isLoading">
}}</span> <span class="icon"><i class="fas fa-check"></i></span>
<span>Initiate The process</span>
</button>
</div>
</div>
</div>
<div class="column is-12" v-if="items.length < 1">
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark"
title="Analyzing" icon="fas fa-spinner fa-spin"
message="Analyzing the backend content. Please wait. It will take a while..."/>
<Message v-if="!isLoading && hasLooked" message_class="has-background-success-90 has-text-dark"
title="Success!" icon="fas fa-check"
message="WatchState did not find any unmatched content in the libraries we looked at."/>
</div>
<div class="column is-12" v-if="items.length > 1">
<h1 class="title is-4">
<span class="icon-text">
<span class="icon has-text-danger"><i class="fas fa-exclamation-triangle"></i></span>
<span>Unmatched Content</span>
</span>
</h1>
</div>
<div class="column is-6" v-for="item in items">
<div class="card">
<header class="card-header">
<p class="card-header-title is-text-overflow">
<NuxtLink target="_blank" :to="item.webUrl ?? item.url">{{ item.title }}</NuxtLink>
</p>
<div class="card-header-icon" @click="item.showItem = !item.showItem">
<span class="icon has-tooltip" v-tooltip="'Toggle raw data'">
<i class="fas fa-film" :class="{'fa-film': 'Movie' === item.type,'fa-tv': 'Movie' !== item.type}"></i>
</span>
</div> </div>
<div class="column is-6" v-if="0 !== item.year && item.year"> </header>
<strong class="is-unselectable">Year:</strong> {{ item.year ?? 'Unknown' }} <div class="card-content">
</div> <div class="columns is-mobile is-multiline">
<div class="column is-12 is-clickable has-text-left" v-if="item?.path" <div class="column is-6">
@click="(e) => e.target.firstChild?.classList?.toggle('is-text-overflow')"> <strong class="is-unselectable">Library:</strong> {{ item.library ?? 'Unknown' }}
<div class="is-text-overflow"> </div>
<strong class="is-unselectable">Path:&nbsp;</strong> <div class="column is-6 has-text-right">
<NuxtLink :to="makeSearchLink('path',item.path)" v-text="item.path"/> <strong class="is-unselectable">Type:</strong> <span class="is-capitalized">{{
item.type ?? 'Unknown'
}}</span>
</div>
<div class="column is-6" v-if="0 !== item.year && item.year">
<strong class="is-unselectable">Year:</strong> {{ item.year ?? 'Unknown' }}
</div>
<div class="column is-12 is-clickable has-text-left" v-if="item?.path"
@click="(e) => e.target.firstChild?.classList?.toggle('is-text-overflow')">
<div class="is-text-overflow">
<strong class="is-unselectable">Path:&nbsp;</strong>
<NuxtLink :to="makeSearchLink('path',item.path)" v-text="item.path"/>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="card-content p-0 m-0" v-if="item?.showItem">
<div class="card-content p-0 m-0" v-if="item?.showItem"> <pre><code>{{ JSON.stringify(item, null, 2) }}</code></pre>
<pre><code>{{ JSON.stringify(item, null, 2) }}</code></pre>
</div>
<div class="card-footer">
<div class="card-footer-item">
<NuxtLink target="_blank" :to="`https://www.imdb.com/find/?q=${fixTitle(item.title)}`">
<span class="icon"><i class="fas fa-search"></i></span>
<span>IMDb</span>
</NuxtLink>
</div> </div>
<div class="card-footer-item"> <div class="card-footer">
<NuxtLink target="_blank" :to="`https://www.themoviedb.org/search?query=${fixTitle(item.title)}`"> <div class="card-footer-item">
<span class="icon"><i class="fas fa-search"></i></span> <NuxtLink target="_blank" :to="`https://www.imdb.com/find/?q=${fixTitle(item.title)}`">
<span>TMDB</span> <span class="icon"><i class="fas fa-search"></i></span>
</NuxtLink> <span>IMDb</span>
</div> </NuxtLink>
<div class="card-footer-item"> </div>
<NuxtLink target="_blank" :to="`https://thetvdb.com/search?query=${fixTitle(item.title)}`"> <div class="card-footer-item">
<span class="icon"><i class="fas fa-search"></i></span> <NuxtLink target="_blank" :to="`https://www.themoviedb.org/search?query=${fixTitle(item.title)}`">
<span>TVDB</span> <span class="icon"><i class="fas fa-search"></i></span>
</NuxtLink> <span>TMDB</span>
</NuxtLink>
</div>
<div class="card-footer-item">
<NuxtLink target="_blank" :to="`https://thetvdb.com/search?query=${fixTitle(item.title)}`">
<span class="icon"><i class="fas fa-search"></i></span>
<span>TVDB</span>
</NuxtLink>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -127,7 +129,7 @@
</template> </template>
<script setup> <script setup>
import {makeSearchLink, notification} from "~/utils/index.js"; import {makeSearchLink, notification} from '~/utils/index'
const backend = useRoute().params.backend const backend = useRoute().params.backend
const items = ref([]) const items = ref([])

View File

@@ -1,108 +1,102 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<NuxtLink to="/backends" v-text="'Backends'"/> <span class="title is-4">
- <NuxtLink to="/backends" v-text="'Backends'"/>
<NuxtLink :to="'/backend/' + backend" v-text="backend"/> -
: Users <NuxtLink :to="'/backend/' + backend" v-text="backend"/>
</span> : Users
<div class="is-pulled-right"> </span>
<div class="field is-grouped"> <div class="is-pulled-right">
<p class="control"> <div class="field is-grouped">
<button class="button is-info" @click="loadContent" :disabled="isLoading" :class="{'is-loading':isLoading}"> <p class="control">
<span class="icon"><i class="fas fa-sync"></i></span> <button class="button is-info" @click="loadContent" :disabled="isLoading"
</button> :class="{'is-loading':isLoading}">
</p> <span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</div>
</div>
<div class="subtitle is-hidden-mobile">
Show all users that are available in the backend.
</div> </div>
</div> </div>
<div class="subtitle is-hidden-mobile">
Show all users that are available in the backend. <div class="column is-12" v-if="items.length < 1">
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
message="Loading users list. Please wait..." v-if="isLoading"/>
<Message v-else message_class="has-background-warning-80 has-text-dark" title="Warning"
icon="fas fa-exclamation-circle"
message="WatchState was unable to get any users from the backend. This is expected if the backend is plex and the token is limited."/>
</div> </div>
</div>
<div class="column is-12" v-if="items.length < 1"> <div class="column is-6" v-for="item in items" :key="`users-${item.id}`">
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin" <div class="card">
message="Loading users list. Please wait..." v-if="isLoading"/> <header class="card-header">
<Message v-else message_class="has-background-warning-80 has-text-dark" title="Warning" <p class="card-header-title is-text-overflow">
icon="fas fa-exclamation-circle" {{ item.name }}
message="WatchState was unable to get any users from the backend. This is expected if the backend is plex and the token is limited."/> </p>
</div> <div class="card-header-icon"></div>
</header>
<div class="card-content">
<div class="columns is-mobile is-multiline">
<div class="column is-6">
<strong>Admin:</strong> {{ item.admin ? 'Yes' : 'No' }}
</div>
<div class="column is-6 has-text-right" v-if="undefined !== item?.guest">
<strong>Guest:</strong> {{ item.guest ? 'Yes' : 'No' }}
</div>
<div class="column is-6 has-text-right" v-if="undefined !== item?.hidden">
<strong>Hidden:</strong> {{ item.guest ? 'Yes' : 'No' }}
</div>
<div class="column is-6" v-if="item?.updatedAt">
<strong>Updated:&nbsp;</strong>
<span class="has-tooltip" v-tooltip="moment(item.updatedAt).format(TOOLTIP_DATE_FORMAT)">
{{ moment(item.updatedAt).fromNow() }}
</span>
</div>
<div class="column is-6 has-text-right" v-if="undefined !== item?.restricted">
<strong>Restricted:</strong> {{ item.restricted ? 'Yes' : 'No' }}
</div>
<div class="column is-6 has-text-right" v-if="undefined !== item?.disabled">
<strong>Disabled:</strong> {{ item.restricted ? 'Yes' : 'No' }}
</div>
<div class="column is-6" v-for="item in items" :key="`users-${item.id}`">
<div class="card">
<header class="card-header">
<p class="card-header-title is-text-overflow">
{{ item.name }}
</p>
<div class="card-header-icon"></div>
</header>
<div class="card-content">
<div class="columns is-mobile is-multiline">
<div class="column is-6">
<strong>Admin:</strong> {{ item.admin ? 'Yes' : 'No' }}
</div> </div>
<div class="column is-6 has-text-right" v-if="undefined !== item?.guest">
<strong>Guest:</strong> {{ item.guest ? 'Yes' : 'No' }}
</div>
<div class="column is-6 has-text-right" v-if="undefined !== item?.hidden">
<strong>Hidden:</strong> {{ item.guest ? 'Yes' : 'No' }}
</div>
<div class="column is-6" v-if="item?.updatedAt">
<strong>Updated:&nbsp;</strong>
<span class="has-tooltip" v-tooltip="moment(item.updatedAt).format(TOOLTIP_DATE_FORMAT)">
{{ moment(item.updatedAt).fromNow() }}
</span>
</div>
<div class="column is-6 has-text-right" v-if="undefined !== item?.restricted">
<strong>Restricted:</strong> {{ item.restricted ? 'Yes' : 'No' }}
</div>
<div class="column is-6 has-text-right" v-if="undefined !== item?.disabled">
<strong>Disabled:</strong> {{ item.restricted ? 'Yes' : 'No' }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> @toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<div class="notification-content content" v-if="show_page_tips"> <div class="notification-content content" v-if="show_page_tips">
<ul> <ul>
<li>For <code>Plex</code> backends, if the <code>X-Plex-Token</code> is limited one, the users will not show <li>For <code>Plex</code> backends, if the <code>X-Plex-Token</code> is limited one, the users will not
up. This is a limitation of the Plex API. show
</li> up. This is a limitation of the Plex API.
</ul> </li>
</div> </ul>
</Message> </div>
</Message>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {notification, TOOLTIP_DATE_FORMAT} from '~/utils/index.js' import moment from 'moment'
import {notification, TOOLTIP_DATE_FORMAT} from '~/utils/index'
import {useStorage} from '@vueuse/core' import {useStorage} from '@vueuse/core'
import request from '~/utils/request.js' import request from '~/utils/request'
import moment from "moment";
const backend = useRoute().params.backend const backend = useRoute().params.backend
const items = ref()
const items = ref({
"id": 14138718,
"uuid": "145bdb152ed42627",
"name": "wowkise",
"admin": true,
"guest": false,
"restricted": false,
"updatedAt": "2024-05-22T12:47:41+03:00"
})
const isLoading = ref(false) const isLoading = ref(false)
const show_page_tips = useStorage('show_page_tips', true) const show_page_tips = useStorage('show_page_tips', true)
const loadContent = async () => { const loadContent = async () => {

View File

@@ -1,169 +1,174 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-server"></i></span> <span class="title is-4">
Backends <span class="icon"><i class="fas fa-server"></i></span>
</span> Backends
<div class="is-pulled-right"> </span>
<div class="field is-grouped"> <div class="is-pulled-right">
<p class="control"> <div class="field is-grouped">
<button class="button is-primary" v-tooltip.bottom="'Add New Backend'" <p class="control">
@click="toggleForm = !toggleForm" :disabled="isLoading"> <button class="button is-primary" v-tooltip.bottom="'Add New Backend'"
<span class="icon"><i class="fas fa-add"></i></span> @click="toggleForm = !toggleForm" :disabled="isLoading">
</button> <span class="icon"><i class="fas fa-add"></i></span>
</p> </button>
<p class="control"> </p>
<button class="button is-info" @click="loadContent" :disabled="isLoading" :class="{'is-loading':isLoading}"> <p class="control">
<span class="icon"><i class="fas fa-sync"></i></span> <button class="button is-info" @click="loadContent" :disabled="isLoading"
</button> :class="{'is-loading':isLoading}">
</p> <span class="icon"><i class="fas fa-sync"></i></span>
</div> </button>
</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">
<BackendAdd @forceExport="e => handleEvents('forceExport', e)" :backends="backends"
@runImport="e => handleEvents('runImport', e)" @addBackend="e => handleEvents('addBackend', e)"/>
</div>
<template v-else>
<div class="column is-12" v-if="backends.length<1">
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Requesting active play sessions. Please wait..."/>
<Message v-else message_class="is-background-warning-80 has-text-dark" title="Warning"
icon="fas fa-exclamation-circle">
No backends found. Please add new backends to start using the tool. You can add new backend by
<NuxtLink @click="toggleForm=true" v-text="'clicking here'"/>
or by clicking the <span class="icon is-clickable" @click="toggleForm=true"><i class="fas fa-add"></i></span>
button above.
</Message>
</div>
<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">
<NuxtLink :to="`/backend/${backend.name}`">
{{ backend.name }}
</NuxtLink>
</p> </p>
<div class="card-header-icon">
<div class="field is-grouped">
<div class="control">
<NuxtLink :to="`/backend/${backend.name}/edit?redirect=/backends`"
v-tooltip="'Edit backend settings'">
<span class="icon has-text-warning"><i class="fas fa-cog"></i></span>
</NuxtLink>
</div>
<div class="control">
<NuxtLink :to="`/backend/${backend.name}/delete?redirect=/backends`" v-tooltip="'Delete backend'">
<span class="icon has-text-danger"><i class="fas fa-trash"></i></span>
</NuxtLink>
</div>
</div>
</div>
</header>
<div class="card-content">
<div class="columns is-multiline has-text-centered">
<div class="column is-6 has-text-left-mobile">
<strong>Last Export:&nbsp;</strong>
<template v-if="backend.export.enabled">
<span v-if="backend.export.lastSync" class="has-tooltip"
v-tooltip="moment(backend.export.lastSync).format(TOOLTIP_DATE_FORMAT)">
{{ moment(backend.export.lastSync).fromNow() }}
</span>
<template v-else>Never</template>
</template>
<template v-else>
<span class="tag is-danger is-light is-pointer-help"
v-tooltip="'Local database is not being sync to this backend.'">Disabled</span>
</template>
</div>
<div class="column is-6 has-text-left-mobile">
<strong>Last Import:&nbsp;</strong>
<template v-if="backend.import.enabled || backend.options?.IMPORT_METADATA_ONLY">
<template v-if="backend.import.lastSync">
<span class="has-tooltip" v-tooltip="moment(backend.import.lastSync).format(TOOLTIP_DATE_FORMAT)">
{{ moment(backend.import.lastSync).fromNow() }}
</span>
<template v-if="!backend.import.enabled && backend.options?.IMPORT_METADATA_ONLY">
&nbsp;
<span class="tag is-warning is-light is-pointer-help"
v-tooltip="'Only metadata being imported from this backend'">
Metadata</span>
</template>
</template>
<template v-else>Never</template>
</template>
<template v-else>
<span class="tag is-danger is-light is-pointer-help"
v-tooltip="'All data import from this backend is disabled.'">Disabled</span>
</template>
</div>
</div>
</div> </div>
<div class="card-footer"> </div>
<div class="card-footer-item"> <div class="is-hidden-mobile">
<div class="field"> <span class="subtitle">This page contains all the backends that are currently configured.</span>
<input :id="backend.name+'_export'" type="checkbox" class="switch is-success"
:checked="backend.export.enabled"
@change="updateValue(backend, 'export.enabled', !backend.export.enabled)">
<label :for="backend.name+'_export'">Export</label>
</div>
</div>
<div class="card-footer-item">
<div class="field">
<input :id="backend.name+'_import'" type="checkbox" class="switch is-success"
:checked="backend.import.enabled"
@change="updateValue(backend, 'import.enabled',!backend.import.enabled)">
<label :for="backend.name+'_import'">Import</label>
</div>
</div>
<div class="card-footer-item">
<NuxtLink :to="api_url + backend.urls.webhook" class="is-info is-light" @click.prevent="copyUrl(backend)">
<span class="icon"><i class="fas fa-copy"></i></span>
<span class="is-hidden-mobile">Copy Webhook URL</span>
<span class="is-hidden-tablet">Webhook</span>
</NuxtLink>
</div>
</div>
<footer class="card-footer">
<div class="card-footer-item is-block">
<div class="control is-fullwidth has-icons-left">
<div class="select is-fullwidth">
<select v-model="selectedCommand" @change="forwardCommand(backend)">
<option value="" disabled>Frequently used commands</option>
<option v-for="(command, index) in usefulCommands" :key="`qc-${index}`" :value="index"
:disabled="false === Boolean(ag(backend, command.state_key, false))">
{{ command.title }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-terminal"></i>
</div>
</div>
</div>
</footer>
</div> </div>
</div> </div>
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <div class="column is-12" v-if="toggleForm">
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> <BackendAdd @forceExport="e => handleEvents('forceExport', e)" :backends="backends"
<ul> @runImport="e => handleEvents('runImport', e)" @addBackend="e => handleEvents('addBackend', e)"/>
<li>
<strong>Import</strong> means pulling data from the backends into the local database.
</li>
<li>
<strong>Export</strong> means pushing data from the local database to the backends.
</li>
</ul>
</Message>
</div> </div>
</template> <template v-else>
<div class="column is-12" v-if="backends.length<1">
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Requesting active play sessions. Please wait..."/>
<Message v-else message_class="is-background-warning-80 has-text-dark" title="Warning"
icon="fas fa-exclamation-circle">
No backends found. Please add new backends to start using the tool. You can add new backend by
<NuxtLink @click="toggleForm=true" v-text="'clicking here'"/>
or by clicking the <span class="icon is-clickable" @click="toggleForm=true"><i
class="fas fa-add"></i></span>
button above.
</Message>
</div>
<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">
<NuxtLink :to="`/backend/${backend.name}`">
{{ backend.name }}
</NuxtLink>
</p>
<div class="card-header-icon">
<div class="field is-grouped">
<div class="control">
<NuxtLink :to="`/backend/${backend.name}/edit?redirect=/backends`"
v-tooltip="'Edit backend settings'">
<span class="icon has-text-warning"><i class="fas fa-cog"></i></span>
</NuxtLink>
</div>
<div class="control">
<NuxtLink :to="`/backend/${backend.name}/delete?redirect=/backends`" v-tooltip="'Delete backend'">
<span class="icon has-text-danger"><i class="fas fa-trash"></i></span>
</NuxtLink>
</div>
</div>
</div>
</header>
<div class="card-content">
<div class="columns is-multiline has-text-centered">
<div class="column is-6 has-text-left-mobile">
<strong>Last Export:&nbsp;</strong>
<template v-if="backend.export.enabled">
<span v-if="backend.export.lastSync" class="has-tooltip"
v-tooltip="moment(backend.export.lastSync).format(TOOLTIP_DATE_FORMAT)">
{{ moment(backend.export.lastSync).fromNow() }}
</span>
<template v-else>Never</template>
</template>
<template v-else>
<span class="tag is-danger is-light is-pointer-help"
v-tooltip="'Local database is not being sync to this backend.'">Disabled</span>
</template>
</div>
<div class="column is-6 has-text-left-mobile">
<strong>Last Import:&nbsp;</strong>
<template v-if="backend.import.enabled || backend.options?.IMPORT_METADATA_ONLY">
<template v-if="backend.import.lastSync">
<span class="has-tooltip" v-tooltip="moment(backend.import.lastSync).format(TOOLTIP_DATE_FORMAT)">
{{ moment(backend.import.lastSync).fromNow() }}
</span>
<template v-if="!backend.import.enabled && backend.options?.IMPORT_METADATA_ONLY">
&nbsp;
<span class="tag is-warning is-light is-pointer-help"
v-tooltip="'Only metadata being imported from this backend'">
Metadata</span>
</template>
</template>
<template v-else>Never</template>
</template>
<template v-else>
<span class="tag is-danger is-light is-pointer-help"
v-tooltip="'All data import from this backend is disabled.'">Disabled</span>
</template>
</div>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<div class="field">
<input :id="backend.name+'_export'" type="checkbox" class="switch is-success"
:checked="backend.export.enabled"
@change="updateValue(backend, 'export.enabled', !backend.export.enabled)">
<label :for="backend.name+'_export'">Export</label>
</div>
</div>
<div class="card-footer-item">
<div class="field">
<input :id="backend.name+'_import'" type="checkbox" class="switch is-success"
:checked="backend.import.enabled"
@change="updateValue(backend, 'import.enabled',!backend.import.enabled)">
<label :for="backend.name+'_import'">Import</label>
</div>
</div>
<div class="card-footer-item">
<NuxtLink :to="api_url + backend.urls.webhook" class="is-info is-light"
@click.prevent="copyUrl(backend)">
<span class="icon"><i class="fas fa-copy"></i></span>
<span class="is-hidden-mobile">Copy Webhook URL</span>
<span class="is-hidden-tablet">Webhook</span>
</NuxtLink>
</div>
</div>
<footer class="card-footer">
<div class="card-footer-item is-block">
<div class="control is-fullwidth has-icons-left">
<div class="select is-fullwidth">
<select v-model="selectedCommand" @change="forwardCommand(backend)">
<option value="" disabled>Frequently used commands</option>
<option v-for="(command, index) in usefulCommands" :key="`qc-${index}`" :value="index"
:disabled="false === Boolean(ag(backend, command.state_key, false))">
{{ command.title }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-terminal"></i>
</div>
</div>
</div>
</footer>
</div>
</div>
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
<strong>Import</strong> means pulling data from the backends into the local database.
</li>
<li>
<strong>Export</strong> means pushing data from the local database to the backends.
</li>
</ul>
</Message>
</div>
</template>
</div>
</div> </div>
</template> </template>

View File

@@ -1,107 +1,110 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-sd-card"></i></span> <span class="title is-4">
Backups <span class="icon"><i class="fas fa-sd-card"></i></span>
</span> Backups
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<button class="button is-primary" @click="queueTask" :disabled="isLoading"
:class="{'is-loading':isLoading, 'is-primary':!queued, 'is-danger':queued}">
<span class="icon"><i class="fas fa-sd-card"></i></span>
<span>{{ !queued ? 'Queue backup' : 'Remove from queue' }}</span>
</button>
</p>
<p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading" :class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
This page contains all of your manually generated and automatic backups.
</span> </span>
</div> <div class="is-pulled-right">
</div> <div class="field is-grouped">
<p class="control">
<div class="column is-12" v-if="items.length < 1 || isLoading"> <button class="button is-primary" @click="queueTask" :disabled="isLoading"
<Message v-if="isLoading" message_class="is-background-info-90 has-text-dark" icon="fas fa-spinner fa-spin" :class="{'is-loading':isLoading, 'is-primary':!queued, 'is-danger':queued}">
title="Loading" message="Loading data. Please wait..."/> <span class="icon"><i class="fas fa-sd-card"></i></span>
<Message v-else title="Warning" message_class="is-background-warning-80 has-text-dark" <span>{{ !queued ? 'Queue backup' : 'Remove from queue' }}</span>
icon="fas fa-exclamation-triangle"> </button>
No backups found. </p>
</Message> <p class="control">
</div> <button class="button is-info" @click="loadContent" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<div class="column is-6-tablet" v-for="(item, index) in items" :key="'backup-'+index"> <span class="icon"><i class="fas fa-sync"></i></span>
<div class="card"> </button>
<header class="card-header"> </p>
<p class="card-header-title is-text-overflow pr-1"> </div>
<span class="icon"><i class="fas fa-download" :class="{'fa-spin':item?.isDownloading}"></i>&nbsp;</span> </div>
<span> <div class="is-hidden-mobile">
<NuxtLink @click="downloadFile(item)" v-text="item.filename"/> <span class="subtitle">
</span> This page contains all of your manually generated and automatic backups.
</p>
<span class="card-header-icon">
<NuxtLink @click="deleteFile(item)" class="has-text-danger" v-tooltip="'Delete this backup file.'">
<span class="icon"><i class="fas fa-trash"></i></span>
</NuxtLink>
</span> </span>
</header> </div>
<div class="card-footer-item"> </div>
<div class="card-footer-item">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span> <div class="column is-12" v-if="items.length < 1 || isLoading">
<span class="has-tooltip" v-tooltip="`Last Update: ${moment(item.date).format(TOOLTIP_DATE_FORMAT)}`"> <Message v-if="isLoading" message_class="is-background-info-90 has-text-dark" icon="fas fa-spinner fa-spin"
{{ moment(item.date).fromNow() }} title="Loading" message="Loading data. Please wait..."/>
<Message v-else title="Warning" message_class="is-background-warning-80 has-text-dark"
icon="fas fa-exclamation-triangle">
No backups found.
</Message>
</div>
<div class="column is-6-tablet" v-for="(item, index) in items" :key="'backup-'+index">
<div class="card">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<span class="icon"><i class="fas fa-download" :class="{'fa-spin':item?.isDownloading}"></i>&nbsp;</span>
<span>
<NuxtLink @click="downloadFile(item)" v-text="item.filename"/>
</span>
</p>
<span class="card-header-icon">
<NuxtLink @click="deleteFile(item)" class="has-text-danger" v-tooltip="'Delete this backup file.'">
<span class="icon"><i class="fas fa-trash"></i></span>
</NuxtLink>
</span> </span>
</div> </header>
<div class="card-footer-item"> <div class="card-footer-item">
<span class="icon"><i class="fas fa-hdd"></i>&nbsp;</span> <div class="card-footer-item">
<span>{{ humanFileSize(item.size) }}</span> <span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
</div> <span class="has-tooltip" v-tooltip="`Last Update: ${moment(item.date).format(TOOLTIP_DATE_FORMAT)}`">
<div class="card-footer-item"> {{ moment(item.date).fromNow() }}
<span class="icon"><i class="fas fa-tag"></i>&nbsp;</span> </span>
<span class="is-capitalized">{{ item.type }}</span> </div>
<div class="card-footer-item">
<span class="icon"><i class="fas fa-hdd"></i>&nbsp;</span>
<span>{{ humanFileSize(item.size) }}</span>
</div>
<div class="card-footer-item">
<span class="icon"><i class="fas fa-tag"></i>&nbsp;</span>
<span class="is-capitalized">{{ item.type }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> @toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul> <ul>
<li> <li>
Backups that are tagged <code>Automatic</code> are subject to auto deletion after <code>9</code> days from Backups that are tagged <code>Automatic</code> are subject to auto deletion after <code>9</code> days from
the date of creation. the date of creation.
</li> </li>
<li> <li>
You can trigger a backup task to run in the background by clicking the You can trigger a backup task to run in the background by clicking the
<code><span class="icon"><i class="fas fa-sd-card"></i></span> Queue backup</code> button. on top right. <code><span class="icon"><i class="fas fa-sd-card"></i></span> Queue backup</code> button. on top right.
Those backups will be tagged as <code>Automatic</code>. Those backups will be tagged as <code>Automatic</code>.
</li> </li>
<li> <li>
To generate a manual backup, you need to use the <code>state:backup</code> command from the console. To generate a manual backup, you need to use the <code>state:backup</code> command from the console.
or by <span class="icon"><i class="fas fa-terminal"></i></span> or by <span class="icon"><i class="fas fa-terminal"></i></span>
<NuxtLink :to="makeConsoleCommand('state:backup -s [backend] --file /config/backup/[file]')" <NuxtLink :to="makeConsoleCommand('state:backup -s [backend] --file /config/backup/[file]')"
v-text="'Web Console'"/> v-text="'Web Console'"/>
page. page.
</li> </li>
</ul> </ul>
</Message> </Message>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import request from '~/utils/request.js' import request from '~/utils/request'
import moment from 'moment' import moment from 'moment'
import {humanFileSize, makeConsoleCommand, notification, TOOLTIP_DATE_FORMAT} from '~/utils/index.js' import {humanFileSize, makeConsoleCommand, notification, TOOLTIP_DATE_FORMAT} from '~/utils/index'
import Message from '~/components/Message.vue' import Message from '~/components/Message'
import {useStorage} from '@vueuse/core' import {useStorage} from '@vueuse/core'
useHead({title: 'Backups'}) useHead({title: 'Backups'})

View File

@@ -1,109 +1,112 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<h1 class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-terminal"></i></span> Console <h1 class="title is-4">
</h1> <span class="icon"><i class="fas fa-terminal"></i></span> Console
<div class="subtitle"> </h1>
You can execute <strong>non-interactive</strong> commands here. <div class="subtitle">
<template v-if="allEnabled"> You can execute <strong>non-interactive</strong> commands here.
The console defaults to <code>console</code> command, if you want to run a different command, prefix <template v-if="allEnabled">
the command with <code>$</code>. For example <code>$ ls</code>. The console defaults to <code>console</code> command, if you want to run a different command, prefix
</template> the command with <code>$</code>. For example <code>$ ls</code>.
<template v-else> </template>
This interface is jailed to <code>console</code> command. <template v-else>
</template> This interface is jailed to <code>console</code> command.
</template>
</div>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12">
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title"> <p class="card-header-title">
<span class="icon"><i class="fas fa-terminal"></i>&nbsp;</span> Terminal <span class="icon"><i class="fas fa-terminal"></i>&nbsp;</span> Terminal
</p> </p>
<p class="card-header-icon"> <p class="card-header-icon">
<span class="icon" @click="clearOutput"><i class="fa fa-broom"></i></span> <span class="icon" @click="clearOutput"><i class="fa fa-broom"></i></span>
</p> </p>
</header> </header>
<section class="card-content p-0 m-0"> <section class="card-content p-0 m-0">
<div ref="outputConsole" style="min-height: 60vh;max-height:70vh;"/> <div ref="outputConsole" style="min-height: 60vh;max-height:70vh;"/>
</section> </section>
<section class="card-content p-1 m-1"> <section class="card-content p-1 m-1">
<div class="field"> <div class="field">
<div class="field-body"> <div class="field-body">
<div class="field is-grouped-tablet"> <div class="field is-grouped-tablet">
<p class="control is-expanded has-icons-left"> <p class="control is-expanded has-icons-left">
<input type="text" class="input is-fullwidth" v-model="command" <input type="text" class="input is-fullwidth" v-model="command"
:placeholder="`system:view ${allEnabled ? 'or $ ls' : ''}`" :placeholder="`system:view ${allEnabled ? 'or $ ls' : ''}`"
list="recent_commands" list="recent_commands"
autocomplete="off" ref="command_input" @keydown.enter="RunCommand" :disabled="isLoading"> autocomplete="off" ref="command_input" @keydown.enter="RunCommand" :disabled="isLoading">
<span class="icon is-left"><i class="fas fa-terminal" :class="{'fa-spin':isLoading}"></i></span> <span class="icon is-left"><i class="fas fa-terminal" :class="{'fa-spin':isLoading}"></i></span>
</p> </p>
<p class="control" v-if="!isLoading"> <p class="control" v-if="!isLoading">
<button class="button is-primary" type="button" :disabled="hasPrefix" @click="RunCommand"> <button class="button is-primary" type="button" :disabled="hasPrefix" @click="RunCommand">
<span class="icon"><i class="fa fa-paper-plane"></i></span> <span class="icon"><i class="fa fa-paper-plane"></i></span>
</button> </button>
</p> </p>
<p class="control" v-if="isLoading"> <p class="control" v-if="isLoading">
<button class="button is-danger" type="button" @click="finished" v-tooltip="'Close connection.'"> <button class="button is-danger" type="button" @click="finished" v-tooltip="'Close connection.'">
<span class="icon"><i class="fa fa-power-off"></i></span> <span class="icon"><i class="fa fa-power-off"></i></span>
</button> </button>
</p> </p>
</div>
</div> </div>
<p class="help" v-if="hasPrefix">
<span class="icon-text">
<span class="icon has-text-danger"><i class="fas fa-exclamation-triangle"></i></span>
<span>Remove the <code>console</code> or <code>docker exec -ti watchstate console</code> from the
input. You should use the command directly, For example i.e <code>db:list --output
yaml</code></span>
</span>
</p>
<p class="help" v-if="hasPlaceholder">
<span class="icon-text">
<span class="icon has-text-warning"><i class="fas fa-exclamation-circle"></i></span>
<span>The command contains <code>[...]</code> which are considered a placeholder, So, please replace
<code>[...]</code> with the intended value if applicable.</span>
</span>
</p>
</div> </div>
<p class="help" v-if="hasPrefix"> </section>
<span class="icon-text"> </div>
<span class="icon has-text-danger"><i class="fas fa-exclamation-triangle"></i></span>
<span>Remove the <code>console</code> or <code>docker exec -ti watchstate console</code> from the
input. You should use the command directly, For example i.e <code>db:list --output yaml</code></span>
</span>
</p>
<p class="help" v-if="hasPlaceholder">
<span class="icon-text">
<span class="icon has-text-warning"><i class="fas fa-exclamation-circle"></i></span>
<span>The command contains <code>[...]</code> which are considered a placeholder, So, please replace
<code>[...]</code> with the intended value if applicable.</span>
</span>
</p>
</div>
</section>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> @toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul> <ul>
<li> <li>
You can also run a command from the task page by clicking on the <strong>Run via console</strong>. The You can also run a command from the task page by clicking on the <strong>Run via console</strong>. The
command will be pre-filled for you. command will be pre-filled for you.
</li> </li>
<li> <li>
Clicking close connection does not stop the command. It only stops the output from being displayed. The 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. command will continue to run until it finishes.
</li> </li>
<li> <li>
The majority of the commands will not show any output unless error has occurred or important information The majority of the commands will not show any output unless error has occurred or important information
needs to be communicated. To see all output, add the <code>-vvv</code> flag. needs to be communicated. To see all output, add the <code>-vvv</code> flag.
</li> </li>
<li> <li>
There is no need to write <code>console</code> or <code>docker exec -ti watchstate console</code> Using There is no need to write <code>console</code> or <code>docker exec -ti watchstate console</code> Using
this interface. Use the command followed by the options directly. For example, <code>db:list --output this interface. Use the command followed by the options directly. For example, <code>db:list --output
yaml</code>. yaml</code>.
</li> </li>
<li> <li>
There is an environment variable <code>WS_CONSOLE_ENABLE_ALL</code> that can be set to <code>true</code> There is an environment variable <code>WS_CONSOLE_ENABLE_ALL</code> that can be set to <code>true</code>
to enable all commands to be run from the console. This is disabled by default. to enable all commands to be run from the console. This is disabled by default.
</li> </li>
<li>To clear the recent commands auto-suggestions, you can use the <code>clear_ac</code> command.</li> <li>To clear the recent commands auto-suggestions, you can use the <code>clear_ac</code> command.</li>
</ul> </ul>
</Message> </Message>
</div> </div>
<datalist id="recent_commands"> <datalist id="recent_commands">
<option v-for="item in recentCommands" :key="item" :value="item"/> <option v-for="item in recentCommands" :key="item" :value="item"/>
</datalist> </datalist>
</div>
</div> </div>
</template> </template>

View File

@@ -1,212 +1,215 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span id="env_page_title" class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-cogs"></i></span> <span id="env_page_title" class="title is-4">
Environment variables <span class="icon"><i class="fas fa-cogs"></i></span>
</span> Environment variables
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<button class="button is-primary" v-tooltip.bottom="'Add new variable'" @click="toggleForm = !toggleForm"
:disabled="isLoading">
<span class="icon">
<i class="fas fa-add"></i>
</span>
</button>
</p>
<p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading || toggleForm"
:class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
This page allow you alter the environment variables that are used to configure the application.
</span> </span>
</div> <div class="is-pulled-right">
</div> <div class="field is-grouped">
<p class="control">
<div class="column is-12" v-if="!toggleForm && items.length < 1"> <button class="button is-primary" v-tooltip.bottom="'Add new variable'" @click="toggleForm = !toggleForm"
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading" :disabled="isLoading">
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/> <span class="icon">
<Message v-else message_class="has-background-warning-90 has-text-dark" title="Information" <i class="fas fa-add"></i>
icon="fas fa-info-circle">
<p>
No environment variables configured yet. Click on the
<i @click="toggleForm=true" class="is-clickable fa fa-add"></i> button to add a new variable.
</p>
</Message>
</div>
<div class="column is-12" v-if="toggleForm">
<form id="env_add_form" @submit.prevent="addVariable">
<div class="card">
<header class="card-header">
<p class="card-header-title is-unselectable is-justify-center">Manage Environment Variable</p>
</header>
<div class="card-content">
<div class="field">
<label class="label is-unselectable" for="form_key">Environment key</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="form_key" id="form_key" @change="keyChanged">
<option value="" disabled>Select Key</option>
<option v-for="item in items" :key="`opt-${item.key}`" :value="item.key">
{{ item.key }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-key"></i>
</div>
</div>
</div>
<div class="field">
<label class="label is-unselectable" for="form_value">Environment value</label>
<div class="control has-icons-left">
<template v-if="'bool' === form_type">
<input id="form_value" type="checkbox" class="switch is-success"
:checked="fixBool(form_value)" @change="form_value = !fixBool(form_value)">
<label for="form_value">
<template v-if="fixBool(form_value)">On (True)</template>
<template v-else>Off (False)</template>
</label>
</template>
<template v-else-if=" 'int' === form_type ">
<input class="input" id="form_value" type="number" placeholder="Value" v-model="form_value"
pattern="[0-9]*" inputmode="numeric">
<div class="icon is-small is-left">
<i class="fas fa-font"></i>
</div>
</template>
<template v-else>
<input class="input" id="form_value" type="text" placeholder="Value" v-model="form_value">
<div class="icon is-small is-left"><i class="fas fa-font"></i></div>
</template>
<div>
<p class="help" v-html="getHelp(form_key)"></p>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<button class="button is-fullwidth is-primary" type="submit" :disabled="!form_key || '' === form_value">
<span class="icon-text">
<span class="icon"><i class="fas fa-save"></i></span>
<span>Save</span>
</span> </span>
</button> </button>
</div> </p>
<div class="card-footer-item"> <p class="control">
<button class="button is-fullwidth is-danger" type="button" @click="cancelForm"> <button class="button is-info" @click="loadContent" :disabled="isLoading || toggleForm"
<span class="icon-text"> :class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-cancel"></i></span> <span class="icon"><i class="fas fa-sync"></i></span>
<span>Cancel</span>
</span>
</button> </button>
</div> </p>
</div> </div>
</div> </div>
</form> <div class="is-hidden-mobile">
</div> <span class="subtitle">
<div v-else class="column is-12" v-if="items"> This page allow you alter the environment variables that are used to configure the application.
<div class="columns is-multiline"> </span>
<div class="column" v-for="item in filteredRows(items)" :key="item.key" </div>
:class="{'is-4':!item?.danger,'is-12':item.danger}"> </div>
<div class="card" :class="{ 'is-danger': item?.danger }">
<div class="column is-12" v-if="!toggleForm && items.length < 1">
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
<Message v-else message_class="has-background-warning-90 has-text-dark" title="Information"
icon="fas fa-info-circle">
<p>
No environment variables configured yet. Click on the
<i @click="toggleForm=true" class="is-clickable fa fa-add"></i> button to add a new variable.
</p>
</Message>
</div>
<div class="column is-12" v-if="toggleForm">
<form id="env_add_form" @submit.prevent="addVariable">
<div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title is-unselectable"> <p class="card-header-title is-unselectable is-justify-center">Manage Environment Variable</p>
<template v-if="item?.danger">
<span class="title is-5 ">
<span class="icon" v-tooltip="'This option is considered dangerous.'">
<i class="has-text-danger fas fa-exclamation-triangle"></i>&nbsp;
</span> {{ item.key }}
</span>
</template>
<template v-else>
<span class="has-tooltip is-clickable" v-tooltip="item.description">
{{ item.key }}
</span>
</template>
</p>
<span class="card-header-icon" v-if="item.mask" @click="item.mask = false" v-tooltip="'Unmask the value'">
<span class="icon"><i class="fas fa-unlock"></i></span>
</span>
</header> </header>
<div class="card-content"> <div class="card-content">
<div class="content"> <div class="field">
<p v-if="'bool' === item.type"> <label class="label is-unselectable" for="form_key">Environment key</label>
<span class="icon-text"> <div class="control has-icons-left">
<span class="icon"> <div class="select is-fullwidth">
<i class="fas fa-toggle-on has-text-primary" v-if="fixBool(item.value)"></i> <select v-model="form_key" id="form_key" @change="keyChanged">
<i class="fas fa-toggle-off" v-else></i> <option value="" disabled>Select Key</option>
</span> <option v-for="item in items" :key="`opt-${item.key}`" :value="item.key">
<span>{{ fixBool(item.value) ? 'On (True)' : 'Off (False)' }}</span> {{ item.key }}
</span> </option>
</p> </select>
<p v-else class="is-text-overflow is-clickable is-unselectable" </div>
:class="{ 'is-masked': item.mask, 'is-unselectable': item.mask }" <div class="icon is-left">
@click="(e) => e.target.classList.toggle('is-text-overflow')"> <i class="fas fa-key"></i>
{{ item.value }}</p> </div>
</div>
</div>
<p v-if="item?.danger" class="title is-5 has-text-danger"> <div class="field">
{{ item.description }} <label class="label is-unselectable" for="form_value">Environment value</label>
</p> <div class="control has-icons-left">
<template v-if="'bool' === form_type">
<input id="form_value" type="checkbox" class="switch is-success"
:checked="fixBool(form_value)" @change="form_value = !fixBool(form_value)">
<label for="form_value">
<template v-if="fixBool(form_value)">On (True)</template>
<template v-else>Off (False)</template>
</label>
</template>
<template v-else-if=" 'int' === form_type ">
<input class="input" id="form_value" type="number" placeholder="Value" v-model="form_value"
pattern="[0-9]*" inputmode="numeric">
<div class="icon is-small is-left">
<i class="fas fa-font"></i>
</div>
</template>
<template v-else>
<input class="input" id="form_value" type="text" placeholder="Value" v-model="form_value">
<div class="icon is-small is-left"><i class="fas fa-font"></i></div>
</template>
<div>
<p class="help" v-html="getHelp(form_key)"></p>
</div>
</div>
</div> </div>
</div> </div>
<footer class="card-footer"> <div class="card-footer">
<div class="card-footer-item"> <div class="card-footer-item">
<button class="button is-primary is-fullwidth" @click="editEnv(item)"> <button class="button is-fullwidth is-primary" type="submit" :disabled="!form_key || '' === form_value">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-edit"></i></span> <span class="icon"><i class="fas fa-save"></i></span>
<span>Edit</span> <span>Save</span>
</span> </span>
</button> </button>
</div> </div>
<div class="card-footer-item"> <div class="card-footer-item">
<button class="button is-fullwidth is-warning" @click="copyText(item.value)"> <button class="button is-fullwidth is-danger" type="button" @click="cancelForm">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-copy"></i></span> <span class="icon"><i class="fas fa-cancel"></i></span>
<span>Copy</span> <span>Cancel</span>
</span> </span>
</button> </button>
</div> </div>
<div class="card-footer-item"> </div>
<button class="button is-fullwidth is-danger" @click="deleteEnv(item)"> </div>
<span class="icon-text"> </form>
<span class="icon"><i class="fas fa-trash"></i></span> </div>
<span>Delete</span> <div v-else class="column is-12" v-if="items">
</span> <div class="columns is-multiline">
</button> <div class="column" v-for="item in filteredRows(items)" :key="item.key"
:class="{'is-4':!item?.danger,'is-12':item.danger}">
<div class="card" :class="{ 'is-danger': item?.danger }">
<header class="card-header">
<p class="card-header-title is-unselectable">
<template v-if="item?.danger">
<span class="title is-5 ">
<span class="icon" v-tooltip="'This option is considered dangerous.'">
<i class="has-text-danger fas fa-exclamation-triangle"></i>&nbsp;
</span> {{ item.key }}
</span>
</template>
<template v-else>
<span class="has-tooltip is-clickable" v-tooltip="item.description">
{{ item.key }}
</span>
</template>
</p>
<span class="card-header-icon" v-if="item.mask" @click="item.mask = false"
v-tooltip="'Unmask the value'">
<span class="icon"><i class="fas fa-unlock"></i></span>
</span>
</header>
<div class="card-content">
<div class="content">
<p v-if="'bool' === item.type">
<span class="icon-text">
<span class="icon">
<i class="fas fa-toggle-on has-text-primary" v-if="fixBool(item.value)"></i>
<i class="fas fa-toggle-off" v-else></i>
</span>
<span>{{ fixBool(item.value) ? 'On (True)' : 'Off (False)' }}</span>
</span>
</p>
<p v-else class="is-text-overflow is-clickable is-unselectable"
:class="{ 'is-masked': item.mask, 'is-unselectable': item.mask }"
@click="(e) => e.target.classList.toggle('is-text-overflow')">
{{ item.value }}</p>
<p v-if="item?.danger" class="title is-5 has-text-danger">
{{ item.description }}
</p>
</div>
</div> </div>
</footer> <footer class="card-footer">
<div class="card-footer-item">
<button class="button is-primary is-fullwidth" @click="editEnv(item)">
<span class="icon-text">
<span class="icon"><i class="fas fa-edit"></i></span>
<span>Edit</span>
</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-fullwidth is-warning" @click="copyText(item.value)">
<span class="icon-text">
<span class="icon"><i class="fas fa-copy"></i></span>
<span>Copy</span>
</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-fullwidth is-danger" @click="deleteEnv(item)">
<span class="icon-text">
<span class="icon"><i class="fas fa-trash"></i></span>
<span>Delete</span>
</span>
</button>
</div>
</footer>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> @toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul> <ul>
<li>Some variables values are masked, to unmask them click on icon <i class="fa fa-unlock"></i>.</li> <li>Some variables values are masked, to unmask them click on icon <i class="fa fa-unlock"></i>.</li>
<li>Some values are too large to fit into the view, clicking on the value will show the full value.</li> <li>Some values are too large to fit into the view, clicking on the value will show the full value.</li>
<li>These values are loaded from the <code>{{ file }}</code> file.</li> <li>These values are loaded from the <code>{{ file }}</code> file.</li>
<li>To add a new variable click on the <i class="fa fa-add"></i> button.</li> <li>To add a new variable click on the <i class="fa fa-add"></i> button.</li>
<li>Environment variables with <span class="has-text-danger">red borders</span> and <i <li>Environment variables with <span class="has-text-danger">red borders</span> and <i
class="fas fa-exclamation-triangle"></i> icon are considered class="fas fa-exclamation-triangle"></i> icon are considered
dangerous. Please be careful when editing them. dangerous. Please be careful when editing them.
</li> </li>
</ul> </ul>
</Message> </Message>
</div> </div>
</div>
</div> </div>
</template> </template>

View File

@@ -1,291 +1,295 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-list-check"></i></span> <span class="title is-4">
Events <span class="icon"><i class="fas fa-list-check"></i></span>
</span> Events
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<button class="button is-info" @click.prevent="loadContent">
<span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
This page will show events that are queued to be handled or sent to the backends.
</span> </span>
</div> <div class="is-pulled-right">
</div> <div class="field is-grouped">
<p class="control">
<div class="column is-12" v-if="queue.length < 1 && progress.length < 1 && requests.length < 1"> <button class="button is-info" @click.prevent="loadContent">
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading" <span class="icon"><i class="fas fa-sync"></i></span>
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/> </button>
<Message v-else message_class="is-background-success-90 has-text-dark" title="Information" </p>
icon="fas fa-info-circle" </div>
message="There are currently no queued events."/> </div>
</div> <div class="is-hidden-mobile">
</div> <span class="subtitle">
This page will show events that are queued to be handled or sent to the backends.
<div class="columns is-multiline" v-if="queue.length > 0">
<div class="column is-12">
<span class="title is-5">
<span class="icon"><i class="fas fa-eye"></i></span>
State events
</span>
<div class="subtitle is-hidden-mobile">
Events that are changing the play state. Consumed by <code>state:push</code> task.
</div>
</div>
<div class="column is-4 is-6-tablet" v-for="i in queue" :key="`queue-${i.key}`">
<div class="card" :class="{ 'is-success': i.item.watched }">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<span class="icon">
<i class="fas" :class="{'fa-eye-slash': !i.item.watched,'fa-eye': i.item.watched}"></i>&nbsp;
</span>
<NuxtLink :to="'/history/'+i.item.id" v-text="makeName(i.item)"/>
</p>
<span class="card-header-icon">
<button class="button is-danger is-small" @click="deleteItem(i.item, 'queue', i.key)">
<span class="icon"><i class="fas fa-trash"></i></span>
</button>
</span> </span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-12 has-text-left" v-if="i.item?.content_title">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i>&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle',i.item.content_title)" v-text="i.item.content_title"/>
</div>
</div>
<div class="column is-12 has-text-left" v-if="i.item?.content_path">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i>&nbsp;</span>
<NuxtLink :to="makeSearchLink('path',i.item.content_path)" v-text="i.item.content_path"/>
</div>
</div>
<div class="column is-12 has-text-left" v-if="i.item?.progress">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-bars-progress"></i></span>
{{ formatDuration(i.item.progress) }}
</div>
</div>
</div>
</div> </div>
<div class="card-footer has-text-centered"> </div>
<div class="card-footer-item">
<div class="is-text-overflow"> <div class="column is-12" v-if="queue.length < 1 && progress.length < 1 && requests.length < 1">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span> <Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
<span class="has-tooltip" icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
v-tooltip="`${getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated_at)).format(TOOLTIP_DATE_FORMAT)}`"> <Message v-else message_class="is-background-success-90 has-text-dark" title="Information"
{{ getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated_at)).fromNow() }} icon="fas fa-info-circle"
message="There are currently no queued events."/>
</div>
</div>
<div class="columns is-multiline" v-if="queue.length > 0">
<div class="column is-12">
<span class="title is-5">
<span class="icon"><i class="fas fa-eye"></i></span>
State events
</span>
<div class="subtitle is-hidden-mobile">
Events that are changing the play state. Consumed by <code>state:push</code> task.
</div>
</div>
<div class="column is-4 is-6-tablet" v-for="i in queue" :key="`queue-${i.key}`">
<div class="card" :class="{ 'is-success': i.item.watched }">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<span class="icon">
<i class="fas" :class="{'fa-eye-slash': !i.item.watched,'fa-eye': i.item.watched}"></i>&nbsp;
</span> </span>
</div> <NuxtLink :to="'/history/'+i.item.id" v-text="makeName(i.item)"/>
</div> </p>
<div class="card-footer-item"> <span class="card-header-icon">
<div class="is-text-overflow"> <button class="button is-danger is-small" @click="deleteItem(i.item, 'queue', i.key)">
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span> <span class="icon"><i class="fas fa-trash"></i></span>
<NuxtLink :to="'/backend/'+i.item.via" v-text="i.item.via"/> </button>
</div>
</div>
<div class="card-footer-item">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span>
<span>{{ i.item.event ?? '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="columns is-multiline" v-if="progress.length > 0">
<div class="column is-12">
<span class="title is-5">
<span class="icon"><i class="fas fa-bars-progress"></i></span>
Watch progress events
</span>
<div class="subtitle is-hidden-mobile">
Events that are changing the play progress. Consumed by <code>state:progress</code> task.
</div>
</div>
<div class="column is-4 is-6-tablet" v-for="i in progress" :key="`progress-${i.key}`">
<div class="card" :class="{ 'is-success': i.item.watched }">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<span class="icon">
<i class="fas" :class="{'fa-eye-slash': !i.item.watched,'fa-eye': i.item.watched}"></i>&nbsp;
</span> </span>
<NuxtLink :to="'/history/'+i.item.id" v-text="makeName(i.item)"/> </header>
</p> <div class="card-content">
<span class="card-header-icon"> <div class="columns is-multiline is-mobile has-text-centered">
<button class="button is-danger is-small" @click="deleteItem(i.item, 'progress', i.key)"> <div class="column is-12 has-text-left" v-if="i.item?.content_title">
<span class="icon"><i class="fas fa-trash"></i></span> <div class="is-text-overflow">
</button> <span class="icon"><i class="fas fa-heading"></i>&nbsp;</span>
</span> <NuxtLink :to="makeSearchLink('subtitle',i.item.content_title)" v-text="i.item.content_title"/>
</header> </div>
<div class="card-content"> </div>
<div class="columns is-multiline is-mobile has-text-centered"> <div class="column is-12 has-text-left" v-if="i.item?.content_path">
<div class="column is-12 has-text-left" v-if="i.item?.content_title"> <div class="is-text-overflow">
<div class="is-text-overflow"> <span class="icon"><i class="fas fa-file"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-heading"></i>&nbsp;</span> <NuxtLink :to="makeSearchLink('path',i.item.content_path)" v-text="i.item.content_path"/>
<NuxtLink :to="makeSearchLink('subtitle',i.item.content_title)" v-text="i.item.content_title"/> </div>
</div>
<div class="column is-12 has-text-left" v-if="i.item?.progress">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-bars-progress"></i></span>
{{ formatDuration(i.item.progress) }}
</div>
</div> </div>
</div> </div>
<div class="column is-12 has-text-left" v-if="i.item?.content_path"> </div>
<div class="card-footer has-text-centered">
<div class="card-footer-item">
<div class="is-text-overflow"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i>&nbsp;</span> <span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<NuxtLink :to="makeSearchLink('path',i.item.content_path)" v-text="i.item.content_path"/> <span class="has-tooltip"
v-tooltip="`${getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated_at)).format(TOOLTIP_DATE_FORMAT)}`">
{{ getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated_at)).fromNow() }}
</span>
</div> </div>
</div> </div>
<div class="column is-6 has-text-left"> <div class="card-footer-item">
<div class="is-text-overflow"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-info-circle"></i></span> <span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
is Tainted: {{ i.item?.isTainted ? 'Yes' : 'No' }} <NuxtLink :to="'/backend/'+i.item.via" v-text="i.item.via"/>
</div> </div>
</div> </div>
<div class="column is-6 has-text-right" v-if="i.item?.progress"> <div class="card-footer-item">
<div class="is-text-overflow"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-bars-progress"></i></span> <span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span>
{{ formatDuration(i.item.progress) }} <span>{{ i.item.event ?? '-' }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="card-footer has-text-centered"> </div>
<div class="card-footer-item">
<div class="is-text-overflow"> </div>
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip" <div class="columns is-multiline" v-if="progress.length > 0">
v-tooltip="`${getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated_at)).format(TOOLTIP_DATE_FORMAT)}`"> <div class="column is-12">
{{ getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated_at)).fromNow() }} <span class="title is-5">
<span class="icon"><i class="fas fa-bars-progress"></i></span>
Watch progress events
</span>
<div class="subtitle is-hidden-mobile">
Events that are changing the play progress. Consumed by <code>state:progress</code> task.
</div>
</div>
<div class="column is-4 is-6-tablet" v-for="i in progress" :key="`progress-${i.key}`">
<div class="card" :class="{ 'is-success': i.item.watched }">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<span class="icon">
<i class="fas" :class="{'fa-eye-slash': !i.item.watched,'fa-eye': i.item.watched}"></i>&nbsp;
</span> </span>
</div> <NuxtLink :to="'/history/'+i.item.id" v-text="makeName(i.item)"/>
</div> </p>
<div class="card-footer-item"> <span class="card-header-icon">
<div class="is-text-overflow"> <button class="button is-danger is-small" @click="deleteItem(i.item, 'progress', i.key)">
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span> <span class="icon"><i class="fas fa-trash"></i></span>
<NuxtLink :to="'/backend/'+i.item.via" v-text="i.item.via"/> </button>
</div>
</div>
<div class="card-footer-item">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span>
<span>{{ i.item.event ?? '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="columns is-multiline" v-if="requests.length > 0">
<div class="column is-12">
<span class="title is-5 is-unselectable">
<span class="icon"><i class="fas fa-envelope"></i></span>
Request events
</span>
<div class="subtitle is-hidden-mobile">
Events from backends. Consumed by <code>state:requests</code> task.
</div>
</div>
<div class="column is-4 is-6-tablet" v-for="i in requests" :key="`requests-${i.key}`">
<div class="card" :class="{ 'is-success': i.item.watched }">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<span class="icon">
<i class="fas" :class="{'fa-eye-slash': !i.item.watched, 'fa-eye': i.item.watched}"></i>&nbsp;
</span> </span>
<NuxtLink :to="'/history/'+i.item.id" v-text="makeName(i.item)" v-if="i.item.id"/> </header>
<template v-else>{{ makeName(i.item) }}</template> <div class="card-content">
</p> <div class="columns is-multiline is-mobile has-text-centered">
<span class="card-header-icon"> <div class="column is-12 has-text-left" v-if="i.item?.content_title">
<button class="button is-danger is-small" @click="deleteItem(i.item, 'requests', i.key)"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-trash"></i></span> <span class="icon"><i class="fas fa-heading"></i>&nbsp;</span>
</button> <NuxtLink :to="makeSearchLink('subtitle',i.item.content_title)" v-text="i.item.content_title"/>
</span> </div>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-12 has-text-left" v-if="i.item?.content_title">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i>&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle',i.item.content_title)" v-text="i.item.content_title"/>
</div> </div>
</div> <div class="column is-12 has-text-left" v-if="i.item?.content_path">
<div class="column is-12 has-text-left" v-if="i.item?.content_path"> <div class="is-text-overflow">
<div class="is-text-overflow"> <span class="icon"><i class="fas fa-file"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-file"></i>&nbsp;</span> <NuxtLink :to="makeSearchLink('path',i.item.content_path)" v-text="i.item.content_path"/>
<NuxtLink :to="makeSearchLink('path',i.item.content_path)" v-text="i.item.content_path"/> </div>
</div> </div>
</div> <div class="column is-6 has-text-left">
<div class="column is-6 has-text-left"> <div class="is-text-overflow">
<div class="is-text-overflow"> <span class="icon"><i class="fas fa-info-circle"></i></span>
<span class="icon"><i class="fas fa-info-circle"></i></span> is Tainted: {{ i.item?.isTainted ? 'Yes' : 'No' }}
is Tainted: {{ i.item?.isTainted ? 'Yes' : 'No' }} </div>
</div> </div>
</div> <div class="column is-6 has-text-right" v-if="i.item?.progress">
<div class="column is-6 has-text-right" v-if="i.item?.progress"> <div class="is-text-overflow">
<div class="is-text-overflow"> <span class="icon"><i class="fas fa-bars-progress"></i></span>
<span class="icon"><i class="fas fa-bars-progress"></i></span> {{ formatDuration(i.item.progress) }}
{{ formatDuration(i.item.progress) }} </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="card-footer has-text-centered">
<div class="card-footer has-text-centered"> <div class="card-footer-item">
<div class="card-footer-item"> <div class="is-text-overflow">
<div class="is-text-overflow"> <span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span> <span class="has-tooltip"
<span class="has-tooltip" v-tooltip="`${getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated_at)).format(TOOLTIP_DATE_FORMAT)}`">
v-tooltip="`${getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated)).format(TOOLTIP_DATE_FORMAT)}`"> {{ getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated_at)).fromNow() }}
{{ getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated)).fromNow() }} </span>
</span> </div>
</div> </div>
</div> <div class="card-footer-item">
<div class="card-footer-item"> <div class="is-text-overflow">
<div class="is-text-overflow"> <span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span> <NuxtLink :to="'/backend/'+i.item.via" v-text="i.item.via"/>
<NuxtLink :to="'/backend/'+i.item.via" v-text="i.item.via"/> </div>
</div> </div>
</div> <div class="card-footer-item">
<div class="card-footer-item"> <div class="is-text-overflow">
<div class="is-text-overflow"> <span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span> <span>{{ i.item.event ?? '-' }}</span>
<span>{{ i.item.event ?? '-' }}</span> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="columns is-multiline"> <div class="columns is-multiline" v-if="requests.length > 0">
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <span class="title is-5 is-unselectable">
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> <span class="icon"><i class="fas fa-envelope"></i></span>
<ul> Request events
<li> </span>
Events marked with <code>is Tainted: Yes</code>, are interesting but are too chaotic to be useful be used to <div class="subtitle is-hidden-mobile">
determine play state. However, we do use them to update local metadata & play progress. Events from backends. Consumed by <code>state:requests</code> task.
</li> </div>
<li> </div>
Events marked with <code>is Tainted: No</code>, are events that are used to determine play state.
</li> <div class="column is-4 is-6-tablet" v-for="i in requests" :key="`requests-${i.key}`">
<li> <div class="card" :class="{ 'is-success': i.item.watched }">
If you are fast enough, you might be able to see the event before it is consumed by the backend. which allow <header class="card-header">
you to delete it from the queue if you desire. <p class="card-header-title is-text-overflow pr-1">
</li> <span class="icon">
</ul> <i class="fas" :class="{'fa-eye-slash': !i.item.watched, 'fa-eye': i.item.watched}"></i>&nbsp;
</Message> </span>
<NuxtLink :to="'/history/'+i.item.id" v-text="makeName(i.item)" v-if="i.item.id"/>
<template v-else>{{ makeName(i.item) }}</template>
</p>
<span class="card-header-icon">
<button class="button is-danger is-small" @click="deleteItem(i.item, 'requests', i.key)">
<span class="icon"><i class="fas fa-trash"></i></span>
</button>
</span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-12 has-text-left" v-if="i.item?.content_title">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i>&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle',i.item.content_title)" v-text="i.item.content_title"/>
</div>
</div>
<div class="column is-12 has-text-left" v-if="i.item?.content_path">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i>&nbsp;</span>
<NuxtLink :to="makeSearchLink('path',i.item.content_path)" v-text="i.item.content_path"/>
</div>
</div>
<div class="column is-6 has-text-left">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-info-circle"></i></span>
is Tainted: {{ i.item?.isTainted ? 'Yes' : 'No' }}
</div>
</div>
<div class="column is-6 has-text-right" v-if="i.item?.progress">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-bars-progress"></i></span>
{{ formatDuration(i.item.progress) }}
</div>
</div>
</div>
</div>
<div class="card-footer has-text-centered">
<div class="card-footer-item">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`${getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated)).format(TOOLTIP_DATE_FORMAT)}`">
{{ getMoment(ag(i.item.extra, `${i.item.via}.received_at`, i.item.updated)).fromNow() }}
</span>
</div>
</div>
<div class="card-footer-item">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
<NuxtLink :to="'/backend/'+i.item.via" v-text="i.item.via"/>
</div>
</div>
<div class="card-footer-item">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span>
<span>{{ i.item.event ?? '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="columns is-multiline">
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
Events marked with <code>is Tainted: Yes</code>, are interesting but are too chaotic to be useful be used
to
determine play state. However, we do use them to update local metadata & play progress.
</li>
<li>
Events marked with <code>is Tainted: No</code>, are events that are used to determine play state.
</li>
<li>
If you are fast enough, you might be able to see the event before it is consumed by the backend. which
allow
you to delete it from the queue if you desire.
</li>
</ul>
</Message>
</div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,467 +1,469 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix">
<span class="is-unselectable"> <span class="title is-4">
<span class="icon"><i class="fas fa-history"></i>&nbsp;</span> <span class="is-unselectable">
<NuxtLink to="/history">History</NuxtLink> <span class="icon"><i class="fas fa-history"></i>&nbsp;</span>
: </span>{{ headerTitle }} <NuxtLink to="/history">History</NuxtLink>
</span> : </span>{{ headerTitle }}
<div class="is-pulled-right" v-if="data?.via">
<div class="field is-grouped">
<p class="control">
<button class="button" @click="toggleWatched"
:class="{ 'is-success': !data.watched, 'is-danger': data.watched }"
v-tooltip.bottom="'Toggle watch state'">
<span class="icon">
<i class="fas" :class="{'fa-eye-slash':data.watched,'fa-eye':!data.watched}"></i>
</span>
</button>
</p>
<p class="control">
<button class="button is-danger" @click="deleteItem(data)" v-tooltip.bottom="'Delete the record'"
:disabled="isDeleting || isLoading" :class="{'is-loading':isDeleting}">
<span class="icon"><i class="fas fa-trash"></i></span>
</button>
</p>
<p class="control">
<button class="button is-info" @click="loadContent(id)" :class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</div>
</div>
<div class="subtitle is-5" v-if="data?.via && data.content_title">
<span class="is-unselectable icon">
<i class="fas fa-tv" :class="{ 'fa-tv': 'episode' === data.type, 'fa-film': 'movie' === data.type }"></i>
</span> </span>
{{ data?.content_title }} <div class="is-pulled-right" v-if="data?.via">
</div> <div class="field is-grouped">
</div> <p class="control">
<button class="button" @click="toggleWatched"
<div class="column is-12" v-if="!data?.via && isLoading"> :class="{ 'is-success': !data.watched, 'is-danger': data.watched }"
<Message message_class="has-background-info-90 has-text-dark" title="Loading" v-tooltip.bottom="'Toggle watch state'">
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
</div>
<div class="column is-12" v-if="data?.not_reported_by && data.not_reported_by.length>0">
<Message message_class="has-background-warning-80 has-text-dark" icon="fas fa-exclamation-triangle"
:toggle="show_history_page_warning" title="Warning" :use-toggle="true"
@toggle="show_history_page_warning=!show_history_page_warning">
<p>
<span class="icon"><i class="fas fa-exclamation"></i></span>
There are no metadata regarding this <strong>{{ data.type }}</strong> from (
<span class="tag mr-1 has-text-dark" v-for="backend in data.not_reported_by" :key="`nr-${backend}`">
<NuxtLink :to="`/backend/${backend}`" v-text="backend"/>
</span>).
</p>
<h5 class="has-text-dark">
<span class="icon-text">
<span class="icon"><i class="fas fa-question-circle"></i></span>
<span>Possible reasons</span>
</span>
</h5>
<ul>
<li>Delayed import operation. Might be yet to be imported due to webhooks not being used, or the backend
doesn't support webhooks.
</li>
<li>Item mismatched at the source backend.</li>
<li>
There are no matching <code>{{ 'episode' === data.type ? 'Series GUIDs' : 'GUIDs' }}</code> in common
being reported, And thus it was treated as separate item.
</li>
</ul>
</Message>
</div>
<div class="column is-12" v-if="data?.via">
<div class="card" :class="{ 'is-success': parseInt(data.watched) }">
<header class="card-header">
<div class="card-header-title is-clickable is-unselectable" @click="data._toggle = !data._toggle">
<span class="icon">
<i class="fas" :class="{'fa-arrow-up': data?._toggle, 'fa-arrow-down': !data?._toggle}"></i>
</span>
<span>Latest local metadata via</span>
</div>
<div class="card-header-icon">
<span class="icon-text">
<span class="icon"><i class="fas fa-server"></i></span>
<span>
<NuxtLink :to="`/backend/${data.via}`" v-text="data.via"/>
</span>
</span>
</div>
</header>
<div class="card-content" v-if="data?._toggle">
<div class="columns is-multiline is-mobile">
<div class="column is-6">
<span class="icon-text">
<span class="icon"><i class="fas fa-passport"></i></span>
<span>
<span class="is-hidden-mobile">ID:&nbsp;</span>
<NuxtLink :to="`/history/${data.id}`" v-text="data.id"/>
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text" v-if="parseInt(data.progress)">
<span class="icon"><i class="fas fa-bars-progress"></i></span>
<span><span class="is-hidden-mobile">Progress:</span> {{ formatDuration(data.progress) }}</span>
</span>
<span v-else>-</span>
</div>
<div class="column is-6 has-text-left">
<span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fas fa-eye-slash" v-if="!data.watched"></i> <i class="fas" :class="{'fa-eye-slash':data.watched,'fa-eye':!data.watched}"></i>
<i class="fas fa-eye" v-else></i>
</span> </span>
<span> </button>
<span class="is-hidden-mobile">Status:</span> </p>
{{ data.watched ? 'Played' : 'Unplayed' }} <p class="control">
</span> <button class="button is-danger" @click="deleteItem(data)" v-tooltip.bottom="'Delete the record'"
</span> :disabled="isDeleting || isLoading" :class="{'is-loading':isDeleting}">
</div> <span class="icon"><i class="fas fa-trash"></i></span>
<div class="column is-6 has-text-right"> </button>
<span class="icon-text"> </p>
<span class="icon"><i class="fas fa-envelope"></i></span> <p class="control">
<span> <button class="button is-info" @click="loadContent(id)" :class="{'is-loading':isLoading}">
<span class="is-hidden-mobile">Event:</span> <span class="icon"><i class="fas fa-sync"></i></span>
{{ ag(data.extra, `${data.via}.event`, 'Unknown') }} </button>
</span> </p>
</span>
</div>
<div class="column is-6 has-text-left">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i></span>
<span>
<span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`Backend updated this record at: ${moment.unix(data.updated).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(data.updated).fromNow() }}
</span>
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text">
<span class="icon" v-if="'episode' === data.type"><i class="fas fa-tv"></i></span>
<span class="icon" v-else><i class="fas fa-film"></i></span>
<span>
<span class="is-hidden-mobile">Type:&nbsp;</span>
<NuxtLink :to="makeSearchLink('type',data.type)" v-text="ucFirst(data.type)"/>
</span>
</span>
</div>
<div class="column is-6 has-text-left" v-if="'episode' === data.type">
<span class="icon-text">
<span class="icon"><i class="fas fa-tv"></i></span>
<span><span class="is-hidden-mobile">Season:&nbsp;</span>
<NuxtLink :to="makeSearchLink('season',data.season)" v-text="data.season"/>
</span>
</span>
</div>
<div class="column is-6 has-text-right" v-if="'episode' === data.type">
<span class="icon-text">
<span class="icon"><i class="fas fa-tv"></i></span>
<span><span class="is-hidden-mobile">Episode:&nbsp;</span>
<NuxtLink :to="makeSearchLink('episode',data.episode)" v-text="data.episode"/>
</span>
</span>
</div>
<div class="column is-12" v-if="data.guids && Object.keys(data.guids).length>0">
<span class="icon-text is-clickable" v-tooltip="'Globally unique identifier for this item'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in data.guids">
<NuxtLink target="_blank" :to="makeGUIDLink( data.type, source.split('guid_')[1], guid, data)">
{{ source.split('guid_')[1] }}://{{ guid }}
</NuxtLink>
</span>
</div>
<div class="column is-12" v-if="data.rguids && Object.keys(data.rguids).length>0">
<span class="icon-text is-clickable" v-tooltip="'Relative Globally unique identifier for this episode'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>rGUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in data.rguids">
<NuxtLink :to="makeSearchLink('rguid', `${source.split('guid_')[1]}://${guid}`)">
{{ source.split('guid_')[1] }}://{{ guid }}
</NuxtLink>
</span>
</div>
<div class="column is-12" v-if="data.parent && Object.keys(data.parent).length>0">
<span class="icon-text is-clickable" v-tooltip="'Globally unique identifier for the series'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>Series GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in data.parent">
<NuxtLink target="_blank" :to="makeGUIDLink( 'series', source.split('guid_')[1], guid, data)">
{{ source.split('guid_')[1] }}://{{ guid }}
</NuxtLink>
</span>
</div>
<div class="column is-12" v-if="data?.content_title">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i></span>
<span class="is-hidden-mobile">Subtitle:&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle', data.content_title)" v-text="data.content_title"/>
</div>
</div>
<div class="column is-12" v-if="data?.content_path">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i></span>
<span class="is-hidden-mobile">File:&nbsp;</span>
<NuxtLink :to="makeSearchLink('path', data.content_path)" v-text="data.content_path"/>
</div>
</div>
<div class="column is-6 has-text-left" v-if="data.created_at">
<span class="icon-text">
<span class="icon"><i class="fas fa-database"></i></span>
<span>
<span class="is-hidden-mobile">Created:&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`DB record created at: ${moment.unix(data.created_at).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(data.created_at).fromNow() }}
</span>
</span>
</span>
</div>
<div class="column is-6 has-text-right" v-if="data.updated_at">
<span class="icon-text">
<span class="icon"><i class="fas fa-database"></i></span>
<span>
<span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`DB record updated at: ${moment.unix(data.updated_at).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(data.updated_at).fromNow() }}
</span>
</span>
</span>
</div>
</div> </div>
</div> </div>
</div> <div class="subtitle is-5" v-if="data?.via && data.content_title">
</div> <span class="is-unselectable icon">
<i class="fas fa-tv" :class="{ 'fa-tv': 'episode' === data.type, 'fa-film': 'movie' === data.type }"></i>
<div class="column is-12" v-if="data?.via && Object.keys(data.metadata).length>0"> </span>
<div class="card" v-for="(item, key) in data.metadata" :key="key" {{ data?.content_title }}
:class="{ 'is-success': parseInt(item.watched) }">
<header class="card-header">
<div class="card-header-title is-clickable is-unselectable" @click="item._toggle = !item._toggle">
<span class="icon">
<i class="fas" :class="{'fa-arrow-up': item?._toggle, 'fa-arrow-down': !item?._toggle}"></i>
</span>
Metadata via
</div>
<div class="card-header-icon">
<span class="icon-text">
<span class="icon"><i class="fas fa-server"></i></span>
<span>
<NuxtLink :to="`/backend/${key}`" v-text="key"/>
</span>
</span>
</div>
</header>
<div class="card-content" v-if="item?._toggle">
<div class="columns is-multiline is-mobile">
<div class="column is-6">
<span class="icon-text">
<span class="icon"><i class="fas fa-passport"></i></span>
<span>
<span class="is-hidden-mobile">ID:&nbsp;</span>
<NuxtLink :to="item?.webUrl" target="_blank" v-text="item.id" v-if="item?.webUrl"/>
<span v-else v-text="item.id"/>
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text" v-if="parseInt(item?.progress)">
<span class="icon"><i class="fas fa-bars-progress"></i></span>
<span><span class="is-hidden-mobile">Progress:</span> {{ formatDuration(item.progress) }}</span>
</span>
<span v-else>-</span>
</div>
<div class="column is-6">
<span class="icon-text">
<span class="icon">
<i class="fas fa-eye-slash" :class="parseInt(item.watched) ?'fa-eye-slash' : 'fa-eye'"></i>
</span>
<span>
<span class="is-hidden-mobile">Status:</span>
{{ parseInt(item.watched) ? 'Played' : 'Unplayed' }}
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text">
<span class="icon"><i class="fas fa-envelope"></i></span>
<span>
<span class="is-hidden-mobile">Event:</span>
{{ ag(data.extra, `${key}.event`, 'Unknown') }}
</span>
</span>
</div>
<div class="column is-6">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i></span>
<span>
<span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`Backend last activity: ${getMoment(ag(data.extra, `${key}.received_at`, data.updated)).format(TOOLTIP_DATE_FORMAT)}`">
{{ getMoment(ag(data.extra, `${key}.received_at`, data.updated)).fromNow() }}
</span>
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text">
<span class="icon" v-if="'episode' === item.type"><i class="fas fa-tv"></i></span>
<span class="icon" v-else><i class="fas fa-film"></i></span>
<span>
<span class="is-hidden-mobile">Type:&nbsp;</span>
<NuxtLink :to="makeSearchLink('type',item.type)" v-text="ucFirst(item.type)"/>
</span>
</span>
</div>
<div class="column is-6" v-if="'episode' === item.type">
<span class="icon-text">
<span class="icon"><i class="fas fa-tv"></i></span>
<span>
<span class="is-hidden-mobile">Season:&nbsp;</span>
<NuxtLink :to="makeSearchLink('season',item.season)" v-text="item.season"/>
</span>
</span>
</div>
<div class="column is-6 has-text-right" v-if="'episode' === item.type">
<span class="icon-text">
<span class="icon"><i class="fas fa-tv"></i></span>
<span>
<span class="is-hidden-mobile">Episode:&nbsp;</span>
<NuxtLink :to="makeSearchLink('episode',item.episode)" v-text="item.episode"/>
</span>
</span>
</div>
<div class="column is-12" v-if="item.guids && Object.keys(item.guids).length>0">
<span class="icon-text is-clickable" v-tooltip="'Globally unique identifier for this item'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in item.guids">
<NuxtLink target="_blank" :to="makeGUIDLink( item.type, source.split('guid_')[1], guid, item)">
{{ source.split('guid_')[1] }}://{{ guid }}
</NuxtLink>
</span>
</div>
<div class="column is-12" v-if="item.parent && Object.keys(item.parent).length>0">
<span class="is-clickable icon-text" v-tooltip="'Globally unique identifier for the series'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>Series GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in item.parent">
<NuxtLink target="_blank" :to="makeGUIDLink( 'series', source.split('guid_')[1], guid, item)">
{{ source.split('guid_')[1] }}://{{ guid }}
</NuxtLink>
</span>
</div>
<div class="column is-12" v-if="item?.extra?.title">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i></span>
<span class="is-hidden-mobile">Subtitle:&nbsp</span>
<NuxtLink :to="makeSearchLink('subtitle', item.extra.title)" v-text="item.extra.title"/>
</div>
</div>
<div class="column is-12" v-if="item?.path">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i></span>
<span class="is-hidden-mobile">File:&nbsp;</span>
<NuxtLink :to="makeSearchLink('path', item.path)" v-text="item.path"/>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12" v-if="!data?.via && isLoading">
<span class="title is-4 is-clickable" @click="showRawData = !showRawData"> <Message message_class="has-background-info-90 has-text-dark" title="Loading"
<span class="icon-text"> icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
<span class="icon">
<i v-if="showRawData" class="fas fa-arrow-up"></i>
<i v-else class="fas fa-arrow-down"></i>
</span>
<span>Show raw data...</span>
</span>
</span>
<div v-if="showRawData" class="mt-2">
<code class="is-block is-pre-wrap">{{ JSON.stringify(data, null, 2) }}</code>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12" v-if="data?.not_reported_by && data.not_reported_by.length>0">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-warning-80 has-text-dark" icon="fas fa-exclamation-triangle"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> :toggle="show_history_page_warning" title="Warning" :use-toggle="true"
<ul> @toggle="show_history_page_warning=!show_history_page_warning">
<li> <p>
To see if your media backends are reporting different metadata for the same file, click on the file link <span class="icon"><i class="fas fa-exclamation"></i></span>
which will filter your history based on that file. There are no metadata regarding this <strong>{{ data.type }}</strong> from (
</li> <span class="tag mr-1 has-text-dark" v-for="backend in data.not_reported_by" :key="`nr-${backend}`">
<li>Clicking on the ID in <code>metadata via</code> boxes will take you directly to the item in the source <NuxtLink :to="`/backend/${backend}`" v-text="backend"/>
backend. While clicking on the GUIDs will take you to that source link, similarly clicking on the series </span>).
GUIDs will take you to the series link that was provided by the external source. </p>
</li> <h5 class="has-text-dark">
<li> <span class="icon-text">
<code>rGUIDSs</code> are relative globally unique identifiers for episodes based on <code>series <span class="icon"><i class="fas fa-question-circle"></i></span>
GUID</code>. They are formatted as <code>GUID://seriesID/season_number/episode_number</code>. We use <span>Possible reasons</span>
<code>rGUIDs</code>, to identify specific episode. This is more reliable than using episode specific </span>
<code>GUID</code>, as they are often misreported in the source data. </h5>
</li> <ul>
<template v-if="data?.not_reported_by && data.not_reported_by.length > 0"> <li>Delayed import operation. Might be yet to be imported due to webhooks not being used, or the backend
<li> doesn't support webhooks.
The warning on top of the page usually is accurate, and it is recommended to check the backend metadata
for the item.
<template v-if="'episode' === data.type">
For episodes, we use <code>rGUIDs</code> to identify the episode, and <strong>important part</strong>
of that GUID is the <code>series GUID</code>. We need at least one reported series GUIDs to match
between your backends. If none are matching, it will be treated as separate series.
</template>
</li> </li>
</template> <li>Item mismatched at the source backend.</li>
</ul> <li>
</Message> There are no matching <code>{{ 'episode' === data.type ? 'Series GUIDs' : 'GUIDs' }}</code> in common
being reported, And thus it was treated as separate item.
</li>
</ul>
</Message>
</div>
<div class="column is-12" v-if="data?.via">
<div class="card" :class="{ 'is-success': parseInt(data.watched) }">
<header class="card-header">
<div class="card-header-title is-clickable is-unselectable" @click="data._toggle = !data._toggle">
<span class="icon">
<i class="fas" :class="{'fa-arrow-up': data?._toggle, 'fa-arrow-down': !data?._toggle}"></i>
</span>
<span>Latest local metadata via</span>
</div>
<div class="card-header-icon">
<span class="icon-text">
<span class="icon"><i class="fas fa-server"></i></span>
<span>
<NuxtLink :to="`/backend/${data.via}`" v-text="data.via"/>
</span>
</span>
</div>
</header>
<div class="card-content" v-if="data?._toggle">
<div class="columns is-multiline is-mobile">
<div class="column is-6">
<span class="icon-text">
<span class="icon"><i class="fas fa-passport"></i></span>
<span>
<span class="is-hidden-mobile">ID:&nbsp;</span>
<NuxtLink :to="`/history/${data.id}`" v-text="data.id"/>
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text" v-if="parseInt(data.progress)">
<span class="icon"><i class="fas fa-bars-progress"></i></span>
<span><span class="is-hidden-mobile">Progress:</span> {{ formatDuration(data.progress) }}</span>
</span>
<span v-else>-</span>
</div>
<div class="column is-6 has-text-left">
<span class="icon-text">
<span class="icon">
<i class="fas fa-eye-slash" v-if="!data.watched"></i>
<i class="fas fa-eye" v-else></i>
</span>
<span>
<span class="is-hidden-mobile">Status:</span>
{{ data.watched ? 'Played' : 'Unplayed' }}
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text">
<span class="icon"><i class="fas fa-envelope"></i></span>
<span>
<span class="is-hidden-mobile">Event:</span>
{{ ag(data.extra, `${data.via}.event`, 'Unknown') }}
</span>
</span>
</div>
<div class="column is-6 has-text-left">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i></span>
<span>
<span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`Backend updated this record at: ${moment.unix(data.updated).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(data.updated).fromNow() }}
</span>
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text">
<span class="icon" v-if="'episode' === data.type"><i class="fas fa-tv"></i></span>
<span class="icon" v-else><i class="fas fa-film"></i></span>
<span>
<span class="is-hidden-mobile">Type:&nbsp;</span>
<NuxtLink :to="makeSearchLink('type',data.type)" v-text="ucFirst(data.type)"/>
</span>
</span>
</div>
<div class="column is-6 has-text-left" v-if="'episode' === data.type">
<span class="icon-text">
<span class="icon"><i class="fas fa-tv"></i></span>
<span><span class="is-hidden-mobile">Season:&nbsp;</span>
<NuxtLink :to="makeSearchLink('season',data.season)" v-text="data.season"/>
</span>
</span>
</div>
<div class="column is-6 has-text-right" v-if="'episode' === data.type">
<span class="icon-text">
<span class="icon"><i class="fas fa-tv"></i></span>
<span><span class="is-hidden-mobile">Episode:&nbsp;</span>
<NuxtLink :to="makeSearchLink('episode',data.episode)" v-text="data.episode"/>
</span>
</span>
</div>
<div class="column is-12" v-if="data.guids && Object.keys(data.guids).length>0">
<span class="icon-text is-clickable" v-tooltip="'Globally unique identifier for this item'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in data.guids">
<NuxtLink target="_blank" :to="makeGUIDLink( data.type, source.split('guid_')[1], guid, data)">
{{ source.split('guid_')[1] }}://{{ guid }}
</NuxtLink>
</span>
</div>
<div class="column is-12" v-if="data.rguids && Object.keys(data.rguids).length>0">
<span class="icon-text is-clickable" v-tooltip="'Relative Globally unique identifier for this episode'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>rGUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in data.rguids">
<NuxtLink :to="makeSearchLink('rguid', `${source.split('guid_')[1]}://${guid}`)">
{{ source.split('guid_')[1] }}://{{ guid }}
</NuxtLink>
</span>
</div>
<div class="column is-12" v-if="data.parent && Object.keys(data.parent).length>0">
<span class="icon-text is-clickable" v-tooltip="'Globally unique identifier for the series'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>Series GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in data.parent">
<NuxtLink target="_blank" :to="makeGUIDLink( 'series', source.split('guid_')[1], guid, data)">
{{ source.split('guid_')[1] }}://{{ guid }}
</NuxtLink>
</span>
</div>
<div class="column is-12" v-if="data?.content_title">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i></span>
<span class="is-hidden-mobile">Subtitle:&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle', data.content_title)" v-text="data.content_title"/>
</div>
</div>
<div class="column is-12" v-if="data?.content_path">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i></span>
<span class="is-hidden-mobile">File:&nbsp;</span>
<NuxtLink :to="makeSearchLink('path', data.content_path)" v-text="data.content_path"/>
</div>
</div>
<div class="column is-6 has-text-left" v-if="data.created_at">
<span class="icon-text">
<span class="icon"><i class="fas fa-database"></i></span>
<span>
<span class="is-hidden-mobile">Created:&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`DB record created at: ${moment.unix(data.created_at).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(data.created_at).fromNow() }}
</span>
</span>
</span>
</div>
<div class="column is-6 has-text-right" v-if="data.updated_at">
<span class="icon-text">
<span class="icon"><i class="fas fa-database"></i></span>
<span>
<span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`DB record updated at: ${moment.unix(data.updated_at).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(data.updated_at).fromNow() }}
</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="column is-12" v-if="data?.via && Object.keys(data.metadata).length>0">
<div class="card" v-for="(item, key) in data.metadata" :key="key"
:class="{ 'is-success': parseInt(item.watched) }">
<header class="card-header">
<div class="card-header-title is-clickable is-unselectable" @click="item._toggle = !item._toggle">
<span class="icon">
<i class="fas" :class="{'fa-arrow-up': item?._toggle, 'fa-arrow-down': !item?._toggle}"></i>
</span>
Metadata via
</div>
<div class="card-header-icon">
<span class="icon-text">
<span class="icon"><i class="fas fa-server"></i></span>
<span>
<NuxtLink :to="`/backend/${key}`" v-text="key"/>
</span>
</span>
</div>
</header>
<div class="card-content" v-if="item?._toggle">
<div class="columns is-multiline is-mobile">
<div class="column is-6">
<span class="icon-text">
<span class="icon"><i class="fas fa-passport"></i></span>
<span>
<span class="is-hidden-mobile">ID:&nbsp;</span>
<NuxtLink :to="item?.webUrl" target="_blank" v-text="item.id" v-if="item?.webUrl"/>
<span v-else v-text="item.id"/>
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text" v-if="parseInt(item?.progress)">
<span class="icon"><i class="fas fa-bars-progress"></i></span>
<span><span class="is-hidden-mobile">Progress:</span> {{ formatDuration(item.progress) }}</span>
</span>
<span v-else>-</span>
</div>
<div class="column is-6">
<span class="icon-text">
<span class="icon">
<i class="fas fa-eye-slash" :class="parseInt(item.watched) ?'fa-eye-slash' : 'fa-eye'"></i>
</span>
<span>
<span class="is-hidden-mobile">Status:</span>
{{ parseInt(item.watched) ? 'Played' : 'Unplayed' }}
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text">
<span class="icon"><i class="fas fa-envelope"></i></span>
<span>
<span class="is-hidden-mobile">Event:</span>
{{ ag(data.extra, `${key}.event`, 'Unknown') }}
</span>
</span>
</div>
<div class="column is-6">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i></span>
<span>
<span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`Backend last activity: ${getMoment(ag(data.extra, `${key}.received_at`, data.updated)).format(TOOLTIP_DATE_FORMAT)}`">
{{ getMoment(ag(data.extra, `${key}.received_at`, data.updated)).fromNow() }}
</span>
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text">
<span class="icon" v-if="'episode' === item.type"><i class="fas fa-tv"></i></span>
<span class="icon" v-else><i class="fas fa-film"></i></span>
<span>
<span class="is-hidden-mobile">Type:&nbsp;</span>
<NuxtLink :to="makeSearchLink('type',item.type)" v-text="ucFirst(item.type)"/>
</span>
</span>
</div>
<div class="column is-6" v-if="'episode' === item.type">
<span class="icon-text">
<span class="icon"><i class="fas fa-tv"></i></span>
<span>
<span class="is-hidden-mobile">Season:&nbsp;</span>
<NuxtLink :to="makeSearchLink('season',item.season)" v-text="item.season"/>
</span>
</span>
</div>
<div class="column is-6 has-text-right" v-if="'episode' === item.type">
<span class="icon-text">
<span class="icon"><i class="fas fa-tv"></i></span>
<span>
<span class="is-hidden-mobile">Episode:&nbsp;</span>
<NuxtLink :to="makeSearchLink('episode',item.episode)" v-text="item.episode"/>
</span>
</span>
</div>
<div class="column is-12" v-if="item.guids && Object.keys(item.guids).length>0">
<span class="icon-text is-clickable" v-tooltip="'Globally unique identifier for this item'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in item.guids">
<NuxtLink target="_blank" :to="makeGUIDLink( item.type, source.split('guid_')[1], guid, item)">
{{ source.split('guid_')[1] }}://{{ guid }}
</NuxtLink>
</span>
</div>
<div class="column is-12" v-if="item.parent && Object.keys(item.parent).length>0">
<span class="is-clickable icon-text" v-tooltip="'Globally unique identifier for the series'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>Series GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in item.parent">
<NuxtLink target="_blank" :to="makeGUIDLink( 'series', source.split('guid_')[1], guid, item)">
{{ source.split('guid_')[1] }}://{{ guid }}
</NuxtLink>
</span>
</div>
<div class="column is-12" v-if="item?.extra?.title">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i></span>
<span class="is-hidden-mobile">Subtitle:&nbsp</span>
<NuxtLink :to="makeSearchLink('subtitle', item.extra.title)" v-text="item.extra.title"/>
</div>
</div>
<div class="column is-12" v-if="item?.path">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i></span>
<span class="is-hidden-mobile">File:&nbsp;</span>
<NuxtLink :to="makeSearchLink('path', item.path)" v-text="item.path"/>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="column is-12">
<span class="title is-4 is-clickable" @click="showRawData = !showRawData">
<span class="icon-text">
<span class="icon">
<i v-if="showRawData" class="fas fa-arrow-up"></i>
<i v-else class="fas fa-arrow-down"></i>
</span>
<span>Show raw data...</span>
</span>
</span>
<div v-if="showRawData" class="mt-2">
<code class="is-block is-pre-wrap">{{ JSON.stringify(data, null, 2) }}</code>
</div>
</div>
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
To see if your media backends are reporting different metadata for the same file, click on the file link
which will filter your history based on that file.
</li>
<li>Clicking on the ID in <code>metadata via</code> boxes will take you directly to the item in the source
backend. While clicking on the GUIDs will take you to that source link, similarly clicking on the series
GUIDs will take you to the series link that was provided by the external source.
</li>
<li>
<code>rGUIDSs</code> are relative globally unique identifiers for episodes based on <code>series
GUID</code>. They are formatted as <code>GUID://seriesID/season_number/episode_number</code>. We use
<code>rGUIDs</code>, to identify specific episode. This is more reliable than using episode specific
<code>GUID</code>, as they are often misreported in the source data.
</li>
<template v-if="data?.not_reported_by && data.not_reported_by.length > 0">
<li>
The warning on top of the page usually is accurate, and it is recommended to check the backend metadata
for the item.
<template v-if="'episode' === data.type">
For episodes, we use <code>rGUIDs</code> to identify the episode, and <strong>important part</strong>
of that GUID is the <code>series GUID</code>. We need at least one reported series GUIDs to match
between your backends. If none are matching, it will be treated as separate series.
</template>
</li>
</template>
</ul>
</Message>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import request from '~/utils/request.js' import request from '~/utils/request'
import { import {
ag, ag,
formatDuration, formatDuration,
@@ -471,10 +473,10 @@ import {
notification, notification,
TOOLTIP_DATE_FORMAT, TOOLTIP_DATE_FORMAT,
ucFirst ucFirst
} from '~/utils/index.js' } from '~/utils/index'
import moment from 'moment' import moment from 'moment'
import {useStorage} from "@vueuse/core"; import {useStorage} from '@vueuse/core'
import Message from "~/components/Message.vue"; import Message from '~/components/Message'
const id = useRoute().params.id const id = useRoute().params.id

View File

@@ -1,296 +1,298 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-history"></i></span> <span class="title is-4">
History <span class="icon"><i class="fas fa-history"></i></span>
</span> History
<div class="is-pulled-right"> </span>
<div class="field is-grouped"> <div class="is-pulled-right">
<div class="field is-grouped">
<div class="control has-icons-left" v-if="showFilter"> <div class="control has-icons-left" v-if="showFilter">
<input type="search" v-model.lazy="filter" class="input" id="filter" <input type="search" v-model.lazy="filter" class="input" id="filter"
placeholder="Filter displayed results."> placeholder="Filter displayed results.">
<span class="icon is-left"> <span class="icon is-left">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</span> </span>
</div>
<div class="control">
<button class="button is-danger is-light" @click="toggleFilter">
<span class="icon"><i class="fas fa-filter"></i></span>
</button>
</div>
<div class="control">
<div class="select">
<select v-model="perpage" :disabled="isLoading" @change="loadContent(1,false)">
<option value="" disabled>Per page</option>
<option v-for="i in [50,100,200,400,500]" :key="`perpage-${i}`" :value="i">
{{ i }}
</option>
</select>
</div>
</div>
<p class="control">
<button class="button is-primary" @click="searchForm = !searchForm">
<span class="icon">
<i class="fas fa-search"></i>
</span>
</button>
</p>
<div class="control">
<button class="button is-info is-light" @click="selectAll = !selectAll"
data-tooltip="Toggle select all">
<span class="icon">
<i class="fas fa-check-square"
:class="{'fa-check-square': !selectAll,'fa-square':selectAll}"></i>
</span>
</button>
</div>
<p class="control">
<button class="button is-info" @click="loadContent(page, true)">
<span class="icon">
<i class="fas fa-sync"></i>
</span>
</button>
</p>
</div> </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="control"> <div class="column is-12" v-if="total && last_page > 1">
<button class="button is-danger is-light" @click="toggleFilter"> <div class="field is-grouped">
<span class="icon"><i class="fas fa-filter"></i></span> <div class="control" v-if="page !== 1">
<button rel="first" class="button" @click="loadContent(1)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span><<</span>
</button>
</div>
<div class="control" v-if="page > 1 && (page-1) !== 1">
<button rel="prev" class="button" @click="loadContent(page-1)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span><</span>
</button> </button>
</div> </div>
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select v-model="perpage" :disabled="isLoading" @change="loadContent(1,false)"> <select v-model="page" @change="loadContent(page)" :disabled="isLoading">
<option value="" disabled>Per page</option> <option v-for="(item, index) in makePagination(page, last_page)" :key="index" :value="item.page"
<option v-for="i in [50,100,200,400,500]" :key="`perpage-${i}`" :value="i"> :disabled="item.page === 0">
{{ i }} {{ item.text }}
</option> </option>
</select> </select>
</div> </div>
</div> </div>
<div class="control" v-if="page !== last_page && (page+1) !== last_page">
<p class="control"> <button rel="next" class="button" @click="loadContent(page+1)" :disabled="isLoading"
<button class="button is-primary" @click="searchForm = !searchForm"> :class="{'is-loading':isLoading}">
<span class="icon"> <span>></span>
<i class="fas fa-search"></i>
</span>
</button>
</p>
<div class="control">
<button class="button is-info is-light" @click="selectAll = !selectAll"
data-tooltip="Toggle select all">
<span class="icon">
<i class="fas fa-check-square"
:class="{'fa-check-square': !selectAll,'fa-square':selectAll}"></i>
</span>
</button> </button>
</div> </div>
<div class="control" v-if="page !== last_page">
<p class="control"> <button rel="last" class="button" @click="loadContent(last_page)" :disabled="isLoading"
<button class="button is-info" @click="loadContent(page, true)"> :class="{'is-loading':isLoading}">
<span class="icon"> <span>>></span>
<i class="fas fa-sync"></i>
</span>
</button> </button>
</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" v-if="page !== 1">
<button rel="first" class="button" @click="loadContent(1)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span><<</span>
</button>
</div>
<div class="control" v-if="page > 1 && (page-1) !== 1">
<button rel="prev" class="button" @click="loadContent(page-1)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span><</span>
</button>
</div>
<div class="control">
<div class="select">
<select v-model="page" @change="loadContent(page)" :disabled="isLoading">
<option v-for="(item, index) in makePagination(page, last_page)" :key="index" :value="item.page"
:disabled="item.page === 0">
{{ item.text }}
</option>
</select>
</div> </div>
</div> </div>
<div class="control" v-if="page !== last_page && (page+1) !== last_page">
<button rel="next" class="button" @click="loadContent(page+1)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span>></span>
</button>
</div>
<div class="control" v-if="page !== last_page">
<button rel="last" class="button" @click="loadContent(last_page)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span>>></span>
</button>
</div>
</div> </div>
</div>
<div class="column is-12" v-if="searchForm"> <div class="column is-12" v-if="searchForm">
<form @submit.prevent="loadContent(1)"> <form @submit.prevent="loadContent(1)">
<div class="field"> <div class="field">
<div class="field-body"> <div class="field-body">
<div class="field is-grouped-tablet"> <div class="field is-grouped-tablet">
<div class="control has-icons-left"> <div class="control has-icons-left">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select v-model="searchField" class="is-capitalized" :disabled="isLoading"> <select v-model="searchField" class="is-capitalized" :disabled="isLoading">
<option value="">Select Field</option> <option value="">Select Field</option>
<option v-for="field in searchable" :key="'search-' + field.key" :value="field.key"> <option v-for="field in searchable" :key="'search-' + field.key" :value="field.key">
{{ field.display ?? field.key }} {{ field.display ?? field.key }}
</option> </option>
</select> </select>
</div>
<div class="icon is-left">
<i class="fas fa-folder-tree"></i>
</div>
</div> </div>
<div class="icon is-left">
<i class="fas fa-folder-tree"></i> <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>
</div>
<div class="control is-expanded has-icons-left"> <div class="control">
<input class="input" type="search" placeholder="Search..." v-model="query" <button class="button is-primary" type="submit" :disabled="!query || '' === searchField || isLoading"
:disabled="'' === searchField || isLoading"> :class="{'is-loading':isLoading}">
<div class="icon is-left"> <span class="icon-text">
<i class="fas fa-search"></i> <span class="icon"><i class="fas fa-search"></i></span>
<span>Search</span>
</span>
</button>
</div> </div>
</div>
<div class="control"> <div class="control">
<button class="button is-primary" type="submit" :disabled="!query || '' === searchField || isLoading" <button class="button is-warning" type="button" @click="clearSearch" :disabled="isLoading">
:class="{'is-loading':isLoading}"> <span class="icon-text">
<span class="icon-text"> <span class="icon"><i class="fas fa-cancel"></i></span>
<span class="icon"><i class="fas fa-search"></i></span> <span>Reset</span>
<span>Search</span> </span>
</span> </button>
</button> </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>
</div> </div>
<p class="help" v-html="getHelp(searchField)"></p>
</div> </div>
<p class="help" v-html="getHelp(searchField)"></p> </form>
</div> </div>
</form>
</div>
<div class="column is-12" v-if="selected_ids.length > 0"> <div class="column is-12" v-if="selected_ids.length > 0">
<div class="field is-grouped is-justify-content-center"> <div class="field is-grouped is-justify-content-center">
<div class="control"> <div class="control">
<button class="button is-danger" @click="massAction('delete')" :disabled="massActionInProgress"> <button class="button is-danger" @click="massAction('delete')" :disabled="massActionInProgress">
<span class="icon"><i class="fas fa-trash"></i></span> <span class="icon"><i class="fas fa-trash"></i></span>
<span class="is-hidden-mobile">Delete '{{ selected_ids.length }}' item/s</span> <span class="is-hidden-mobile">Delete '{{ selected_ids.length }}' item/s</span>
</button> </button>
</div> </div>
<div class="control"> <div class="control">
<button class="button is-primary" @click="massAction('mark_played')" :disabled="massActionInProgress"> <button class="button is-primary" @click="massAction('mark_played')" :disabled="massActionInProgress">
<span class="icon"><i class="fas fa-eye"></i></span> <span class="icon"><i class="fas fa-eye"></i></span>
<span class="is-hidden-mobile">Mark '{{ selected_ids.length }}' item/s as played</span> <span class="is-hidden-mobile">Mark '{{ selected_ids.length }}' item/s as played</span>
</button> </button>
</div> </div>
<div class="control"> <div class="control">
<button class="button is-warning" @click="massAction('mark_unplayed')" :disabled="massActionInProgress"> <button class="button is-warning" @click="massAction('mark_unplayed')" :disabled="massActionInProgress">
<span class="icon"><i class="fas fa-eye-slash"></i></span> <span class="icon"><i class="fas fa-eye-slash"></i></span>
<span class="is-hidden-mobile">Mark '{{ selected_ids.length }}' item/s as unplayed</span> <span class="is-hidden-mobile">Mark '{{ selected_ids.length }}' item/s as unplayed</span>
</button> </button>
</div>
</div> </div>
</div> </div>
</div>
<div class="column is-12" v-if="items?.length < 1 || filteredRows(items).length < 1"> <div class="column is-12" v-if="items?.length < 1 || filteredRows(items).length < 1">
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading" <Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/> icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
<Message v-else class="has-background-warning-80 has-text-dark" title="Warning" <Message v-else class="has-background-warning-80 has-text-dark" title="Warning"
icon="fas fa-exclamation-triangle" :use-close="true" @close="clearSearch"> icon="fas fa-exclamation-triangle" :use-close="true" @close="clearSearch">
<div class="icon-text"> <div class="icon-text">
No items found. No items found.
<span v-if="query">For <code><strong>{{ searchField }}</strong> : <strong>{{ query }}</strong></code></span> <span v-if="query">For <code><strong>{{ searchField }}</strong> : <strong>{{ query }}</strong></code></span>
<span v-if="filter">For <code><strong>Filter</strong> : <strong>{{ filter }}</strong></code></span> <span v-if="filter">For <code><strong>Filter</strong> : <strong>{{ filter }}</strong></code></span>
</div> </div>
<code class="is-block mt-4" v-if="error">{{ error }}</code> <code class="is-block mt-4" v-if="error">{{ error }}</code>
</Message> </Message>
</div> </div>
<div class="column is-12"> <div class="column is-12">
<div class="columns is-multiline" v-if="items?.length>0"> <div class="columns is-multiline" v-if="items?.length>0">
<template v-for="item in items" :key="item.id"> <template v-for="item in items" :key="item.id">
<Lazy :unrender="true" :min-height="240" class="column is-6-tablet" v-if="filterItem(item)"> <Lazy :unrender="true" :min-height="240" class="column is-6-tablet" v-if="filterItem(item)">
<div class="card" :class="{ 'is-success': item.watched }"> <div class="card" :class="{ 'is-success': item.watched }">
<header class="card-header"> <header class="card-header">
<p class="card-header-title is-text-overflow pr-1"> <p class="card-header-title is-text-overflow pr-1">
<span class="icon is-unselectable"> <span class="icon is-unselectable">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" :value="item.id" v-model="selected_ids"> <input type="checkbox" :value="item.id" v-model="selected_ids">
</label>&nbsp; </label>&nbsp;
</span>
<NuxtLink :to="'/history/'+item.id" v-text="item?.full_title ?? makeName(item)"/>
</p>
<span class="card-header-icon" @click="item.showRawData = !item?.showRawData">
<span class="icon">
<i class="fas" :class="{'fa-tv': 'episode' === item.type, 'fa-film': 'movie' === item.type}"></i>
</span>
</span> </span>
<NuxtLink :to="'/history/'+item.id" v-text="item?.full_title ?? makeName(item)"/> </header>
</p> <div class="card-content">
<span class="card-header-icon" @click="item.showRawData = !item?.showRawData"> <div class="columns is-multiline is-mobile has-text-centered">
<span class="icon"> <div class="column is-12 has-text-left" v-if="item?.content_title">
<i class="fas" :class="{'fa-tv': 'episode' === item.type, 'fa-film': 'movie' === item.type}"></i> <div class="field is-grouped">
</span> <div class="control is-clickable"
</span> :class="{'is-text-overflow': !item?.expand_title, 'is-text-contents': item?.expand_title}"
</header> @click="item.expand_title = !item?.expand_title">
<div class="card-content"> <span class="icon"><i class="fas fa-heading"></i>&nbsp;</span>
<div class="columns is-multiline is-mobile has-text-centered"> <NuxtLink :to="makeSearchLink('subtitle',item.content_title)" v-text="item.content_title"/>
<div class="column is-12 has-text-left" v-if="item?.content_title"> </div>
<div class="field is-grouped"> <div class="control">
<div class="control is-clickable" <span class="icon is-clickable" @click="copyText(item.content_title, false)">
:class="{'is-text-overflow': !item?.expand_title, 'is-text-contents': item?.expand_title}" <i class="fas fa-copy"></i></span>
@click="item.expand_title = !item?.expand_title"> </div>
<span class="icon"><i class="fas fa-heading"></i>&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle',item.content_title)" v-text="item.content_title"/>
</div>
<div class="control">
<span class="icon is-clickable" @click="copyText(item.content_title, false)">
<i class="fas fa-copy"></i></span>
</div> </div>
</div> </div>
</div> <div class="column is-12 has-text-left" v-if="item?.content_path">
<div class="column is-12 has-text-left" v-if="item?.content_path"> <div class="field is-grouped">
<div class="field is-grouped"> <div class="control is-clickable"
<div class="control is-clickable" :class="{'is-text-overflow': !item?.expand_path, 'is-text-contents': item?.expand_path}"
:class="{'is-text-overflow': !item?.expand_path, 'is-text-contents': item?.expand_path}" @click="item.expand_path = !item?.expand_path">
@click="item.expand_path = !item?.expand_path"> <span class="icon"><i class="fas fa-file"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-file"></i>&nbsp;</span> <NuxtLink :to="makeSearchLink('path',item.content_path)" v-text="item.content_path"/>
<NuxtLink :to="makeSearchLink('path',item.content_path)" v-text="item.content_path"/> </div>
</div> <div class="control">
<div class="control"> <span class="icon is-clickable" @click="copyText(item.content_path, false)">
<span class="icon is-clickable" @click="copyText(item.content_path, false)"> <i class="fas fa-copy"></i></span>
<i class="fas fa-copy"></i></span> </div>
</div> </div>
</div> </div>
</div> <div class="column is-12 has-text-left" v-if="item?.progress">
<div class="column is-12 has-text-left" v-if="item?.progress"> <span class="icon"><i class="fas fa-bars-progress"></i></span>
<span class="icon"><i class="fas fa-bars-progress"></i></span> <span>{{ formatDuration(item.progress) }}</span>
<span>{{ formatDuration(item.progress) }}</span> </div>
</div> </div>
</div> </div>
</div> <div class="card-content p-0 m-0" v-if="item?.showRawData">
<div class="card-content p-0 m-0" v-if="item?.showRawData">
<pre style="position: relative; max-height: 343px;"><code>{{ JSON.stringify(item, null, 2) }}</code> <pre style="position: relative; max-height: 343px;"><code>{{ JSON.stringify(item, null, 2) }}</code>
<button class="button m-4" @click="() => copyText(JSON.stringify(item, null, 2))" <button class="button m-4" @click="() => copyText(JSON.stringify(item, null, 2))"
style="position: absolute; top:0; right:0;"> style="position: absolute; top:0; right:0;">
<span class="icon"><i class="fas fa-copy"></i></span> <span class="icon"><i class="fas fa-copy"></i></span>
</button> </button>
</pre> </pre>
</div>
<div class="card-footer has-text-centered">
<div class="card-footer-item">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`Record updated at: ${moment.unix(item.updated_at).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(item.updated_at).fromNow() }}
</span>
</div>
</div> </div>
<div class="card-footer-item"> <div class="card-footer has-text-centered">
<div class="is-text-overflow"> <div class="card-footer-item">
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span> <div class="is-text-overflow">
<NuxtLink :to="'/backend/'+item.via" v-text="item.via"/> <span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span v-if="item?.metadata && Object.keys(item?.metadata).length > 1" <span class="has-tooltip"
v-tooltip="`Also reported by: ${Object.keys(item.metadata).filter(i => i !== item.via).join(', ')}.`"> v-tooltip="`Record updated at: ${moment.unix(item.updated_at).format(TOOLTIP_DATE_FORMAT)}`">
(<span class="has-tooltip">+{{ Object.keys(item.metadata).length - 1 }}</span>) {{ moment.unix(item.updated_at).fromNow() }}
</span> </span>
</div>
</div> </div>
</div> <div class="card-footer-item">
<div class="card-footer-item"> <div class="is-text-overflow">
<div class="is-text-overflow"> <span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span> <NuxtLink :to="'/backend/'+item.via" v-text="item.via"/>
{{ item.event ?? '-' }} <span v-if="item?.metadata && Object.keys(item?.metadata).length > 1"
v-tooltip="`Also reported by: ${Object.keys(item.metadata).filter(i => i !== item.via).join(', ')}.`">
(<span class="has-tooltip">+{{ Object.keys(item.metadata).length - 1 }}</span>)
</span>
</div>
</div>
<div class="card-footer-item">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span>
{{ item.event ?? '-' }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </Lazy>
</Lazy> </template>
</template> </div>
</div> </div>
</div>
</div>
</div> </div>
</template> </template>

View File

@@ -1,291 +1,293 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span id="env_page_title" class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-ban"></i></span> <span id="env_page_title" class="title is-4">
Ignored GUIDs <span class="icon"><i class="fas fa-ban"></i></span>
</span> Ignored GUIDs
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<button class="button is-primary" v-tooltip.bottom="'Add New Ignore rule'"
@click="toggleForm = !toggleForm">
<span class="icon">
<i class="fas fa-add"></i>
</span>
</button>
</p>
<p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading || toggleForm"
:class="{'is-loading':isLoading}">
<span class="icon">
<i class="fas fa-sync"></i>
</span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
This page allow you to ignore specific <code>GUID</code> from being processed by the system.
</span> </span>
</div> <div class="is-pulled-right">
</div> <div class="field is-grouped">
<p class="control">
<div class="column is-12" v-if="!toggleForm && items.length < 1"> <button class="button is-primary" v-tooltip.bottom="'Add New Ignore rule'"
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading" @click="toggleForm = !toggleForm">
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/> <span class="icon">
<Message v-else message_class="has-background-success-90 has-text-dark" title="Information" icon="fas fa-check"> <i class="fas fa-add"></i>
There are no ignore rules configured. You can add new ignore rules by clicking on the
<i @click="toggleForm=true" class="is-clickable fas fa-add"></i> button.
</Message>
</div>
<div class="column is-12" v-if="toggleForm">
<form id="page_form" @submit.prevent="addIgnoreRule">
<div class="card">
<header class="card-header">
<p class="card-header-title is-unselectable is-justify-center">Add Ignore rule</p>
</header>
<div class="card-content">
<div class="field">
<label class="label is-unselectable" for="form_select_backend">Backend</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="form_select_backend" v-model="form.backend">
<option value="" disabled>Select Backend</option>
<option v-for="backend in backends" :key="backend.name" :value="backend.name">
{{ backend.name }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-server"></i>
</div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>Ignore rules applies to backends, you must select the correct backend you want to ignore the
GUID from</span>
</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_select_guid">Provider</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="form_select_guid" v-model="form.db">
<option value="" disabled>Select GUID provider</option>
<option v-for="guid in guids" :key="guid.guid" :value="guid.guid">
{{ guid.guid }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-database"></i>
</div>
</div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
<span>You must select the GUID provider that giving you incorrect data.</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_ignore_id">GUID Value</label>
<div class="control has-icons-left">
<input class="input" id="form_ignore_id" type="text" v-model="form.id">
<div class="icon is-small is-left"><i class="fas fa-font"></i></div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>The GUID value to ignore.</span>
</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable">Type</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="form_select_backend" v-model="form.type" class="is-capitalized">
<option value="" disabled>Select type</option>
<option v-for="type in types" :key="type" :value="type">
{{ type }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-server"></i>
</div>
</div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
<span>What kind of data the <code>GUID value</code> reference?</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_scoped">Scope</label>
<div class="control has-icons-left">
<input id="form_scoped" type="checkbox" class="switch is-success" v-model="form.scoped">
<label for="form_scoped">
<template v-if="form.scoped">On (True)</template>
<template v-else>Off (False)</template>
</label>
</div>
<p class="help">
<span class="icon"><i class="fas fa-exclamation"></i></span>
<span>By default, Rules are globally applied to all items from the selected backend, you can limit the
scope, by enabling this option.
</span>
</p>
</div>
<div class="field" v-if="form.scoped">
<label class="label is-unselectable" for="form_scoped_to">Scoped To</label>
<div class="control has-icons-left">
<input class="input" id="form_scoped_to" type="text" v-model="form.scoped_to">
<div class="icon is-small is-left"><i class="fas fa-font"></i></div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
<span>The id to associate this rule with. The value must be the <code>{{ form.type }}</code> id as
being reported by the backend.</span>
</p>
</div>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<button class="button is-fullwidth is-primary" type="submit" :disabled="false === checkForm">
<span class="icon-text">
<span class="icon"><i class="fas fa-save"></i></span>
<span>Save</span>
</span> </span>
</button> </button>
</div> </p>
<div class="card-footer-item"> <p class="control">
<button class="button is-fullwidth is-danger" type="button" @click="cancelForm"> <button class="button is-info" @click="loadContent" :disabled="isLoading || toggleForm"
<span class="icon-text"> :class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-cancel"></i></span> <span class="icon">
<span>Cancel</span> <i class="fas fa-sync"></i>
</span> </span>
</button> </button>
</div> </p>
</div> </div>
</div> </div>
</form> <div class="is-hidden-mobile">
</div> <span class="subtitle">
This page allow you to ignore specific <code>GUID</code> from being processed by the system.
</span>
</div>
</div>
<div v-else class="column is-12" v-if="items"> <div class="column is-12" v-if="!toggleForm && items.length < 1">
<div class="columns is-multiline"> <Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
<div class="column is-6" v-for="item in items" :key="item.rule"> icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
<Message v-else message_class="has-background-success-90 has-text-dark" title="Information" icon="fas fa-check">
There are no ignore rules configured. You can add new ignore rules by clicking on the
<i @click="toggleForm=true" class="is-clickable fas fa-add"></i> button.
</Message>
</div>
<div class="column is-12" v-if="toggleForm">
<form id="page_form" @submit.prevent="addIgnoreRule">
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title is-unselectable is-text-overflow"> <p class="card-header-title is-unselectable is-justify-center">Add Ignore rule</p>
<template v-if="item.title">{{ item.title }}</template>
<template v-else>
{{ item.scoped ? 'Unknown title' : '**Global**' }}
</template>
</p>
<span class="card-header-icon">
<span class="icon">
<i class="fas" :class="{'fa-tv':'Show'===item.type,'fa-film': 'Movie' === item.type}"></i>
</span>
</span>
</header> </header>
<div class="card-content"> <div class="card-content">
<div class="columns is-multiline is-mobile"> <div class="field">
<div class="column is-6"> <label class="label is-unselectable" for="form_select_backend">Backend</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="form_select_backend" v-model="form.backend">
<option value="" disabled>Select Backend</option>
<option v-for="backend in backends" :key="backend.name" :value="backend.name">
{{ backend.name }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-server"></i>
</div>
</div>
<p class="help">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-server"></i></span> <span class="icon"><i class="fas fa-info"></i></span>
<span> <span>Ignore rules applies to backends, you must select the correct backend you want to ignore the
<NuxtLink :to="`/backend/${item.backend}`" v-text="item.backend"/> GUID from</span>
</span>
</span> </span>
</div> </p>
<div class="column is-6 has-text-right"> </div>
<strong>Scope:&nbsp;</strong>
<NuxtLink :to="makeItemLink(item)" v-text="item.scoped_to" v-if="item.scoped_to"/>
<template v-else>Global</template>
</div>
<div class="column is-6"> <div class="field">
<label class="label is-unselectable" for="form_select_guid">Provider</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="form_select_guid" v-model="form.db">
<option value="" disabled>Select GUID provider</option>
<option v-for="guid in guids" :key="guid.guid" :value="guid.guid">
{{ guid.guid }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-database"></i>
</div>
</div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
<span>You must select the GUID provider that giving you incorrect data.</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_ignore_id">GUID Value</label>
<div class="control has-icons-left">
<input class="input" id="form_ignore_id" type="text" v-model="form.id">
<div class="icon is-small is-left"><i class="fas fa-font"></i></div>
</div>
<p class="help">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-database"></i></span> <span class="icon"><i class="fas fa-info"></i></span>
<span> <span>The GUID value to ignore.</span>
<NuxtLink target="_blank" :to="makeGUIDLink(item.type, item.db, item.id)"
v-text="`${item.db}://${item.id}`"/>
</span>
</span> </span>
</div> </p>
</div>
<div class="column is-6 has-text-right"> <div class="field">
<label class="label is-unselectable">Type</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="form_select_backend" v-model="form.type" class="is-capitalized">
<option value="" disabled>Select type</option>
<option v-for="type in types" :key="type" :value="type">
{{ type }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-server"></i>
</div>
</div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
<span>What kind of data the <code>GUID value</code> reference?</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_scoped">Scope</label>
<div class="control has-icons-left">
<input id="form_scoped" type="checkbox" class="switch is-success" v-model="form.scoped">
<label for="form_scoped">
<template v-if="form.scoped">On (True)</template>
<template v-else>Off (False)</template>
</label>
</div>
<p class="help">
<span class="icon"><i class="fas fa-exclamation"></i></span>
<span>By default, Rules are globally applied to all items from the selected backend, you can limit the
scope, by enabling this option.
</span>
</p>
</div>
<div class="field" v-if="form.scoped">
<label class="label is-unselectable" for="form_scoped_to">Scoped To</label>
<div class="control has-icons-left">
<input class="input" id="form_scoped_to" type="text" v-model="form.scoped_to">
<div class="icon is-small is-left"><i class="fas fa-font"></i></div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
<span>The id to associate this rule with. The value must be the <code>{{ form.type }}</code> id as
being reported by the backend.</span>
</p>
</div>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<button class="button is-fullwidth is-primary" type="submit" :disabled="false === checkForm">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i></span> <span class="icon"><i class="fas fa-save"></i></span>
<span class="has-tooltip" <span>Save</span>
v-tooltip="`Created at: ${moment(item.created).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment(item.created).fromNow() }}</span>
</span> </span>
</div> </button>
</div>
<div class="card-footer-item">
<button class="button is-fullwidth is-danger" type="button" @click="cancelForm">
<span class="icon-text">
<span class="icon"><i class="fas fa-cancel"></i></span>
<span>Cancel</span>
</span>
</button>
</div> </div>
</div> </div>
<footer class="card-footer"> </div>
<div class="card-footer-item"> </form>
<button class="button is-fullwidth is-warning" @click="copyText(item.rule)"> </div>
<span class="icon-text">
<span class="icon"><i class="fas fa-copy"></i></span> <div v-else class="column is-12" v-if="items">
<span>Copy</span> <div class="columns is-multiline">
<div class="column is-6" v-for="item in items" :key="item.rule">
<div class="card">
<header class="card-header">
<p class="card-header-title is-unselectable is-text-overflow">
<template v-if="item.title">{{ item.title }}</template>
<template v-else>
{{ item.scoped ? 'Unknown title' : '**Global**' }}
</template>
</p>
<span class="card-header-icon">
<span class="icon">
<i class="fas" :class="{'fa-tv':'Show'===item.type,'fa-film': 'Movie' === item.type}"></i>
</span> </span>
</button> </span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile">
<div class="column is-6">
<span class="icon-text">
<span class="icon"><i class="fas fa-server"></i></span>
<span>
<NuxtLink :to="`/backend/${item.backend}`" v-text="item.backend"/>
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<strong>Scope:&nbsp;</strong>
<NuxtLink :to="makeItemLink(item)" v-text="item.scoped_to" v-if="item.scoped_to"/>
<template v-else>Global</template>
</div>
<div class="column is-6">
<span class="icon-text">
<span class="icon"><i class="fas fa-database"></i></span>
<span>
<NuxtLink target="_blank" :to="makeGUIDLink(item.type, item.db, item.id)"
v-text="`${item.db}://${item.id}`"/>
</span>
</span>
</div>
<div class="column is-6 has-text-right">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i></span>
<span class="has-tooltip"
v-tooltip="`Created at: ${moment(item.created).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment(item.created).fromNow() }}</span>
</span>
</div>
</div>
</div> </div>
<div class="card-footer-item"> <footer class="card-footer">
<button class="button is-fullwidth is-danger" @click="deleteIgnore(item)"> <div class="card-footer-item">
<span class="icon-text"> <button class="button is-fullwidth is-warning" @click="copyText(item.rule)">
<span class="icon"><i class="fas fa-trash"></i></span> <span class="icon-text">
<span>Delete</span> <span class="icon"><i class="fas fa-copy"></i></span>
</span> <span>Copy</span>
</button> </span>
</div> </button>
</footer> </div>
<div class="card-footer-item">
<button class="button is-fullwidth is-danger" @click="deleteIgnore(item)">
<span class="icon-text">
<span class="icon"><i class="fas fa-trash"></i></span>
<span>Delete</span>
</span>
</button>
</div>
</footer>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> @toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul> <ul>
<li>Ignoring specific GUID sometimes helps in preventing incorrect data being added to WatchState, due to <li>Ignoring specific GUID sometimes helps in preventing incorrect data being added to WatchState, due to
incorrect metadata being provided by backends. incorrect metadata being provided by backends.
</li> </li>
<li> <li>
<code>GUID</code> means in terms of WatchState is the unique identifier for a specific item in the <code>GUID</code> means in terms of WatchState is the unique identifier for a specific item in the
external data source. external data source.
</li> </li>
<li>To add a new ignore rule click on the <i @click="toggleForm=true" class="is-clickable fa fa-add"></i> <li>To add a new ignore rule click on the <i @click="toggleForm=true" class="is-clickable fa fa-add"></i>
button. button.
</li> </li>
</ul> </ul>
</Message> </Message>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import request from '~/utils/request.js' import 'assets/css/bulma-switch.css'
import {awaitElement, copyText, notification, stringToRegex, TOOLTIP_DATE_FORMAT} from '~/utils/index.js' import request from '~/utils/request'
import {awaitElement, copyText, notification, stringToRegex, TOOLTIP_DATE_FORMAT} from '~/utils/index'
import {useStorage} from '@vueuse/core' import {useStorage} from '@vueuse/core'
import moment from 'moment' import moment from 'moment'
import 'assets/css/bulma-switch.css' import Message from '~/components/Message'
import Message from "~/components/Message.vue";
useHead({title: 'Ignored GUIDs'}) useHead({title: 'Ignored GUIDs'})

View File

@@ -1,107 +1,109 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12"> <div class="columns is-multiline">
<h1 class="title is-4"> <div class="column is-12">
<span class="icon"><i class="fas fa-history"></i>&nbsp;</span> <h1 class="title is-4">
<NuxtLink to="/history">Latest History</NuxtLink> <span class="icon"><i class="fas fa-history"></i>&nbsp;</span>
</h1> <NuxtLink to="/history">Latest History</NuxtLink>
</div> </h1>
</div>
<div class="column is-12"> <div class="column is-12">
<div class="columns is-multiline" v-if="lastHistory.length>1"> <div class="columns is-multiline" v-if="lastHistory.length>1">
<div class="column is-6-tablet" v-for="history in lastHistory" :key="history.id"> <div class="column is-6-tablet" v-for="history in lastHistory" :key="history.id">
<div class="card" :class="{ 'is-success': history.watched }"> <div class="card" :class="{ 'is-success': history.watched }">
<header class="card-header"> <header class="card-header">
<p class="card-header-title is-text-overflow"> <p class="card-header-title is-text-overflow">
<NuxtLink :to="`/history/${history.id}`" v-text="makeName(history)"/> <NuxtLink :to="`/history/${history.id}`" v-text="makeName(history)"/>
</p> </p>
<span class="card-header-icon"> <span class="card-header-icon">
<span class="icon" v-if="'episode' === history.type"><i class="fas fa-tv"></i></span> <span class="icon" v-if="'episode' === history.type"><i class="fas fa-tv"></i></span>
<span class="icon" v-else><i class="fas fa-film"></i></span> <span class="icon" v-else><i class="fas fa-film"></i></span>
</span> </span>
</header> </header>
<div class="card-content"> <div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered"> <div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-4-tablet is-6-mobile has-text-left-mobile"> <div class="column is-4-tablet is-6-mobile has-text-left-mobile">
<div class="is-text-overflow"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span> <span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip" <span class="has-tooltip"
v-tooltip="`Record updated at: ${moment.unix(history.updated_at).format(TOOLTIP_DATE_FORMAT)}`"> v-tooltip="`Record updated at: ${moment.unix(history.updated_at).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(history.updated_at).fromNow() }} {{ moment.unix(history.updated_at).fromNow() }}
</span> </span>
</div>
</div> </div>
</div> <div class="column is-4-tablet is-6-mobile has-text-right-mobile">
<div class="column is-4-tablet is-6-mobile has-text-right-mobile"> <div class="is-text-overflow">
<div class="is-text-overflow"> <span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span> <NuxtLink :to="'/backend/'+history.via" v-text="history.via"/>
<NuxtLink :to="'/backend/'+history.via" v-text="history.via"/> <span v-if="history?.metadata && Object.keys(history?.metadata).length > 1"
<span v-if="history?.metadata && Object.keys(history?.metadata).length > 1" v-tooltip="`Also reported by: ${Object.keys(history.metadata).filter(i => i !== history.via).join(', ')}.`">
v-tooltip="`Also reported by: ${Object.keys(history.metadata).filter(i => i !== history.via).join(', ')}.`"> (<span class="has-tooltip">+{{ Object.keys(history.metadata).length - 1 }}</span>)
(<span class="has-tooltip">+{{ Object.keys(history.metadata).length - 1 }}</span>) </span>
</span> </div>
</div> </div>
</div> <div class="column is-4-tablet is-12-mobile has-text-left-mobile">
<div class="column is-4-tablet is-12-mobile has-text-left-mobile"> <div class="is-text-overflow">
<div class="is-text-overflow"> <span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span> {{ history.event }}
{{ history.event }} </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="card-footer" v-if="history.progress">
<div class="card-footer" v-if="history.progress"> <div class="card-footer-item">
<div class="card-footer-item"> <span class="has-text-success" v-if="history.watched">Played</span>
<span class="has-text-success" v-if="history.watched">Played</span> <span class="has-text-danger" v-else>Unplayed</span>
<span class="has-text-danger" v-else>Unplayed</span> </div>
<div class="card-footer-item">{{ formatDuration(history.progress) }}</div>
</div> </div>
<div class="card-footer-item">{{ formatDuration(history.progress) }}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="column is-12" v-else>
<Message title="Warning" message_class="has-background-warning-90 has-text-dark"
icon="fas fa-exclamation-triangle"
message="No items were found. There are probably no items in the local database yet.">
</Message>
</div>
</div> </div>
<div class="column is-12" v-else>
<Message title="Warning" message_class="has-background-warning-90 has-text-dark"
icon="fas fa-exclamation-triangle"
message="No items were found. There are probably no items in the local database yet.">
</Message>
</div>
</div>
<div class="column is-12" v-for="log in logs" :key="log.filename"> <div class="column is-12" v-for="log in logs" :key="log.filename">
<h1 class="title is-4"> <h1 class="title is-4">
<span class="icon" v-if="'access' === log.type"><i class="fas fa-key"></i></span> <span class="icon" v-if="'access' === log.type"><i class="fas fa-key"></i></span>
<span class="icon" v-if="'task' === log.type"><i class="fas fa-tasks"></i></span> <span class="icon" v-if="'task' === log.type"><i class="fas fa-tasks"></i></span>
<span class="icon" v-if="'app' === log.type"><i class="fas fa-bugs"></i></span> <span class="icon" v-if="'app' === log.type"><i class="fas fa-bugs"></i></span>
<span class="icon" v-if="'webhook' === log.type"><i class="fas fa-book"></i></span> <span class="icon" v-if="'webhook' === log.type"><i class="fas fa-book"></i></span>
<NuxtLink :to="`/logs/${log.filename}`"> <NuxtLink :to="`/logs/${log.filename}`">
Latest {{ log.type }} logs Latest {{ log.type }} logs
</NuxtLink> </NuxtLink>
</h1> </h1>
<code class="box logs-container"> <code class="box logs-container">
<span class="is-block" v-for="(item, index) in log.lines" :key="log.filename + '-' + index"> <span class="is-block" v-for="(item, index) in log.lines" :key="log.filename + '-' + index">
{{ item }} {{ item }}
</span>
</code>
</div>
<div class="column is-12">
<div class="content">
<Message title="Welcome" message_class="has-background-info-90 has-text-dark" icon="fas fa-heart">
If you have question, or want clarification on something, or just want to chat with other users, you are
welcome to join our <span class="icon-text is-underlined">
<span class="icon"><i class="fas fa-brands fa-discord"></i></span>
<span>
<NuxtLink to="https://discord.gg/haUXHJyj6Y" target="_blank" v-text="'Discord server'"/>
</span> </span>
</span>. For bug reports, feature requests, or contributions, please visit the </code>
<span class="icon-text is-underlined"> </div>
<span class="icon"><i class="fas fa-brands fa-github"></i></span>
<div class="column is-12">
<div class="content">
<Message title="Welcome" message_class="has-background-info-90 has-text-dark" icon="fas fa-heart">
If you have question, or want clarification on something, or just want to chat with other users, you are
welcome to join our <span class="icon-text is-underlined">
<span class="icon"><i class="fas fa-brands fa-discord"></i></span>
<span> <span>
<NuxtLink to="https://github.com/arabcoders/watchstate/issues/new/choose" target="_blank" <NuxtLink to="https://discord.gg/haUXHJyj6Y" target="_blank" v-text="'Discord server'"/>
v-text="'GitHub repository'"/>
</span> </span>
</span>. </span>. For bug reports, feature requests, or contributions, please visit the
</Message> <span class="icon-text is-underlined">
<span class="icon"><i class="fas fa-brands fa-github"></i></span>
<span>
<NuxtLink to="https://github.com/arabcoders/watchstate/issues/new/choose" target="_blank"
v-text="'GitHub repository'"/>
</span>
</span>.
</Message>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -116,10 +118,10 @@
</style> </style>
<script setup> <script setup>
import request from '~/utils/request.js' import request from '~/utils/request'
import moment from 'moment' import moment from 'moment'
import Message from '~/components/Message.vue' import Message from '~/components/Message'
import {formatDuration, makeName, TOOLTIP_DATE_FORMAT} from '../utils/index.js' import {formatDuration, makeName, TOOLTIP_DATE_FORMAT} from '../utils/index'
useHead({title: 'Index'}) useHead({title: 'Index'})

View File

@@ -1,67 +1,70 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-globe"></i>&nbsp;</span> <span class="title is-4">
<NuxtLink to="/logs">Logs</NuxtLink> <span class="icon"><i class="fas fa-globe"></i>&nbsp;</span>
: {{ filename }} <NuxtLink to="/logs">Logs</NuxtLink>
</span> : {{ filename }}
</span>
<div class="is-pulled-right" v-if="!error"> <div class="is-pulled-right" v-if="!error">
<div class="field is-grouped"> <div class="field is-grouped">
<p class="control"> <p class="control">
<button class="button is-danger" v-tooltip.bottom="'Delete Logfile.'" @click="deleteFile"> <button class="button is-danger" v-tooltip.bottom="'Delete Logfile.'" @click="deleteFile">
<span class="icon"><i class="fas fa-trash"></i></span> <span class="icon"><i class="fas fa-trash"></i></span>
</button> </button>
</p> </p>
<p class="control"> <p class="control">
<button class="button is-danger is-light" v-tooltip.bottom="'Download the entire logfile.'" <button class="button is-danger is-light" v-tooltip.bottom="'Download the entire logfile.'"
@click="downloadFile" :class="{'is-loading':isDownloading}"> @click="downloadFile" :class="{'is-loading':isDownloading}">
<span class="icon"><i class="fas fa-download"></i></span> <span class="icon"><i class="fas fa-download"></i></span>
</button> </button>
</p> </p>
<p class="control" v-if="filename.includes(moment().format('YYYYMMDD'))"> <p class="control" v-if="filename.includes(moment().format('YYYYMMDD'))">
<button class="button" v-tooltip.bottom="'Watch log'" @click="watchLog" <button class="button" v-tooltip.bottom="'Watch log'" @click="watchLog"
:class="{'is-primary':!stream,'is-danger':stream}"> :class="{'is-primary':!stream,'is-danger':stream}">
<span class="icon"><i class="fas fa-stream"></i></span> <span class="icon"><i class="fas fa-stream"></i></span>
</button> </button>
</p> </p>
<p class="control"> <p class="control">
<button class="button is-warning" @click="wrapLines = !wrapLines" v-tooltip.bottom="'Toggle wrap line'"> <button class="button is-warning" @click="wrapLines = !wrapLines" v-tooltip.bottom="'Toggle wrap line'">
<span class="icon"><i class="fas fa-text-width"></i></span> <span class="icon"><i class="fas fa-text-width"></i></span>
</button> </button>
</p> </p>
<p class="control"> <p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading" :class="{'is-loading':isLoading}"> <button class="button is-info" @click="loadContent" :disabled="isLoading"
<span class="icon"><i class="fas fa-sync"></i></span> :class="{'is-loading':isLoading}">
</button> <span class="icon"><i class="fas fa-sync"></i></span>
</p> </button>
</p>
</div>
</div> </div>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12">
<div class="notification has-background-info-90 has-text-dark" v-if="stream"> <div class="notification has-background-info-90 has-text-dark" v-if="stream">
<button class="delete" @click="watchLog"></button> <button class="delete" @click="watchLog"></button>
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-spinner fa-pulse"></i></span> <span class="icon"><i class="fas fa-spinner fa-pulse"></i></span>
<span>Streaming log content...</span> <span>Streaming log content...</span>
</span> </span>
</div>
<code ref="logContainer" class="box logs-container" v-if="!error"
:class="{'is-pre': !wrapLines, 'is-pre-wrap': wrapLines}">
<span class="is-log-line is-block pt-1" v-for="(item, index) in data" :key="'log_line-'+index">
{{ item }}
</span>
</code>
<Message v-if="error" title="API Error" message_class="has-background-warning-90 has-text-dark"
:message="error" :use-close="true" @close="router.push('/logs')"/>
</div> </div>
<code ref="logContainer" class="box logs-container" v-if="!error"
:class="{'is-pre': !wrapLines, 'is-pre-wrap': wrapLines}">
<span class="is-log-line is-block pt-1" v-for="(item, index) in data" :key="'log_line-'+index">
{{ item }}
</span>
</code>
<Message v-if="error" title="API Error" message_class="has-background-warning-90 has-text-dark"
:message="error" :use-close="true" @close="router.push('/logs')"/>
</div> </div>
</div> </div>
</template> </template>
@@ -75,12 +78,11 @@
</style> </style>
<script setup> <script setup>
import Message from '~/components/Message'
import Message from '~/components/Message.vue'
import moment from 'moment' import moment from 'moment'
import {useStorage} from '@vueuse/core' import {useStorage} from '@vueuse/core'
import {notification} from '~/utils/index.js' import {notification} from '~/utils/index'
import request from '~/utils/request.js' import request from '~/utils/request'
const router = useRouter() const router = useRouter()
const filename = useRoute().params.filename const filename = useRoute().params.filename

View File

@@ -1,65 +1,69 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix">
<span class="icon"><i class="fas fa-globe"></i></span> <span class="title is-4">
Logs <span class="icon"><i class="fas fa-globe"></i></span>
</span> Logs
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading" :class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</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> </span>
</div> <div class="is-pulled-right">
</div> <div class="field is-grouped">
<p class="control">
<div class="column is-12" v-if="logs.length < 1 || isLoading"> <button class="button is-info" @click="loadContent" :disabled="isLoading"
<Message v-if="isLoading" message_class="is-background-info-90 has-text-dark" icon="fas fa-spinner fa-spin" :class="{'is-loading':isLoading}">
title="Loading" message="Loading data. Please wait..."/> <span class="icon"><i class="fas fa-sync"></i></span>
<Message v-else title="Warning" message_class="is-background-warning-80 has-text-dark" </button>
icon="fas fa-exclamation-triangle" message="No logs files found."/> </p>
</div>
<div class="column is-4-tablet" v-for="(item, index) in logs" :key="'log-'+index">
<div class="card">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<NuxtLink :to="'/logs/'+item.filename">{{ item.filename ?? item.date }}</NuxtLink>
</p>
<span class="card-header-icon">
<span class="icon" v-if="'access' === item.type"><i class="fas fa-key"></i></span>
<span class="icon" v-if="'task' === item.type"><i class="fas fa-tasks"></i></span>
<span class="icon" v-if="'app' === item.type"><i class="fas fa-bugs"></i></span>
<span class="icon" v-if="'webhook' === item.type"><i class="fas fa-book"></i></span>
</span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-6-mobile is-pre">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip" v-tooltip="`Last Update: ${moment(item.modified).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment(item.modified).fromNow() }}
</span>
</div>
<div class="column is-6-mobile">
<span class="icon"><i class="fas fa-hdd"></i>&nbsp;</span>
<span>{{ humanFileSize(item.size) }}</span>
</div>
<div class="column is-6-mobile">
<span class="icon"><i class="fas fa-tag"></i>&nbsp;</span>
<span class="is-capitalized">{{ item.type }}</span>
</div>
</div> </div>
</div> </div>
<div class="card-footer"> <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-12" v-if="logs.length < 1 || isLoading">
<Message v-if="isLoading" message_class="is-background-info-90 has-text-dark" icon="fas fa-spinner fa-spin"
title="Loading" message="Loading data. Please wait..."/>
<Message v-else title="Warning" message_class="is-background-warning-80 has-text-dark"
icon="fas fa-exclamation-triangle" message="No logs files found."/>
</div>
<div class="column is-4-tablet" v-for="(item, index) in logs" :key="'log-'+index">
<div class="card">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<NuxtLink :to="'/logs/'+item.filename">{{ item.filename ?? item.date }}</NuxtLink>
</p>
<span class="card-header-icon">
<span class="icon" v-if="'access' === item.type"><i class="fas fa-key"></i></span>
<span class="icon" v-if="'task' === item.type"><i class="fas fa-tasks"></i></span>
<span class="icon" v-if="'app' === item.type"><i class="fas fa-bugs"></i></span>
<span class="icon" v-if="'webhook' === item.type"><i class="fas fa-book"></i></span>
</span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-6-mobile is-pre">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`Last Update: ${moment(item.modified).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment(item.modified).fromNow() }}
</span>
</div>
<div class="column is-6-mobile">
<span class="icon"><i class="fas fa-hdd"></i>&nbsp;</span>
<span>{{ humanFileSize(item.size) }}</span>
</div>
<div class="column is-6-mobile">
<span class="icon"><i class="fas fa-tag"></i>&nbsp;</span>
<span class="is-capitalized">{{ item.type }}</span>
</div>
</div>
</div>
<div class="card-footer">
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -67,10 +71,10 @@
</template> </template>
<script setup> <script setup>
import request from "~/utils/request.js"; import request from '~/utils/request'
import moment from "moment"; import moment from 'moment'
import {humanFileSize, TOOLTIP_DATE_FORMAT} from "~/utils/index.js"; import {humanFileSize, TOOLTIP_DATE_FORMAT} from '~/utils/index'
import Message from "~/components/Message.vue"; import Message from '~/components/Message'
useHead({title: 'Logs'}) useHead({title: 'Logs'})

View File

@@ -1,267 +1,269 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4 "> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-database"></i></span> <span class="title is-4 ">
Data Parity <span class="icon"><i class="fas fa-database"></i></span>
</span> Data Parity
<div class="is-pulled-right"> </span>
<div class="field is-grouped"> <div class="is-pulled-right">
<div class="control has-icons-left" v-if="showFilter"> <div class="field is-grouped">
<input type="search" v-model.lazy="filter" class="input" id="filter" <div class="control has-icons-left" v-if="showFilter">
placeholder="Filter displayed results."> <input type="search" v-model.lazy="filter" class="input" id="filter"
<span class="icon is-left"> placeholder="Filter displayed results.">
<i class="fas fa-filter"></i> <span class="icon is-left">
</span> <i class="fas fa-filter"></i>
</div> </span>
</div>
<div class="control"> <div class="control">
<button class="button is-danger is-light" @click="toggleFilter"> <button class="button is-danger is-light" @click="toggleFilter">
<span class="icon"><i class="fas fa-filter"></i></span> <span class="icon"><i class="fas fa-filter"></i></span>
</button>
</div>
<div class="control" v-if="min && max" v-tooltip.bottom="'Minimum number of backends'">
<div class="select">
<select v-model="min" :disabled="isDeleting || isLoading">
<option v-for="i in numberRange(1,max+1)" :key="`min-${i}`" :value="i">
{{ i }}
</option>
</select>
</div>
</div>
<p class="control">
<button class="button is-danger" @click="deleteData" v-tooltip.bottom="'Delete The reported records'"
:disabled="isDeleting || isLoading || items.length<1" :class="{'is-loading':isDeleting}">
<span class="icon"><i class="fas fa-trash"></i></span>
</button>
</p>
<div class="control">
<button class="button is-info is-light" @click="selectAll = !selectAll"
data-tooltip="Toggle select all">
<span class="icon">
<i class="fas fa-check-square"
:class="{'fa-check-square': !selectAll,'fa-square':selectAll}"></i>
</span>
</button>
</div>
<p class="control">
<button class="button is-info" @click.prevent="loadContent(page, true, true)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">This page shows local database records not being reported by the specified number of
backends.</span>
</div>
</div>
<div class="column is-12" v-if="total && last_page > 1">
<div class="field is-grouped">
<div class="control" v-if="page !== 1">
<button rel="first" class="button" @click="loadContent(1)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span><<</span>
</button> </button>
</div> </div>
<div class="control" v-if="page > 1 && (page-1) !== 1">
<div class="control" v-if="min && max" v-tooltip.bottom="'Minimum number of backends'"> <button rel="prev" class="button" @click="loadContent(page-1)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span><</span>
</button>
</div>
<div class="control">
<div class="select"> <div class="select">
<select v-model="min" :disabled="isDeleting || isLoading"> <select v-model="page" @change="loadContent(page)" :disabled="isLoading">
<option v-for="i in numberRange(1,max+1)" :key="`min-${i}`" :value="i"> <option v-for="(item, index) in makePagination(page, last_page)" :key="index" :value="item.page">
{{ i }} {{ item.text }}
</option> </option>
</select> </select>
</div> </div>
</div> </div>
<p class="control"> <div class="control" v-if="page !== last_page && (page+1) !== last_page">
<button class="button is-danger" @click="deleteData" v-tooltip.bottom="'Delete The reported records'" <button rel="next" class="button" @click="loadContent(page+1)" :disabled="isLoading"
:disabled="isDeleting || isLoading || items.length<1" :class="{'is-loading':isDeleting}">
<span class="icon"><i class="fas fa-trash"></i></span>
</button>
</p>
<div class="control">
<button class="button is-info is-light" @click="selectAll = !selectAll"
data-tooltip="Toggle select all">
<span class="icon">
<i class="fas fa-check-square"
:class="{'fa-check-square': !selectAll,'fa-square':selectAll}"></i>
</span>
</button>
</div>
<p class="control">
<button class="button is-info" @click.prevent="loadContent(page, true, true)" :disabled="isLoading"
:class="{'is-loading':isLoading}"> :class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-sync"></i></span> <span>></span>
</button>
</div>
<div class="control" v-if="page !== last_page">
<button rel="last" class="button" @click="loadContent(last_page)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span>>></span>
</button> </button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">This page shows local database records not being reported by the specified number of
backends.</span>
</div>
</div>
<div class="column is-12" v-if="total && last_page > 1">
<div class="field is-grouped">
<div class="control" v-if="page !== 1">
<button rel="first" class="button" @click="loadContent(1)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span><<</span>
</button>
</div>
<div class="control" v-if="page > 1 && (page-1) !== 1">
<button rel="prev" class="button" @click="loadContent(page-1)" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span><</span>
</button>
</div>
<div class="control">
<div class="select">
<select v-model="page" @change="loadContent(page)" :disabled="isLoading">
<option v-for="(item, index) in makePagination(page, last_page)" :key="index" :value="item.page">
{{ item.text }}
</option>
</select>
</div> </div>
</div> </div>
<div class="control" v-if="page !== last_page && (page+1) !== last_page"> </div>
<button rel="next" class="button" @click="loadContent(page+1)" :disabled="isLoading"
:class="{'is-loading':isLoading}"> <div class="column is-12" v-if="selected_ids.length > 0">
<span>></span> <div class="field is-grouped is-justify-content-center">
</button> <div class="control">
</div> <button class="button is-danger" @click="massDelete()" :disabled="massActionInProgress"
<div class="control" v-if="page !== last_page"> :class="{'is-loading':massActionInProgress}">
<button rel="last" class="button" @click="loadContent(last_page)" :disabled="isLoading" <span class="icon"><i class="fas fa-trash"></i></span>
:class="{'is-loading':isLoading}"> <span class="is-hidden-mobile">Delete '{{ selected_ids.length }}' selected item/s</span>
<span>>></span> </button>
</button> </div>
</div> </div>
</div> </div>
</div>
<div class="column is-12" v-if="selected_ids.length > 0"> <div class="column is-12">
<div class="field is-grouped is-justify-content-center"> <div class="columns is-multiline" v-if="filteredRows(items)?.length>0">
<div class="control"> <template v-for="item in items" :key="item.id">
<button class="button is-danger" @click="massDelete()" :disabled="massActionInProgress" <Lazy :unrender="true" :min-height="343" class="column is-6-tablet" v-if="filterItem(item)">
:class="{'is-loading':massActionInProgress}"> <div class="card" :class="{ 'is-success': item.watched }">
<span class="icon"><i class="fas fa-trash"></i></span> <header class="card-header">
<span class="is-hidden-mobile">Delete '{{ selected_ids.length }}' selected item/s</span> <p class="card-header-title is-text-overflow pr-1">
</button> <span class="icon">
</div> <label class="checkbox">
</div> <input type="checkbox" :value="item.id" v-model="selected_ids">
</div> </label>&nbsp;
</span>
<div class="column is-12"> <NuxtLink :to="'/history/'+item.id" v-text="makeName(item)"/>
<div class="columns is-multiline" v-if="filteredRows(items)?.length>0"> </p>
<template v-for="item in items" :key="item.id"> <span class="card-header-icon" @click="item.showRawData = !item?.showRawData">
<Lazy :unrender="true" :min-height="343" class="column is-6-tablet" v-if="filterItem(item)"> <span class="icon">
<div class="card" :class="{ 'is-success': item.watched }"> <i class="fas"
<header class="card-header"> :class="{ 'fa-tv': 'episode' === item.type.toLowerCase(), 'fa-film': 'movie' === item.type.toLowerCase()}"></i>
<p class="card-header-title is-text-overflow pr-1"> </span>
<span class="icon">
<label class="checkbox">
<input type="checkbox" :value="item.id" v-model="selected_ids">
</label>&nbsp;
</span> </span>
<NuxtLink :to="'/history/'+item.id" v-text="makeName(item)"/> </header>
</p> <div class="card-content">
<span class="card-header-icon" @click="item.showRawData = !item?.showRawData"> <div class="columns is-multiline is-mobile">
<span class="icon"> <div class="column is-12">
<i class="fas" <div class="field is-grouped">
:class="{ 'fa-tv': 'episode' === item.type.toLowerCase(), 'fa-film': 'movie' === item.type.toLowerCase()}"></i> <div class="control is-clickable"
</span> :class="{'is-text-overflow': !item?.expand_title, 'is-text-contents': item?.expand_title}"
</span> @click="item.expand_title = !item?.expand_title">
</header> <span class="icon"><i class="fas fa-heading"></i>&nbsp;</span>
<div class="card-content"> <template v-if="item?.content_title">
<div class="columns is-multiline is-mobile"> <NuxtLink :to="makeSearchLink('subtitle', item.content_title)" v-text="item.content_title"/>
<div class="column is-12"> </template>
<div class="field is-grouped"> <template v-else>
<div class="control is-clickable" <NuxtLink :to="makeSearchLink('subtitle', item.title)" v-text="item.title"/>
:class="{'is-text-overflow': !item?.expand_title, 'is-text-contents': item?.expand_title}" </template>
@click="item.expand_title = !item?.expand_title"> </div>
<span class="icon"><i class="fas fa-heading"></i>&nbsp;</span> <div class="control">
<template v-if="item?.content_title"> <span class="icon is-clickable"
<NuxtLink :to="makeSearchLink('subtitle', item.content_title)" v-text="item.content_title"/> @click="copyText(item?.content_title ?? item.title, false)">
</template> <i class="fas fa-copy"></i></span>
<template v-else> </div>
<NuxtLink :to="makeSearchLink('subtitle', item.title)" v-text="item.title"/>
</template>
</div>
<div class="control">
<span class="icon is-clickable"
@click="copyText(item?.content_title ?? item.title, false)">
<i class="fas fa-copy"></i></span>
</div> </div>
</div> </div>
</div> <div class="column is-12">
<div class="column is-12"> <div class="field is-grouped">
<div class="field is-grouped"> <div class="control is-clickable"
<div class="control is-clickable" :class="{'is-text-overflow': !item?.expand_path, 'is-text-contents': item?.expand_path}"
:class="{'is-text-overflow': !item?.expand_path, 'is-text-contents': item?.expand_path}" @click="item.expand_path = !item?.expand_path">
@click="item.expand_path = !item?.expand_path"> <span class="icon"><i class="fas fa-file"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-file"></i>&nbsp;</span> <NuxtLink v-if="item?.content_path" :to="makeSearchLink('path', item.content_path)"
<NuxtLink v-if="item?.content_path" :to="makeSearchLink('path', item.content_path)" v-text="item.content_path"/>
v-text="item.content_path"/> <span v-else>No path found.</span>
<span v-else>No path found.</span> </div>
</div> <div class="control">
<div class="control"> <span class="icon is-clickable"
<span class="icon is-clickable" @click="copyText(item?.content_path ?item.content_path : null, false)">
@click="copyText(item?.content_path ?item.content_path : null, false)"> <i class="fas fa-copy"></i></span>
<i class="fas fa-copy"></i></span> </div>
</div> </div>
</div> </div>
</div> <div class="column is-12">
<div class="column is-12"> <div class="field is-grouped">
<div class="field is-grouped"> <div class="control is-expanded is-unselectable">
<div class="control is-expanded is-unselectable"> <span class="icon"><i class="fas fa-info"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-info"></i>&nbsp;</span> <span>Has metadata from</span>
<span>Has metadata from</span> </div>
</div> <div class="control">
<div class="control"> <NuxtLink v-for="backend in item.reported_by" :key="`${item.id}-rb-${backend}`"
<NuxtLink v-for="backend in item.reported_by" :key="`${item.id}-rb-${backend}`" :to="'/backend/'+backend" v-text="backend" class="tag is-primary ml-1"/>
:to="'/backend/'+backend" v-text="backend" class="tag is-primary ml-1"/> <NuxtLink v-for="backend in item.not_reported_by" :key="`${item.id}-nrb-${backend}`"
<NuxtLink v-for="backend in item.not_reported_by" :key="`${item.id}-nrb-${backend}`" :to="'/backend/'+backend" v-text="backend" class="tag is-danger ml-1"/>
:to="'/backend/'+backend" v-text="backend" class="tag is-danger ml-1"/> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="card-content p-0 m-0" v-if="item?.showRawData">
<div class="card-content p-0 m-0" v-if="item?.showRawData">
<pre style="position: relative; max-height: 343px;"><code>{{ JSON.stringify(item, null, 2) }}</code> <pre style="position: relative; max-height: 343px;"><code>{{ JSON.stringify(item, null, 2) }}</code>
<button class="button is-small m-4" @click="() => copyText(JSON.stringify(item, null, 2))" <button class="button is-small m-4" @click="() => copyText(JSON.stringify(item, null, 2))"
style="position: absolute; top:0; right:0;"> style="position: absolute; top:0; right:0;">
<span class="icon"><i class="fas fa-copy"></i></span> <span class="icon"><i class="fas fa-copy"></i></span>
</button> </button>
</pre> </pre>
</div>
<div class="card-footer">
<div class="card-footer-item">
<span class="icon">
<i class="fas" :class="{'fa-eye':item.watched,'fa-eye-slash':!item.watched}"></i>&nbsp;
</span>
<span class="has-text-success" v-if="item.watched">Played</span>
<span class="has-text-danger" v-else>Unplayed</span>
</div> </div>
<div class="card-footer-item"> <div class="card-footer">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span> <div class="card-footer-item">
<span class="has-tooltip" <span class="icon">
v-tooltip="`Record updated at: ${moment.unix(item.updated_at).format(TOOLTIP_DATE_FORMAT)}`"> <i class="fas" :class="{'fa-eye':item.watched,'fa-eye-slash':!item.watched}"></i>&nbsp;
{{ moment.unix(item.updated_at).fromNow() }} </span>
</span> <span class="has-text-success" v-if="item.watched">Played</span>
<span class="has-text-danger" v-else>Unplayed</span>
</div>
<div class="card-footer-item">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip"
v-tooltip="`Record updated at: ${moment.unix(item.updated_at).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(item.updated_at).fromNow() }}
</span>
</div>
</div> </div>
</div> </div>
</div> </lazy>
</lazy> </template>
</template> </div>
</div>
<div class="column is-12" v-else> <div class="column is-12" v-else>
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading" <Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/> icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
<template v-else> <template v-else>
<Message message_class="has-background-warning-80 has-text-dark" v-if="filter && items.length > 1" <Message message_class="has-background-warning-80 has-text-dark" v-if="filter && items.length > 1"
title="Information" title="Information"
icon="fas fa-check"> icon="fas fa-check">
The filter <code>{{ filter }}</code> did not match any records. The filter <code>{{ filter }}</code> did not match any records.
</Message> </Message>
<Message message_class="has-background-success-90 has-text-dark" v-if="!filter || items.length < 1" <Message message_class="has-background-success-90 has-text-dark" v-if="!filter || items.length < 1"
title="Success" title="Success"
icon="fas fa-check"> icon="fas fa-check">
WatchState did not find any records matching the criteria. All records has at least <code>{{ min }}</code> WatchState did not find any records matching the criteria. All records has at least <code>{{ min }}</code>
backends reporting it. backends reporting it.
</Message> </Message>
</template> </template>
</div> </div>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> @toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul> <ul>
<li> <li>
You can specify the minimum number of backends that need to report the record to be considered valid. You can specify the minimum number of backends that need to report the record to be considered valid.
</li> </li>
<li> <li>
By clicking the <span class="fa fa-trash"></span> icon you will delete the the reported items from the By clicking the <span class="fa fa-trash"></span> icon you will delete the the reported items from the
local database. If the items are not fixed by the time <code>import</code> is run, they will re-appear. local database. If the items are not fixed by the time <code>import</code> is run, they will re-appear.
</li> </li>
<li> <li>
Deleting records works by deleting everything at or below the specified number of backends. For example, Deleting records works by deleting everything at or below the specified number of backends. For example,
if you set the minimum to <code>3</code>, all records that are reported by <code>3</code> or fewer if you set the minimum to <code>3</code>, all records that are reported by <code>3</code> or fewer
backends will be deleted. backends will be deleted.
</li> </li>
<li> <li>
Records showing here most likely means your backends, are not reporting same data. This could be due to Records showing here most likely means your backends, are not reporting same data. This could be due to
many reasons, including using different external databases i.e. <code>TheMovieDB</code> vs many reasons, including using different external databases i.e. <code>TheMovieDB</code> vs
<code>TheTVDB</code>. <code>TheTVDB</code>.
</li> </li>
<li> <li>
The results are cached in your browser temporarily to provide faster response, as the operation to The results are cached in your browser temporarily to provide faster response, as the operation to
generate the report is quite intensive. If you want to refresh the data, click the <span generate the report is quite intensive. If you want to refresh the data, click the <span
class="fa fa-sync"></span> icon. class="fa fa-sync"></span> icon.
</li> </li>
</ul> </ul>
</Message> </Message>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,53 +1,55 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-flag"></i></span> <span class="title is-4">
System Report <span class="icon"><i class="fas fa-flag"></i></span>
</span> System Report
<div class="is-pulled-right" v-if="false === show_report_warning"> </span>
<div class="field is-grouped"> <div class="is-pulled-right" v-if="false === show_report_warning">
<p class="control"> <div class="field is-grouped">
<button class="button is-primary" @click="copyText(data.join('\n'))" v-tooltip.bottom="'Copy Report'"> <p class="control">
<span class="icon"><i class="fas fa-copy"></i></span> <button class="button is-primary" @click="copyText(data.join('\n'))" v-tooltip.bottom="'Copy Report'">
</button> <span class="icon"><i class="fas fa-copy"></i></span>
</p> </button>
</p>
</div>
</div>
<div class="subtitle is-hidden-mobile">
This page shows basic information about the various components of the system.
</div> </div>
</div> </div>
<div class="subtitle is-hidden-mobile">
This page shows basic information about the various components of the system.
</div>
</div>
<div class="column is-12"> <div class="column is-12">
<template v-if="show_report_warning"> <template v-if="show_report_warning">
<Message message_class="has-background-warning-80 has-text-dark" title="Warning" <Message message_class="has-background-warning-80 has-text-dark" title="Warning"
icon="fas fa-exclamation-triangle"> icon="fas fa-exclamation-triangle">
While we try to make sure no sensitive information is leaked via the report, it's possible that something 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 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. report it to the developers. so we can fix it.
</Message> </Message>
<div class="mt-4 has-text-centered"> <div class="mt-4 has-text-centered">
<NuxtLink class="is-block is-fullwidth is-primary" @click="show_report_warning = false"> <NuxtLink class="is-block is-fullwidth is-primary" @click="show_report_warning = false">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-thumbs-up"></i></span> <span class="icon"><i class="fas fa-thumbs-up"></i></span>
<span>I Understand. Show me the report.</span> <span>I Understand. Show me the report.</span>
</span> </span>
</NuxtLink> </NuxtLink>
</div> </div>
</template> </template>
<Message message_class="has-background-info-90 has-text-dark" v-if="!show_report_warning && data.length < 1" <Message message_class="has-background-info-90 has-text-dark" v-if="!show_report_warning && data.length < 1"
title="Loading" icon="fas fa-spinner fa-spin" message="Generating the report. Please wait..."/> title="Loading" icon="fas fa-spinner fa-spin" message="Generating the report. Please wait..."/>
<template v-if="!show_report_warning && data.length > 0"> <template v-if="!show_report_warning && data.length > 0">
<pre style="min-height: 60vh;max-height:70vh; overflow-y: scroll" id="report-content" <pre style="min-height: 60vh;max-height:70vh; overflow-y: scroll" id="report-content"
><code><span v-for="(item, index) in data" :key="index" class="is-block">{{ item }}</span></code></pre> ><code><span v-for="(item, index) in data" :key="index" class="is-block">{{ item }}</span></code></pre>
</template> </template>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {copyText} from '~/utils/index.js' import {copyText} from '~/utils/index'
useHead({title: `System Report`}) useHead({title: `System Report`})

View File

@@ -1,61 +1,63 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-redo"></i></span> <span class="title is-4">
System reset <span class="icon"><i class="fas fa-redo"></i></span>
</span> System reset
</span>
<div class="is-pulled-right"> <div class="is-pulled-right">
<div class="field is-grouped"></div> <div class="field is-grouped"></div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">Reset the system state.</span>
</div>
</div> </div>
<div class="is-hidden-mobile"> <div class="column is-12" v-if="error">
<span class="subtitle">Reset the system state.</span> <Message message_class="is-background-warning-80 has-text-dark" title="Error" icon="fas fa-exclamation-circle"
:use-close="true" @close="navigateTo('/backends')"
:message="`${error.error.code}: ${error.error.message}`"/>
</div> </div>
<template v-if="isResetting">
<div class="column is-12">
<Message message_class="has-background-warning-90 has-text-dark" title="Working..."
icon="fas fa-spin fa-exclamation-triangle" message="Reset in progress, Please wait..."/>
</div>
</template>
<template v-else>
<div class="column is-12">
<Message message_class="is-background-warning-80 has-text-dark" title="Important information"
icon="fas fa-exclamation-triangle">
<p>
Are you sure you want to reset the system state? This operation will remove all records and metadata from
the database. This action is irreversible.
</p>
<h5 class="has-text-dark">This operation will do the following</h5>
<ul>
<li>Remove all data from local database.</li>
<li>Attempt to flush the cache.</li>
<li>Reset the backends last sync date.</li>
</ul>
<p>There is no undo operation. This action is irreversible.</p>
</Message>
</div>
<div class="column is-12">
<Confirm @confirmed="resetSystem()"
title="Perform system reset"
title-icon="fa-redo"
warning="Depending on your hardware speed, the reset operation might take long time. do not interrupt the process, or close the browser tab. You will be redirected to the index page automatically once the process is complete. Otherwise, you might end up with a corrupted database and/or state."
/>
</div>
</template>
</div> </div>
<div class="column is-12" v-if="error">
<Message message_class="is-background-warning-80 has-text-dark" title="Error" icon="fas fa-exclamation-circle"
:use-close="true" @close="navigateTo('/backends')"
:message="`${error.error.code}: ${error.error.message}`"/>
</div>
<template v-if="isResetting">
<div class="column is-12">
<Message message_class="has-background-warning-90 has-text-dark" title="Working..."
icon="fas fa-spin fa-exclamation-triangle" message="Reset in progress, Please wait..."/>
</div>
</template>
<template v-else>
<div class="column is-12">
<Message message_class="is-background-warning-80 has-text-dark" title="Important information"
icon="fas fa-exclamation-triangle">
<p>
Are you sure you want to reset the system state? This operation will remove all records and metadata from
the database. This action is irreversible.
</p>
<h5 class="has-text-dark">This operation will do the following</h5>
<ul>
<li>Remove all data from local database.</li>
<li>Attempt to flush the cache.</li>
<li>Reset the backends last sync date.</li>
</ul>
<p>There is no undo operation. This action is irreversible.</p>
</Message>
</div>
<div class="column is-12">
<Confirm @confirmed="resetSystem()"
title="Perform system reset"
title-icon="fa-redo"
warning="Depending on your hardware speed, the reset operation might take long time. do not interrupt the process, or close the browser tab. You will be redirected to the index page automatically once the process is complete. Otherwise, you might end up with a corrupted database and/or state."
/>
</div>
</template>
</div> </div>
</template> </template>

View File

@@ -1,224 +1,230 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span id="env_page_title" class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fa fa-bug-slash"></i></span> <span id="env_page_title" class="title is-4">
Log Suppressor <span class="icon"><i class="fa fa-bug-slash"></i></span>
</span> Log Suppressor
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<button class="button is-primary" v-tooltip.bottom="'Add new rule'" @click="toggleForm = !toggleForm">
<span class="icon">
<i class="fa fa-add"></i>
</span>
</button>
</p>
<p class="control">
<button class="button is-info" @click="loadContent">
<span class="icon"><i class="fa fa-sync"></i></span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
This page allow you to suppress some logs from being shown/recorded.
</span> </span>
</div> <div class="is-pulled-right">
</div> <div class="field is-grouped">
<p class="control">
<div class="column is-12" v-if="toggleForm"> <button class="button is-primary" v-tooltip.bottom="'Add new rule'" @click="toggleForm = !toggleForm">
<form id="page_form" @submit.prevent="sendData"> <span class="icon">
<div class="card"> <i class="fa fa-add"></i>
<header class="card-header"> </span>
<p class="card-header-title is-unselectable is-justify-center"> </button>
<template v-if="formData.id">
Edit suppression rule
</template>
<template v-else>
Add new suppression rule
</template>
</p> </p>
</header> <p class="control">
<button class="button is-info" @click="loadContent">
<div class="card-content"> <span class="icon"><i class="fa fa-sync"></i></span>
<div class="field">
<label class="label is-unselectable" for="form_type">Matching type</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="formData.type" id="form_type" :disabled="formData?.id" class="is-capitalized">
<option value="" disabled>Select Type</option>
<option v-for="type in types" :key="`form-${type}`" :value="type">
{{ type }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fa fa-microchip"></i>
</div>
</div>
</div>
<div class="field">
<label class="label is-unselectable" for="form_rule">Rule</label>
<div class="control has-icons-left">
<input class="input" id="form_rule" type="text" v-model="formData.rule"
:placeholder="'regex' === formData.type ? '/this match \d+/is' : 'hide_me'">
<div class="icon is-small is-left">
<i class="fa"
:class="{ 'fa-code': 'regex' === formData.type, 'fa-heading': 'contains' === formData.type }"></i>
</div>
<p class="help">
<template v-if="'regex' === formData.type">
Regular expression. To test try
<span>
<NuxtLink to="https://regex101.com/" target="_blank" v-text="'this link'"/>
</span><span></span>. Select <code>PCRE2 (PHP >=7.3)</code> flavor.
</template>
<template v-else>
Case sensitive string contains match.
</template>
</p>
</div>
</div>
<div class="field">
<label class="label is-unselectable" for="form_example">Example</label>
<div class="control has-icons-left">
<input class="input" id="form_example" type="text" v-model="formData.example"
placeholder="String example to test the rule against.">
<div class="icon is-small is-left"><i class="fa fa-font"></i></div>
<p class="help">
The example text must trigger the supplied rule. This is used to test the rule. if it's working as
expected.
</p>
</div>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<button class="button is-fullwidth is-primary" type="submit"
:disabled="!formData?.rule || !formData.example">
<span class="icon-text">
<span class="icon"><i class="fa fa-save"></i></span>
<span>Save</span>
</span>
</button> </button>
</div> </p>
<div class="card-footer-item">
<button class="button is-fullwidth is-danger" type="button"
@click="cancelForm">
<span class="icon-text">
<span class="icon"><i class="fa fa-cancel"></i></span>
<span>Cancel</span>
</span>
</button>
</div>
</div> </div>
</div> </div>
</form> <div class="is-hidden-mobile">
</div> <span class="subtitle">
This page allow you to suppress some logs from being shown/recorded.
</span>
</div>
</div>
<div class="column is-12" v-if="isLoading"> <div class="column is-12" v-if="toggleForm">
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin" <form id="page_form" @submit.prevent="sendData">
message="Loading data. Please wait..."/>
</div>
<div class="column is-12" v-if="false === isLoading && items.length<1">
<Message message_class="has-background-warning-90 has-text-dark" title="No suppression rules"
icon="fa fa-exclamation-triangle">
<p>
No suppression rules were found. To add a new rule click the <span class="is-clickable icon"><i
class="fa fa-add"></i></span> button on top right of this page.
</p>
</Message>
</div>
<div class="column is-12" v-if="items">
<div class="columns is-multiline">
<div class="column is-6" v-for="item in items" :key="item.key">
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title is-justify-center is-unselectable"> <p class="card-header-title is-unselectable is-justify-center">
<span class="icon"><i class="fa fa-microchip"></i></span> <template v-if="formData.id">
<span class="is-capitalized">{{ item.type }}</span> Edit suppression rule
</template>
<template v-else>
Add new suppression rule
</template>
</p> </p>
</header> </header>
<div class="card-content"> <div class="card-content">
<div class="columns is-multiline"> <div class="field">
<div class="column is-12"> <label class="label is-unselectable" for="form_type">Matching type</label>
<span class="icon"> <div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="formData.type" id="form_type" :disabled="formData?.id" class="is-capitalized">
<option value="" disabled>Select Type</option>
<option v-for="type in types" :key="`form-${type}`" :value="type">
{{ type }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fa fa-microchip"></i>
</div>
</div>
</div>
<div class="field">
<label class="label is-unselectable" for="form_rule">Rule</label>
<div class="control has-icons-left">
<input class="input" id="form_rule" type="text" v-model="formData.rule"
:placeholder="'regex' === formData.type ? '/this match \d+/is' : 'hide_me'">
<div class="icon is-small is-left">
<i class="fa" <i class="fa"
:class="{ 'fa-code': 'regex' === formData.type, 'fa-heading': 'contains' === formData.type }"></i> :class="{ 'fa-code': 'regex' === formData.type, 'fa-heading': 'contains' === formData.type }"></i>
</span> </div>
<code>{{ item.rule }}</code> <p class="help">
<template v-if="'regex' === formData.type">
Regular expression. To test try
<span>
<NuxtLink to="https://regex101.com/" target="_blank" v-text="'this link'"/>
</span><span></span>. Select <code>PCRE2 (PHP >=7.3)</code> flavor.
</template>
<template v-else>
Case sensitive string contains match.
</template>
</p>
</div> </div>
<div class="column is-12"> </div>
<span class="icon"><i class="fa fa-font"></i></span>
<code>{{ item.example }}</code> <div class="field">
<label class="label is-unselectable" for="form_example">Example</label>
<div class="control has-icons-left">
<input class="input" id="form_example" type="text" v-model="formData.example"
placeholder="String example to test the rule against.">
<div class="icon is-small is-left"><i class="fa fa-font"></i></div>
<p class="help">
The example text must trigger the supplied rule. This is used to test the rule. if it's working as
expected.
</p>
</div> </div>
</div> </div>
</div> </div>
<footer class="card-footer">
<div class="card-footer">
<div class="card-footer-item"> <div class="card-footer-item">
<button class="button is-primary is-fullwidth" @click="editItem(item)"> <button class="button is-fullwidth is-primary" type="submit"
:disabled="!formData?.rule || !formData.example">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fa fa-edit"></i></span> <span class="icon"><i class="fa fa-save"></i></span>
<span>Edit</span> <span>Save</span>
</span> </span>
</button> </button>
</div> </div>
<div class="card-footer-item"> <div class="card-footer-item">
<button class="button is-fullwidth is-danger" @click="deleteItem(item)"> <button class="button is-fullwidth is-danger" type="button"
@click="cancelForm">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fa fa-trash"></i></span> <span class="icon"><i class="fa fa-cancel"></i></span>
<span>Delete</span> <span>Cancel</span>
</span> </span>
</button> </button>
</div> </div>
</footer> </div>
</div>
</form>
</div>
<div class="column is-12" v-if="isLoading">
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
message="Loading data. Please wait..."/>
</div>
<div class="column is-12" v-if="false === isLoading && items.length<1">
<Message message_class="has-background-warning-90 has-text-dark" title="No suppression rules"
icon="fa fa-exclamation-triangle">
<p>
No suppression rules were found. To add a new rule click the <span class="is-clickable icon"><i
class="fa fa-add"></i></span> button on top right of this page.
</p>
</Message>
</div>
<div class="column is-12" v-if="items">
<div class="columns is-multiline">
<div class="column is-6" v-for="item in items" :key="item.key">
<div class="card">
<header class="card-header">
<p class="card-header-title is-justify-center is-unselectable">
<span class="icon"><i class="fa fa-microchip"></i></span>
<span class="is-capitalized">{{ item.type }}</span>
</p>
</header>
<div class="card-content">
<div class="columns is-multiline">
<div class="column is-12">
<span class="icon">
<i class="fa"
:class="{ 'fa-code': 'regex' === formData.type, 'fa-heading': 'contains' === formData.type }"></i>
</span>
<code>{{ item.rule }}</code>
</div>
<div class="column is-12">
<span class="icon"><i class="fa fa-font"></i></span>
<code>{{ item.example }}</code>
</div>
</div>
</div>
<footer class="card-footer">
<div class="card-footer-item">
<button class="button is-primary is-fullwidth" @click="editItem(item)">
<span class="icon-text">
<span class="icon"><i class="fa fa-edit"></i></span>
<span>Edit</span>
</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-fullwidth is-danger" @click="deleteItem(item)">
<span class="icon-text">
<span class="icon"><i class="fa fa-trash"></i></span>
<span>Delete</span>
</span>
</button>
</div>
</footer>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fa fa-info-circle"> @toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fa fa-info-circle">
<ul> <ul>
<li>The log suppressor work on almost everything that <code>WatchState</code> output. However, there are some <li>The log suppressor work on almost everything that <code>WatchState</code> output. However, there are
exceptions. For example <code>system:suppress</code>, <code>system:report</code> command output will not be some
filtered. If you find a a place where the rule supposed to work but it's not please report it on exceptions. For example <code>system:suppress</code>, <code>system:report</code> command output will not
<span class="icon-text is-underlined"> be
<span class="icon"><i class="fas fa-brands fa-discord"></i></span> filtered. If you find a a place where the rule supposed to work but it's not please report it on
<span> <span class="icon-text is-underlined">
<NuxtLink to="https://discord.gg/haUXHJyj6Y" target="_blank" v-text="'Discord server'"/> <span class="icon"><i class="fas fa-brands fa-discord"></i></span>
</span> <span>
</span>, <strong>NOT</strong> on GitHub issues tracker. <NuxtLink to="https://discord.gg/haUXHJyj6Y" target="_blank" v-text="'Discord server'"/>
</li> </span>
<li>The use case for this feature, is that sometimes it's out of your hands to fix a problem, and the constant </span>, <strong>NOT</strong> on GitHub issues tracker.
logging of the same error can be annoying. This feature allows you to suppress the error from being </li>
shown/recorded. <li>The use case for this feature, is that sometimes it's out of your hands to fix a problem, and the
</li> constant
<li> logging of the same error can be annoying. This feature allows you to suppress the error from being
Rule of thumb, it's less compute intensive to use <code>contains</code> type, than <code>regex</code> type. shown/recorded.
As each rule will be tested against every single outputted message. The less rules you have, the better. </li>
Having many rules will lead to performance degradation. <li>
</li> Rule of thumb, it's less compute intensive to use <code>contains</code> type, than <code>regex</code>
</ul> type.
</Message> As each rule will be tested against every single outputted message. The less rules you have, the better.
Having many rules will lead to performance degradation.
</li>
</ul>
</Message>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import 'assets/css/bulma-switch.css' import 'assets/css/bulma-switch.css'
import request from '~/utils/request.js' import request from '~/utils/request'
import {notification} from '~/utils/index.js' import {notification} from '~/utils/index'
import {useStorage} from '@vueuse/core' import {useStorage} from '@vueuse/core'
import Message from '~/components/Message.vue' import Message from '~/components/Message'
useHead({title: 'Log Suppressor'}) useHead({title: 'Log Suppressor'})

View File

@@ -1,177 +1,180 @@
<template> <template>
<div class="columns is-multiline"> <div>
<div class="column is-12 is-clearfix is-unselectable"> <div class="columns is-multiline">
<span class="title is-4"> <div class="column is-12 is-clearfix is-unselectable">
<span class="icon"><i class="fas fa-tasks"></i></span> <span class="title is-4">
Tasks <span class="icon"><i class="fas fa-tasks"></i></span>
</span> Tasks
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<button class="button has-text-dark" @click="show_worker_status=!show_worker_status" :disabled="isLoading"
:class="{'has-background-success-90':status?.status, 'has-background-warning-90': !status?.status}"
v-tooltip.bottom="'Toggle worker status'">
<span class="icon"><i :class="`fas fa-microchip`"></i></span>
</button>
</p>
<p class="control">
<button class="button is-info" @click="loadContent()" :disabled="isLoading"
:class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
This page contains all the tasks that are currently configured.
</span> </span>
</div> <div class="is-pulled-right">
</div> <div class="field is-grouped">
<p class="control">
<div id="queued_tasks" class="column is-12" v-if="queued.length > 0"> <button class="button has-text-dark" @click="show_worker_status=!show_worker_status" :disabled="isLoading"
<Message message_class="has-background-success-90 has-text-dark" title="Queued Tasks" :class="{'has-background-success-90':status?.status, 'has-background-warning-90': !status?.status}"
icon="fas fa-circle-notch fa-spin"> v-tooltip.bottom="'Toggle worker status'">
<p> <span class="icon"><i :class="`fas fa-microchip`"></i></span>
The following tasks </button>
<template v-for="(task, index) in queued" :key="`queued-${index}`"> </p>
<NuxtLink :to="`#${task}`"> <p class="control">
<span class="tag has-text-dark is-capitalized">{{ task }}</span> <button class="button is-info" @click="loadContent()" :disabled="isLoading"
</NuxtLink> :class="{'is-loading':isLoading}">
<template v-if="queued.length > index+1">,&nbsp;</template> <span class="icon"><i class="fas fa-sync"></i></span>
</template> </button>
are queued to be run in background soon. </p>
</p>
</Message>
</div>
<div class="column is-12" v-if="isLoading">
<Message message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
</div>
<div class="column is-12" v-if="status && (show_worker_status || !status.status)">
<Message
class="is-2"
:message_class="`has-text-dark ${status.status ? 'has-background-success-90' : 'has-background-warning-90'}`"
:title="`Task runner process is ${status.status ? 'active' : 'not active'}`"
:icon="`fas fa-${status.status ? 'pause' : 'exclamation-circle'}`">
{{ status.message }}
<p v-if="!status.status">
<span class="icon"><i class="fas fa-info-circle"></i></span>
To restart the task runner, you have to restart the container.
</p>
</Message>
</div>
<div v-for="task in tasks" :key="task.name" class="column is-6-tablet is-12-mobile">
<div class="card" :class="{ 'is-gray' : !task.enabled, 'is-success': task.enabled }">
<header class="card-header">
<div class="is-capitalized card-header-title">
{{ task.name }}
</div>
<span class="card-header-icon" v-tooltip="'Enable/Disable Task.'">
<input :id="task.name" type="checkbox" class="switch is-success" :checked="task.enabled"
@change="toggleTask(task)">
<label :for="task.name"></label>
</span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-12 has-text-left" v-if="task.description">
{{ task.description }}
</div>
<div class="column is-12 has-text-left">
<strong class="is-hidden-mobile">Runs: </strong>
<NuxtLink class="has-tooltip" target="_blank"
:to="`https://crontab.guru/#${task.timer.replace(/ /g, '_')}`">
{{ cronstrue.toString(task.timer) }}
</NuxtLink>
</div>
<div class="column is-6 has-text-left">
<strong class="is-hidden-mobile">Timer:&nbsp;</strong>
<NuxtLink class="has-tooltip" :to='makeEnvLink(`WS_CRON_${task.name.toUpperCase()}_AT`, task.timer)'>
{{ task.timer }}
</NuxtLink>
</div>
<div class="column is-6 has-text-right" v-if="task.args">
<strong class="is-hidden-mobile">Args:&nbsp;</strong>
<NuxtLink class="has-tooltip" :to='makeEnvLink(`WS_CRON_${task.name.toUpperCase()}_ARGS`, task.args)'>
{{ task.args }}
</NuxtLink>
</div>
<div class="column is-6 has-text-left">
<strong class="is-hidden-mobile">Prev Run:&nbsp;</strong>
<template v-if="task.enabled">
<span class="has-tooltip"
v-tooltip="`Last run was at: ${moment(task.prev_run).format(TOOLTIP_DATE_FORMAT)}`">
{{ task.prev_run ? moment(task.prev_run).fromNow() : '???' }}
</span>
</template>
<template v-else>
<span class="tag is-danger">Disabled</span>
</template>
</div>
<div class="column is-6 has-text-right">
<strong class="is-hidden-mobile">Next Run:&nbsp;</strong>
<template v-if="task.enabled">
<span class="has-tooltip"
v-tooltip="`Next run will be at: ${moment(task.next_run).format(TOOLTIP_DATE_FORMAT)}`">
{{ task.next_run ? moment(task.next_run).fromNow() : 'Never' }}
</span>
</template>
<template v-else>
<span class="tag is-danger">Disabled</span>
</template>
</div>
</div> </div>
</div> </div>
<footer class="card-footer"> <div class="is-hidden-mobile">
<div class="card-footer-item"> <span class="subtitle">
<button class="button is-info" @click="queueTask(task)" This page contains all the tasks that are currently configured.
:class="{'is-danger':task.queued,'is-info':!task.queued}"> </span>
<span class="icon-text"> </div>
<span class="icon"><i class="fas fa-clock" :class="{ 'fa-spin': task.queued }"></i></span>
<span>
<template v-if="!task.queued">Queue Task</template>
<template v-else>Remove from queue</template>
</span>
</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-warning" @click="confirmRun(task)">
<span class="icon-text">
<span class="icon"><i class="fas fa-terminal"></i></span>
<span class="is-hidden-mobile">Run via console</span>
<span class="is-hidden-tablet">Run now</span>
</span>
</button>
</div>
</footer>
</div> </div>
</div>
<div class="column is-12"> <div id="queued_tasks" class="column is-12" v-if="queued.length > 0">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-success-90 has-text-dark" title="Queued Tasks"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> icon="fas fa-circle-notch fa-spin">
<ul> <p>
<li>For long running tasks like <code>Import</code> and <code>Export</code>, you should queue the task to run The following tasks
in background. As running them via web console will take longer if you have many backends and/or has large <template v-for="(task, index) in queued" :key="`queued-${index}`">
libraries. <NuxtLink :to="`#${task}`">
</li> <span class="tag has-text-dark is-capitalized">{{ task }}</span>
<li>Use the switch next to the task to enable or disable the task from being run automatically.</li> </NuxtLink>
<li>To change when task is scheduled to run, please visit <template v-if="queued.length > index+1">,&nbsp;</template>
<span class="icon"><i class="fas fa-cogs"></i>&nbsp;</span> </template>
<NuxtLink to="/env" v-text="'Environment variables'"/> are queued to be run in background soon.
page. The <code>WS_CRON_(TASK)_*</code> variables are used to control scheduled tasks. </p>
</li> </Message>
<li>Clicking on the <code>Runs</code> link will take you to external page that will show for you more </div>
information about the cron timer syntax. While clicking on <code>Timer</code> or <code>Args</code>
link will take you to edit the related environment variable. <div class="column is-12" v-if="isLoading">
</li> <Message message_class="has-background-info-90 has-text-dark" title="Loading"
</ul> icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
</Message> </div>
<div class="column is-12" v-if="status && (show_worker_status || !status.status)">
<Message
class="is-2"
:message_class="`has-text-dark ${status.status ? 'has-background-success-90' : 'has-background-warning-90'}`"
:title="`Task runner process is ${status.status ? 'active' : 'not active'}`"
:icon="`fas fa-${status.status ? 'pause' : 'exclamation-circle'}`">
{{ status.message }}
<p v-if="!status.status">
<span class="icon"><i class="fas fa-info-circle"></i></span>
To restart the task runner, you have to restart the container.
</p>
</Message>
</div>
<div v-for="task in tasks" :key="task.name" class="column is-6-tablet is-12-mobile">
<div class="card" :class="{ 'is-gray' : !task.enabled, 'is-success': task.enabled }">
<header class="card-header">
<div class="is-capitalized card-header-title">
{{ task.name }}
</div>
<span class="card-header-icon" v-tooltip="'Enable/Disable Task.'">
<input :id="task.name" type="checkbox" class="switch is-success" :checked="task.enabled"
@change="toggleTask(task)">
<label :for="task.name"></label>
</span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-12 has-text-left" v-if="task.description">
{{ task.description }}
</div>
<div class="column is-12 has-text-left">
<strong class="is-hidden-mobile">Runs: </strong>
<NuxtLink class="has-tooltip" target="_blank"
:to="`https://crontab.guru/#${task.timer.replace(/ /g, '_')}`">
{{ cronstrue.toString(task.timer) }}
</NuxtLink>
</div>
<div class="column is-6 has-text-left">
<strong class="is-hidden-mobile">Timer:&nbsp;</strong>
<NuxtLink class="has-tooltip" :to='makeEnvLink(`WS_CRON_${task.name.toUpperCase()}_AT`, task.timer)'>
{{ task.timer }}
</NuxtLink>
</div>
<div class="column is-6 has-text-right" v-if="task.args">
<strong class="is-hidden-mobile">Args:&nbsp;</strong>
<NuxtLink class="has-tooltip" :to='makeEnvLink(`WS_CRON_${task.name.toUpperCase()}_ARGS`, task.args)'>
{{ task.args }}
</NuxtLink>
</div>
<div class="column is-6 has-text-left">
<strong class="is-hidden-mobile">Prev Run:&nbsp;</strong>
<template v-if="task.enabled">
<span class="has-tooltip"
v-tooltip="`Last run was at: ${moment(task.prev_run).format(TOOLTIP_DATE_FORMAT)}`">
{{ task.prev_run ? moment(task.prev_run).fromNow() : '???' }}
</span>
</template>
<template v-else>
<span class="tag is-danger">Disabled</span>
</template>
</div>
<div class="column is-6 has-text-right">
<strong class="is-hidden-mobile">Next Run:&nbsp;</strong>
<template v-if="task.enabled">
<span class="has-tooltip"
v-tooltip="`Next run will be at: ${moment(task.next_run).format(TOOLTIP_DATE_FORMAT)}`">
{{ task.next_run ? moment(task.next_run).fromNow() : 'Never' }}
</span>
</template>
<template v-else>
<span class="tag is-danger">Disabled</span>
</template>
</div>
</div>
</div>
<footer class="card-footer">
<div class="card-footer-item">
<button class="button is-info" @click="queueTask(task)"
:class="{'is-danger':task.queued,'is-info':!task.queued}">
<span class="icon-text">
<span class="icon"><i class="fas fa-clock" :class="{ 'fa-spin': task.queued }"></i></span>
<span>
<template v-if="!task.queued">Queue Task</template>
<template v-else>Remove from queue</template>
</span>
</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-warning" @click="confirmRun(task)">
<span class="icon-text">
<span class="icon"><i class="fas fa-terminal"></i></span>
<span class="is-hidden-mobile">Run via console</span>
<span class="is-hidden-tablet">Run now</span>
</span>
</button>
</div>
</footer>
</div>
</div>
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
<li>For long running tasks like <code>Import</code> and <code>Export</code>, you should queue the task to
run
in background. As running them via web console will take longer if you have many backends and/or has large
libraries.
</li>
<li>Use the switch next to the task to enable or disable the task from being run automatically.</li>
<li>To change when task is scheduled to run, please visit
<span class="icon"><i class="fas fa-cogs"></i>&nbsp;</span>
<NuxtLink to="/env" v-text="'Environment variables'"/>
page. The <code>WS_CRON_(TASK)_*</code> variables are used to control scheduled tasks.
</li>
<li>Clicking on the <code>Runs</code> link will take you to external page that will show for you more
information about the cron timer syntax. While clicking on <code>Timer</code> or <code>Args</code>
link will take you to edit the related environment variable.
</li>
</ul>
</Message>
</div>
</div> </div>
</div> </div>
</template> </template>