admin: add settings section

Signed-off-by: Varun Patil <varunpatil@ucla.edu>
pull/563/head
Varun Patil 2023-04-09 23:24:59 -07:00
parent 5e32987932
commit f73b82bdd5
12 changed files with 731 additions and 12 deletions

View File

@ -47,6 +47,10 @@ Memories is a *batteries-included* photo management solution for Nextcloud with
<command>OCA\Memories\Command\PlacesSetup</command> <command>OCA\Memories\Command\PlacesSetup</command>
<command>OCA\Memories\Command\MigrateGoogleTakeout</command> <command>OCA\Memories\Command\MigrateGoogleTakeout</command>
</commands> </commands>
<settings>
<admin>OCA\Memories\Settings\Admin</admin>
<admin-section>OCA\Memories\Settings\AdminSection</admin-section>
</settings>
<navigations> <navigations>
<navigation> <navigation>
<name>Memories</name> <name>Memories</name>

View File

@ -83,6 +83,8 @@ return [
// Config API // Config API
['name' => 'Other#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'], ['name' => 'Other#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'],
['name' => 'Other#getSystemConfig', 'url' => '/api/system-config', 'verb' => 'GET'],
['name' => 'Other#setSystemConfig', 'url' => '/api/system-config/{key}', 'verb' => 'PUT'],
// Service worker // Service worker
['name' => 'Other#serviceWorker', 'url' => '/service-worker.js', 'verb' => 'GET'], ['name' => 'Other#serviceWorker', 'url' => '/service-worker.js', 'verb' => 'GET'],

85
img/app-dark.svg 100644
View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
id="svg2"
sodipodi:docname="app.svg"
viewBox="0 0 650 650"
version="1.1"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
inkscape:export-filename="C:\Users\varun\Downloads\app.png"
inkscape:export-xdpi="44.700001"
inkscape:export-ydpi="44.700001"
width="650"
height="650"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs16" />
<title
id="title3629">Camera</title>
<sodipodi:namedview
id="base"
bordercolor="#666666"
inkscape:pageshadow="2"
inkscape:window-y="-9"
pagecolor="#ffffff"
inkscape:window-height="991"
inkscape:window-maximized="1"
inkscape:zoom="0.35355339"
inkscape:window-x="-9"
showgrid="false"
borderopacity="1.0"
inkscape:current-layer="layer1"
inkscape:cx="89.095455"
inkscape:cy="148.49242"
inkscape:window-width="1920"
inkscape:pageopacity="0.0"
inkscape:document-units="px"
inkscape:pagecheckerboard="0"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<g
id="layer1"
inkscape:label="bg"
inkscape:groupmode="layer"
transform="translate(-151.94602,-318.77312)">
<path
style="fill:#000000;fill-opacity:1;stroke-width:2.13333"
d="m 197.73165,860.67627 c -18.61338,-6.82684 -36.75782,-26.01129 -41.74653,-44.13937 -1.829,-6.64627 -2.33554,-48.09549 -1.88392,-154.15716 l 0.6177,-145.06666 4.93339,-10.01572 c 16.12451,-32.73596 27.47173,-36.77498 105.46662,-37.5407 l 63.46666,-0.62308 0.0128,-8.97692 c 0.0286,-21.53158 14.50791,-42.38798 33.30228,-47.9693 14.35532,-4.26306 208.49298,-3.66246 220.80965,0.68312 18.77632,6.62468 31.61107,24.94489 31.69598,45.2426 l 0.0459,11.2 h 65.04378 c 84.02378,0 95.74352,3.94726 112.45,37.8737 l 7.03955,14.29547 0.63814,134.29886 c 0.8464,178.12549 -1.26265,188.01876 -43.44674,203.80184 -15.31511,5.73014 -542.98637,6.76314 -558.44531,1.09332 z M 510.98557,774.6975 c 88.64983,-31.88148 102.45316,-145.666 23.87294,-196.79099 -102.54562,-66.7171 -220.77781,66.53341 -142.78216,160.91892 29.81079,36.0752 77.95716,50.59979 118.90922,35.87207 z m -60.78933,-42.43819 c -25.88523,-9.13355 -42.15328,-37.1623 -39.9792,-68.88145 5.67091,-82.737 128.4049,-82.50709 133.37154,0.24984 3.13721,52.27458 -43.21406,86.33693 -93.39234,68.63161 z"
id="path3940" />
<path
style="fill:#000000;stroke-width:2.13333"
id="path3510"
d="" />
<path
style="fill:#000000;stroke-width:2.13333"
id="path3490"
d="" />
<path
style="fill:#000000;stroke-width:2.13333"
id="path3452"
d="" />
</g>
<g
id="layer2"
inkscape:label="camera"
inkscape:groupmode="layer"
transform="translate(-151.94602,-318.77312)" />
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title>Camera</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -56,6 +56,59 @@ class OtherController extends GenericApiController
}); });
} }
/**
* @AdminRequired
*
* @NoCSRFRequired
*/
public function getSystemConfig(): Http\Response
{
return Util::guardEx(function () {
$config = [];
foreach (Util::systemConfigDefaults() as $key => $default) {
$config[$key] = $this->config->getSystemValue($key, $default);
}
return new JSONResponse($config, Http::STATUS_OK);
});
}
/**
* @AdminRequired
*
* @NoCSRFRequired
*
* @param mixed $value
*/
public function setSystemConfig(string $key, $value): Http\Response
{
return Util::guardEx(function () use ($key, $value) {
// Make sure not running in read-only mode
if ($this->config->getSystemValue('memories.readonly', false)) {
throw Exceptions::Forbidden('Cannot change settings in readonly mode');
}
// Make sure the key is valid
$defaults = Util::systemConfigDefaults();
if (!\array_key_exists($key, $defaults)) {
throw Exceptions::BadRequest('Invalid key');
}
// Make sure the value has the same type as the default value
if (\gettype($value) !== \gettype($defaults[$key])) {
throw Exceptions::BadRequest('Invalid value type');
}
if ($value === $defaults[$key]) {
$this->config->deleteSystemValue($key);
} else {
$this->config->setSystemValue($key, $value);
}
return new JSONResponse([], Http::STATUS_OK);
});
}
/** /**
* @NoAdminRequired * @NoAdminRequired
* *

View File

@ -0,0 +1,48 @@
<?php
namespace OCA\Memories\Settings;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\Settings\ISettings;
class Admin implements ISettings
{
/** @var IConfig */
private $config;
/** @var IL10N */
private $l;
public function __construct(
IConfig $config,
IL10N $l
) {
$this->config = $config;
$this->l = $l;
}
/**
* @return TemplateResponse
*/
public function getForm()
{
$parameters = [
];
\OCP\Util::addScript('memories', 'memories-main');
return new TemplateResponse('memories', 'admin', $parameters);
}
public function getSection()
{
return 'memories';
}
public function getPriority()
{
return 50;
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace OCA\Memories\Settings;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class AdminSection implements IIconSection
{
/** @var IL10N */
private $l;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct(
IL10N $l,
IURLGenerator $urlGenerator
) {
$this->l = $l;
$this->urlGenerator = $urlGenerator;
}
/**
* @return TemplateResponse
*/
public function getForm()
{
$parameters = [
];
return new TemplateResponse('memories', 'admin', $parameters);
}
public function getID()
{
return 'memories';
}
public function getName()
{
return $this->l->t('Memories');
}
public function getPriority()
{
return 75;
}
public function getIcon()
{
return $this->urlGenerator->imagePath('memories', 'app-dark.svg');
}
}

View File

@ -279,7 +279,69 @@ class Util
*/ */
public static function placesGISType(): int public static function placesGISType(): int
{ {
return (int) \OC::$server->get(\OCP\IConfig::class)->getSystemValue('memories.gis_type', -1); return self::getSystemConfig('memories.gis_type');
}
/**
* Get a system config key with the correct default.
*
* @param null|mixed $default
*/
public static function getSystemConfig(string $key, $default = null)
{
$config = \OC::$server->get(\OCP\IConfig::class);
return $config->getSystemValue($key, $default ?? self::systemConfigDefaults()[$key]);
}
/** Get list of defaults for all system config keys. */
public static function systemConfigDefaults(): array
{
return [
// Places database type identifier
'memories.gis_type' => -1,
// Disable transcoding
'memories.vod.disable' => true,
// VA-API configuration options
'memories.vod.vaapi' => false, // Transcode with VA-API
'memories.vod.vaapi.low_power' => false, // Use low_power mode for VA-API
// NVENC configuration options
'memories.vod.nvenc' => false, // Transcode with NVIDIA NVENC
'memories.vod.nvenc.temporal_aq' => false,
'memories.vod.nvenc.scale' => 'npp', // npp or cuda
// Paths to ffmpeg and ffprobe binaries
'memories.vod.ffmpeg' => '',
'memories.vod.ffprobe' => '',
// Path to go-vod binary
'memories.vod.path' => '',
// Path to use for transcoded files (/tmp/go-vod/instanceid)
// Make sure this has plenty of space
'memories.vod.tempdir' => '',
// Bind address to use when starting the transcoding server
'memories.vod.bind' => '127.0.0.1:47788',
// Address used to connect to the transcoding server
// If not specified, the bind address above will be used
'memories.vod.connect' => '127.0.0.1:47788',
// Mark go-vod as external. If true, Memories will not attempt to
// start go-vod if it is not running already.
'memories.vod.external' => false,
// Set the default video quality for a first time user
// 0 => Auto (default)
// -1 => Original (max quality with transcoding)
// -2 => Direct (disable transcoding)
// 1080 => 1080p (and so on)
'memories.video_default_quality' => '0',
];
} }
/** /**

384
src/Admin.vue 100644
View File

@ -0,0 +1,384 @@
<template>
<div class="outer" v-if="loaded">
<div>
<h2>{{ t("memories", "Video Streaming") }}</h2>
<p>
{{
t(
"memories",
"Live transcoding provides for adaptive streaming of videos using HLS."
)
}}
<br />
{{
t(
"memories",
"Note that this may be very CPU intensive without hardware acceleration."
)
}}
<NcCheckboxRadioSwitch
:checked.sync="enableTranscoding"
@update:checked="update('enableTranscoding')"
type="switch"
>
{{ t("memories", "Enable Transcoding") }}
</NcCheckboxRadioSwitch>
<NcTextField
:label="t('memories', 'ffmpeg path')"
:label-visible="true"
:value="ffmpegPath"
@change="update('ffmpegPath', $event.target.value)"
/>
<NcTextField
:label="t('memories', 'ffprobe path')"
:label-visible="true"
:value="ffprobePath"
@change="update('ffprobePath', $event.target.value)"
/>
<br />
{{ t("memories", "Global default video quality (user may override)") }}
<NcCheckboxRadioSwitch
:checked.sync="videoDefaultQuality"
value="0"
name="vdq_radio"
type="radio"
@update:checked="update('videoDefaultQuality')"
>{{ t("memories", "Auto (adaptive transcode)") }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
:checked.sync="videoDefaultQuality"
value="-1"
name="vdq_radio"
type="radio"
@update:checked="update('videoDefaultQuality')"
>{{ t("memories", "Original (transcode with max quality)") }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
:checked.sync="videoDefaultQuality"
value="-2"
name="vdq_radio"
type="radio"
@update:checked="update('videoDefaultQuality')"
>{{ t("memories", "Direct (original video file without transcode)") }}
</NcCheckboxRadioSwitch>
</p>
<h3>{{ t("memories", "Transcoder configuration") }}</h3>
<p>
{{
t(
"memories",
"Memories uses the go-vod transcoder. You can run go-vod exernally (e.g. in a separate Docker container for hardware acceleration) or use the built-in transcoder. To use an external transcoder, enable the following option and follow the instructions at this link:"
)
}}
<a
target="_blank"
href="https://github.com/pulsejet/memories/wiki/HW-Transcoding"
>
{{ t("memories", "external transcoder configuration") }}
</a>
<NcCheckboxRadioSwitch
:checked.sync="enableExternalTranscoder"
@update:checked="update('enableExternalTranscoder')"
type="switch"
>
{{ t("memories", "Enable external transcoder (go-vod)") }}
</NcCheckboxRadioSwitch>
<NcTextField
:label="t('memories', 'Binary path (local only)')"
:label-visible="true"
:value="goVodPath"
@change="update('goVodPath', $event.target.value)"
/>
<NcTextField
:label="t('memories', 'Bind address (local only)')"
:label-visible="true"
:value="goVodBind"
@change="update('goVodBind', $event.target.value)"
/>
<NcTextField
:label="t('memories', 'Connection address (same as bind if local)')"
:label-visible="true"
:value="goVodConnect"
@change="update('goVodConnect', $event.target.value)"
/>
</p>
<h3>{{ t("memories", "Hardware Acceleration") }}</h3>
<p>
{{
t(
"memories",
"You must first make sure the correct drivers are installed before configuring acceleration."
)
}}
<br />
{{
t(
"memories",
"Make sure you test hardware acceleration with various options after enabling."
)
}}
<br />
{{
t(
"memories",
"Do not enable multiple types of hardware acceleration simultaneously."
)
}}
<br />
<br />
{{
t(
"memories",
"Intel processors supporting QuickSync Video (QSV) as well as some AMD GPUs can be used for transcoding using VA-API acceleration."
)
}}
{{
t(
"memories",
"For more details on driver installation, check the following link:"
)
}}
<a
target="_blank"
href="https://github.com/pulsejet/memories/wiki/HW-Transcoding#va-api"
>
VA-API configuration
</a>
<NcNoteCard type="warning">
{{
t(
"memories",
"/dev/dri/renderD128 is required for VA-API acceleration."
)
}}
</NcNoteCard>
<NcCheckboxRadioSwitch
:checked.sync="enableVaapi"
@update:checked="update('enableVaapi')"
type="switch"
>
{{ t("memories", "Enable acceleration with VA-API") }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
:checked.sync="enableVaapiLowPower"
@update:checked="update('enableVaapiLowPower')"
type="switch"
>
{{ t("memories", "Enable low-power mode (QSV)") }}
</NcCheckboxRadioSwitch>
{{
t(
"memories",
"NVIDIA GPUs can be used for transcoding using the NVENC encoder with the proper drivers."
)
}}
<br />
{{
t(
"memories",
"Depending on the versions of the installed SDK and ffmpeg, you need to specify the scaler to use"
)
}}
<NcNoteCard type="warning">
{{
t(
"memories",
"No automated tests are available for NVIDIA acceleration."
)
}}
</NcNoteCard>
<NcCheckboxRadioSwitch
:checked.sync="enableNvenc"
@update:checked="update('enableNvenc')"
type="switch"
>
{{ t("memories", "Enable acceleration with NVENC") }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
:checked.sync="enableNvencTemporalAQ"
@update:checked="update('enableNvencTemporalAQ')"
type="switch"
>
{{ t("memories", "Enable NVENC Temporal AQ") }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
:checked.sync="nvencScaler"
value="npp"
name="nvence_scaler_radio"
type="radio"
@update:checked="update('nvencScaler')"
class="m-radio"
>{{ t("memories", "NPP scaler") }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
:checked.sync="nvencScaler"
value="cuda"
name="nvence_scaler_radio"
type="radio"
class="m-radio"
@update:checked="update('nvencScaler')"
>{{ t("memories", "CUDA scaler") }}</NcCheckboxRadioSwitch
>
</p>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import axios from "@nextcloud/axios";
import { API } from "./services/API";
import { showError } from "@nextcloud/dialogs";
const NcCheckboxRadioSwitch = () =>
import("@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch");
const NcNoteCard = () => import("@nextcloud/vue/dist/Components/NcNoteCard");
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField");
/** Map from UI to backend settings */
const settings = {
enableTranscoding: "memories.vod.disable",
ffmpegPath: "memories.vod.ffmpeg",
ffprobePath: "memories.vod.ffprobe",
goVodPath: "memories.vod.path",
goVodBind: "memories.vod.bind",
goVodConnect: "memories.vod.connect",
enableExternalTranscoder: "memories.vod.external",
videoDefaultQuality: "memories.video_default_quality",
enableVaapi: "memories.vod.vaapi",
enableVaapiLowPower: "memories.vod.vaapi.low_power",
enableNvenc: "memories.vod.nvenc",
enableNvencTemporalAQ: "memories.vod.nvenc.temporal_aq",
nvencScaler: "memories.vod.nvenc.scale",
};
/** Invert setting before saving */
const invertedBooleans = ["enableTranscoding"];
export default defineComponent({
name: "Admin",
components: {
NcCheckboxRadioSwitch,
NcNoteCard,
NcTextField,
},
data: () => ({
loaded: false,
enableTranscoding: false,
ffmpegPath: "",
ffprobePath: "",
goVodPath: "",
goVodBind: "",
goVodConnect: "",
enableExternalTranscoder: false,
videoDefaultQuality: "",
enableVaapi: false,
enableVaapiLowPower: false,
enableNvenc: false,
enableNvencTemporalAQ: false,
nvencScaler: "",
}),
mounted() {
this.reload();
},
methods: {
async reload() {
const res = await axios.get(API.SYSTEM_CONFIG(null));
for (const key in settings) {
if (!res.data.hasOwnProperty(settings[key])) {
console.error(
`Setting ${settings[key]} not found in backend response`
);
continue;
}
this[key] = res.data[settings[key]];
if (invertedBooleans.includes(key)) {
this[key] = !this[key];
}
}
this.loaded = true;
},
async update(key: string, value = null) {
value ||= this[key];
const setting = settings[key];
// Inversion
if (invertedBooleans.includes(key)) {
value = !value;
}
axios
.put(API.SYSTEM_CONFIG(setting), {
value: value,
})
.catch((err) => {
console.error(err);
showError(this.t("memories", "Failed to update setting"));
});
},
},
});
</script>
<style lang="scss" scoped>
.outer {
padding: 20px;
.checkbox-radio-switch {
margin: 2px 8px;
}
.m-radio {
display: inline-block;
}
h2 {
font-size: 1.5em;
font-weight: bold;
}
h3 {
font-size: 1.2em;
font-weight: 500;
margin-top: 20px;
}
a {
color: var(--color-primary-element);
}
}
</style>

View File

@ -8,6 +8,7 @@ import XImg from "./components/frame/XImg.vue";
import GlobalMixin from "./mixins/GlobalMixin"; import GlobalMixin from "./mixins/GlobalMixin";
import App from "./App.vue"; import App from "./App.vue";
import Admin from "./Admin.vue";
import router from "./router"; import router from "./router";
import { Route } from "vue-router"; import { Route } from "vue-router";
import { generateFilePath } from "@nextcloud/router"; import { generateFilePath } from "@nextcloud/router";
@ -103,8 +104,20 @@ window.addEventListener("DOMContentLoaded", () => {
); );
}); });
export default new Vue({ let app = null;
el: "#content",
router, const adminSection = document.getElementById("memories-admin-content");
render: (h) => h(App), if (adminSection) {
}); app = new Vue({
el: "#memories-admin-content",
render: (h) => h(Admin),
});
} else {
app = new Vue({
el: "#content",
router,
render: (h) => h(App),
});
}
export default app;

View File

@ -184,6 +184,12 @@ export class API {
return gen(`${BASE}/config/{setting}`, { setting }); return gen(`${BASE}/config/{setting}`, { setting });
} }
static SYSTEM_CONFIG(setting: string | null) {
return setting
? gen(`${BASE}/system-config/{setting}`, { setting })
: gen(`${BASE}/system-config`);
}
static MAP_CLUSTERS() { static MAP_CLUSTERS() {
return tok(gen(`${BASE}/map/clusters`)); return tok(gen(`${BASE}/map/clusters`));
} }

View File

@ -297,12 +297,17 @@ export const constants = {
/** Cache store */ /** Cache store */
let staticCache: Cache | null = null; let staticCache: Cache | null = null;
const cacheName = `memories-${loadState("memories", "version")}-${ let cacheName: string;
getCurrentUser()?.uid let memoriesVersion: string;
}`;
openCache().then((cache) => { try {
staticCache = cache; memoriesVersion = loadState("memories", "version");
}); const uid = getCurrentUser()?.uid;
cacheName = `memories-${memoriesVersion}-${uid}`;
openCache().then((cache) => (staticCache = cache));
} catch (e) {
console.warn("Failed to open cache");
}
// Clear all caches except the current one // Clear all caches except the current one
window.caches?.keys().then((keys) => { window.caches?.keys().then((keys) => {

View File

@ -0,0 +1 @@
<div id="memories-admin-content"></div>