set custom icon

- split settings collection into settings_private and settings_public
- add icon input to frontend
This commit is contained in:
Maxi Quoß
2023-03-10 02:11:56 +01:00
parent f061c7d00a
commit 79a70b6053
13 changed files with 314 additions and 68 deletions

View File

@@ -13,14 +13,14 @@ var CronPing *cron.Cron
var CronWakeShutdown *cron.Cron
func RunPing(app *pocketbase.PocketBase) {
settingsRecords, err := app.Dao().FindRecordsByExpr("settings")
settingsPrivateRecords, err := app.Dao().FindRecordsByExpr("settings_private")
if err != nil {
logger.Error.Println(err)
}
// init cronjob
CronPing = cron.New()
CronPing.AddFunc(settingsRecords[0].GetString("interval"), func() {
CronPing.AddFunc(settingsPrivateRecords[0].GetString("interval"), func() {
// skip cron if no realtime clients connected
realtimeClients := len(app.SubscriptionsBroker().Clients())
if realtimeClients == 0 {

View File

@@ -0,0 +1,80 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `{
"id": "t1rajoyzl6691g3",
"created": "2023-03-09 22:17:29.463Z",
"updated": "2023-03-09 22:17:29.463Z",
"name": "settings_public",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "s1grje98",
"name": "website_title",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "tlzz87pj",
"name": "favicon",
"type": "file",
"required": false,
"unique": false,
"options": {
"maxSelect": 1,
"maxSize": 5242880,
"mimeTypes": [
"image/x-icon",
"image/png",
"image/svg+xml",
"image/gif",
"image/jpeg"
],
"thumbs": []
}
}
],
"listRule": "",
"viewRule": "",
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}`
collection := &models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
return err
}
return daos.New(db).SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("t1rajoyzl6691g3")
if err != nil {
return err
}
return dao.DeleteCollection(collection)
})
}

View File

@@ -0,0 +1,65 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/types"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("nmj3ko20gzkg8n3")
if err != nil {
return err
}
collection.Name = "settings_private"
collection.ListRule = nil
collection.ViewRule = nil
// remove
collection.Schema.RemoveField("slrjkipw")
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("nmj3ko20gzkg8n3")
if err != nil {
return err
}
collection.Name = "settings"
collection.ListRule = types.Pointer("@request.auth.id != \"\"")
collection.ViewRule = types.Pointer("@request.auth.id != \"\"")
// add
del_website_title := &schema.SchemaField{}
json.Unmarshal([]byte(`{
"system": false,
"id": "slrjkipw",
"name": "website_title",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}`), del_website_title)
collection.Schema.AddField(del_website_title)
return dao.SaveCollection(collection)
})
}

View File

@@ -70,12 +70,12 @@ func HandlerScan(c echo.Context) error {
}
// check if scan range is valid
allSettings, err := App.Dao().FindRecordsByExpr("settings")
allPrivateSettings, err := App.Dao().FindRecordsByExpr("settings_private")
if err != nil {
return err
}
settings := allSettings[0]
scanRange := settings.GetString("scan_range")
settingsPrivate := allPrivateSettings[0]
scanRange := settingsPrivate.GetString("scan_range")
_, ipNet, err := net.ParseCIDR(scanRange)
if err != nil {
return apis.NewBadRequestError(err.Error(), nil)

View File

@@ -85,7 +85,7 @@ func StartPocketBase(distDirFS fs.FS) {
// add event hook before starting server.
// using this outside App.OnBeforeServe() would not work
App.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error {
if e.Model.TableName() == "settings" {
if e.Model.TableName() == "settings_private" {
for _, job := range cronjobs.CronPing.Entries() {
cronjobs.CronPing.Remove(job.ID)
}
@@ -119,18 +119,32 @@ func StartPocketBase(distDirFS fs.FS) {
}
func importSettings() error {
// get first settings record
settingsRecords, err := App.Dao().FindRecordsByExpr("settings")
// get first settingsPrivate record
settingsPrivateRecords, err := App.Dao().FindRecordsByExpr("settings_private")
if err != nil {
return err
}
settingsCollection, err := App.Dao().FindCollectionByNameOrId("settings")
settingsPrivateCollection, err := App.Dao().FindCollectionByNameOrId("settings_private")
if err != nil {
return err
}
settings := models.NewRecord(settingsCollection)
if len(settingsRecords) > 0 {
settings = settingsRecords[0]
settingsPrivate := models.NewRecord(settingsPrivateCollection)
if len(settingsPrivateRecords) > 0 {
settingsPrivate = settingsPrivateRecords[0]
}
// get first settingsPublic record
settingsPublicRecords, err := App.Dao().FindRecordsByExpr("settings_public")
if err != nil {
return err
}
settingsPublicCollection, err := App.Dao().FindCollectionByNameOrId("settings_public")
if err != nil {
return err
}
settingsPublic := models.NewRecord(settingsPublicCollection)
if len(settingsPublicRecords) > 0 {
settingsPublic = settingsPublicRecords[0]
}
// set ping interval settings. priority:
@@ -138,22 +152,22 @@ func importSettings() error {
// 2nd: database entry
// 3rd: default values
interval := "@every 3s"
if settings.GetString("interval") != "" {
interval = settings.GetString("interval")
if settingsPrivate.GetString("interval") != "" {
interval = settingsPrivate.GetString("interval")
}
if os.Getenv("UPSNAP_INTERVAL") != "" {
interval = os.Getenv("UPSNAP_INTERVAL")
}
// save settings to db
settings.Set("interval", interval)
settingsPrivate.Set("interval", interval)
if scanRange := os.Getenv("UPSNAP_SCAN_RANGE"); scanRange != "" {
settings.Set("scan_range", scanRange)
settingsPrivate.Set("scan_range", scanRange)
}
if scanRange := os.Getenv("UPSNAP_WEBSITE_TITLE"); scanRange != "" {
settings.Set("website_title", scanRange)
if websiteTitle := os.Getenv("UPSNAP_WEBSITE_TITLE"); websiteTitle != "" {
settingsPublic.Set("website_title", websiteTitle)
}
if err := App.Dao().SaveRecord(settings); err != nil {
if err := App.Dao().SaveRecord(settingsPublic); err != nil {
return err
}

View File

@@ -2,7 +2,6 @@
<html lang="en" class="h-100">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/gopher.svg" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>

View File

@@ -2,6 +2,7 @@
import { dev } from '$app/environment';
import { parseISO, formatDistance } from 'date-fns';
import { pocketbase } from '@stores/pocketbase';
import Fa from 'svelte-fa/src/fa.svelte';
import {
faPowerOff,
faEllipsisVertical,
@@ -10,7 +11,6 @@
faCircleDown,
faLock
} from '@fortawesome/free-solid-svg-icons';
import Fa from 'svelte-fa/src/fa.svelte';
import { sortPorts } from '../sorts';
export let device;

View File

@@ -1,8 +1,8 @@
<script>
import { goto } from '$app/navigation';
import { pocketbase, devices } from '@stores/pocketbase';
import Fa from 'svelte-fa/src/fa.svelte';
import { faEye, faEyeSlash, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import Fa from 'svelte-fa';
export let device;
export let mode;

View File

@@ -1,5 +1,5 @@
<script>
import { pocketbase, authorizedStore } from '@stores/pocketbase';
import { pocketbase, authorizedStore, settings_public } from '@stores/pocketbase';
let username;
let password;
@@ -43,7 +43,13 @@
<div class="container text-dark-emphasis">
<div class="row justify-content-center align-items-center">
<div class="col text-center p-3 p-md-5">
<img src="/gopher.svg" alt="Logo" class="logo pe-none user-select-none" />
<img
src={$settings_public.favicon !== ''
? `${$pocketbase.baseUrl}/api/files/settings_public/${$settings_public.id}/${$settings_public.favicon}`
: '/gopher.svg'}
alt={$settings_public.website_title ? $settings_public.website_title : 'UpSnap'}
class="logo pe-none user-select-none"
/>
<h2 class="text-dark-emphasis fw-bold text-center mt-3">Login</h2>
</div>
</div>

View File

@@ -2,7 +2,8 @@
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { theme } from '@stores/theme';
import { pocketbase, authorizedStore, settings } from '@stores/pocketbase';
import { pocketbase, authorizedStore, settings_public } from '@stores/pocketbase';
import Fa from 'svelte-fa/src/fa.svelte';
import {
faSun,
faMoon,
@@ -10,7 +11,6 @@
faBrush,
faRightFromBracket
} from '@fortawesome/free-solid-svg-icons';
import Fa from 'svelte-fa';
let userInfo = {
usernameOrEmail: '',
@@ -18,11 +18,6 @@
};
onMount(async () => {
$pocketbase.collection('settings').subscribe('*', function (e) {
settings.set(e.record);
document.title = $settings.website_title;
});
if ($pocketbase.authStore.baseModel?.collectionName === 'users') {
userInfo.role = 'user';
} else {
@@ -41,24 +36,18 @@
}
</script>
<svelte:head>
{#if $settings.website_title !== ''}
<title>{$settings.website_title}</title>
{:else}
<title>UpSnap</title>
{/if}
</svelte:head>
<nav class="navbar navbar-expand-sm pt-0">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<img
src="/gopher.svg"
alt="UpSnap"
src={$settings_public.favicon !== ''
? `${$pocketbase.baseUrl}/api/files/settings_public/${$settings_public.id}/${$settings_public.favicon}`
: '/gopher.svg'}
alt={$settings_public.website_title ? $settings_public.website_title : 'UpSnap'}
width="45"
height="45"
class:me-2={$settings.website_title !== ''}
/>{$settings.website_title ? $settings.website_title : ''}
class:me-2={$settings_public.website_title !== ''}
/>{$settings_public.website_title ? $settings_public.website_title : ''}
</a>
<button
class="navbar-toggler border-0"

View File

@@ -5,8 +5,15 @@
import Login from '@components/Login.svelte';
import Transition from '@components/Transition.svelte';
import { theme } from '@stores/theme';
import { pocketbase, authorizedStore, devices, settings } from '@stores/pocketbase';
import {
pocketbase,
authorizedStore,
devices,
settings_private,
settings_public
} from '@stores/pocketbase';
let favicon;
let preferesDark;
let isAuth = false;
@@ -38,6 +45,15 @@
isAuth = state;
});
// load public settings and subscribe to changes and change favicon
const settingsPublicRes = await $pocketbase.collection('settings_public').getList(1, 1);
settings_public.set(settingsPublicRes.items[0]);
updateSettingsPublic();
$pocketbase.collection('settings_public').subscribe('*', function (e) {
settings_public.set(e.record);
updateSettingsPublic();
});
// load auth from localstorage
const pbCookie = localStorage.getItem('pocketbase_auth');
if (pbCookie) {
@@ -55,10 +71,18 @@
}
});
async function getSettingsAndDevices() {
let settingsRes = {};
settingsRes = await $pocketbase.collection('settings').getList(1, 1);
settings.set(settingsRes.items[0]);
function updateSettingsPublic() {
favicon.href =
$settings_public.favicon === ''
? '/gopher.svg'
: `${$pocketbase.baseUrl}/api/files/settings_public/${$settings_public.id}/${$settings_public.favicon}`;
document.title =
$settings_public.website_title === '' ? 'UpSnap' : $settings_public.website_title;
}
async function getSettingsPrivateAndDevices() {
const settingsPrivateRes = await $pocketbase.collection('settings_private').getList(1, 1);
settings_private.set(settingsPrivateRes.items[0]);
let tempDevices = {};
const devicesRes = await $pocketbase.collection('devices').getFullList(200, {
@@ -72,11 +96,12 @@
}
$: if (isAuth) {
getSettingsAndDevices();
getSettingsPrivateAndDevices();
}
</script>
<svelte:head>
<link rel="shortcut icon" href="/gopher.svg" bind:this={favicon} />
<script>
if (document) {
preferesDark = window.matchMedia('(prefers-color-scheme: dark)').matches

View File

@@ -1,14 +1,16 @@
<script>
import { dev } from '$app/environment';
import { pocketbase, settings, devices } from '@stores/pocketbase';
import { pocketbase, settings_private, settings_public, devices } from '@stores/pocketbase';
import UnauthorizedMsg from '@components/UnauthorizedMsg.svelte';
import DeviceForm from '@components/DeviceForm.svelte';
import Fa from 'svelte-fa/src/fa.svelte';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import Fa from 'svelte-fa';
let version = import.meta.env.UPSNAP_VERSION;
let files;
let iconFiles = [];
let iconPreview;
let restoreFiles = [];
let timeout;
let buttons = {
settings: {
@@ -43,13 +45,38 @@
};
let scannedDevices = {};
$: if (iconPreview && iconFiles.length > 0) {
iconPreview.src = URL.createObjectURL(iconFiles[0]);
} else if (iconPreview && $settings_public.favicon !== '') {
iconPreview.src = `${$pocketbase.baseUrl}/api/files/settings_public/${$settings_public.id}/${$settings_public.favicon}`;
} else if (iconPreview) {
iconPreview.src = '/gopher.svg';
}
async function saveSettings() {
buttons.settings.state = 'waiting';
try {
if ($settings.interval === '') {
$settings.interval = '@every 3s';
// update settings private
if ($settings_private.interval === '') {
$settings_private.interval = '@every 3s';
}
await $pocketbase.collection('settings').update($settings.id, $settings);
await $pocketbase
.collection('settings_private')
.update($settings_private.id, $settings_private);
// update settings public
if (iconFiles.length > 0) {
const formData = new FormData();
formData.append('favicon', iconFiles[0]);
await $pocketbase
.collection('settings_public')
.update($settings_public.id, formData);
}
await $pocketbase
.collection('settings_public')
.update($settings_public.id, $settings_public);
//set button
clearTimeout(timeout);
timeout = setTimeout(() => {
buttons.settings.state = 'none';
@@ -69,8 +96,8 @@
async function scanDevices() {
buttons.scan.state = 'waiting';
await $pocketbase
.collection('settings')
.update($settings.id, $settings)
.collection('settings_private')
.update($settings_private.id, $settings_private)
.catch((err) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
@@ -137,7 +164,7 @@
}
let reader = new FileReader();
reader.readAsText(files[0]);
reader.readAsText(restoreFiles[0]);
reader.onload = async (e) => {
// parse uploaded file
let data = JSON.parse(e.target.result);
@@ -195,9 +222,9 @@
<UnauthorizedMsg />
<section class="m-0 mt-4 p-4 shadow-sm">
<form on:submit|preventDefault={saveSettings}>
<h3 class="mb-3">Ping interval</h3>
<div class="row">
<div class="col-md-6">
<h3 class="mb-3">Ping interval</h3>
<p>Sets the interval in which the devices are pinged.</p>
<div class="input-group mb-3">
<span class="input-group-text">Cron</span>
@@ -207,10 +234,10 @@
aria-label="Interval"
aria-describedby="addon-wrapping"
type="text"
bind:value={$settings.interval}
bind:value={$settings_private.interval}
/>
</div>
<h3 class="my-3">Website title</h3>
<h3 class="my-3">Title</h3>
<p>Set the website title in the navbar.</p>
<div class="input-group mb-3">
<span class="input-group-text" id="website-title">Title</span>
@@ -220,9 +247,38 @@
placeholder="e.g. 'UpSnap'"
aria-label="Website title"
aria-describedby="website-title"
bind:value={$settings.website_title}
bind:value={$settings_public.website_title}
/>
</div>
<h3 class="my-3">Icon</h3>
<img
src=""
alt=""
class="img-fluid icon-preview mb-3"
bind:this={iconPreview}
/>
<div class="mb-3">
<label for="iconInput" class="form-label"
>Set a custom favicon (.ico, .png, .svg, .jpg/.jpeg).</label
>
<div class="input-group">
<input
class="form-control"
type="file"
id="iconInput"
accept=".ico,.png,.svg,.gif,.jpg,.jpeg"
bind:files={iconFiles}
/>
<button
type="button"
class="btn btn-outline-danger"
on:click={() => {
iconFiles = [];
$settings_public.favicon = '';
}}>Back to Gopher</button
>
</div>
</div>
</div>
<div class="col-md-6">
<div class="callout callout-info m-0">
@@ -281,7 +337,7 @@
placeholder="e.g. '192.168.1.0/24'"
aria-label="ip-range"
aria-describedby="ip-range"
bind:value={$settings.scan_range}
bind:value={$settings_private.scan_range}
/>
</div>
<button
@@ -290,7 +346,8 @@
class:btn-success={buttons.scan.state === 'success' ? true : false}
class:btn-warning={buttons.scan.state === 'waiting' ? true : false}
class:btn-danger={buttons.scan.state === 'failed' ? true : false}
disabled={$settings.scan_range === '' || buttons.scan.state !== 'none'
disabled={$settings_private.scan_range === '' ||
buttons.scan.state !== 'none'
? true
: false}
>
@@ -375,7 +432,7 @@
aria-describedby="Restore"
type="file"
accept=".json"
bind:files
bind:files={restoreFiles}
/>
<button
type="button"
@@ -383,7 +440,7 @@
class:btn-success={buttons.restore.state === 'success' ? true : false}
class:btn-warning={buttons.restore.state === 'waiting' ? true : false}
class:btn-danger={buttons.restore.state === 'failed' ? true : false}
disabled={files ? false : true}
disabled={restoreFiles ? false : true}
on:click={() => restoreV2Backup()}
>
{#if buttons.restore.state === 'none'}
@@ -408,3 +465,9 @@
{/if}
</p>
</div>
<style lang="scss">
.icon-preview {
height: 130px;
}
</style>

View File

@@ -10,8 +10,13 @@ pb.autoCancellation(false);
// export stores
export const pocketbase = writable(pb);
export const settings = writable({
website_title: ''
export const settings_private = writable({
interal: '',
scan_range: ''
});
export const settings_public = writable({
website_title: '',
favicon: ''
});
export const devices = writable({});
export const authorizedStore = writable(false);