mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
Merge branch 'manuel-rw-gridstack' into gridstack-wip-meierschlumpf
This commit is contained in:
@@ -4,205 +4,37 @@
|
|||||||
"name": "default"
|
"name": "default"
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
{
|
|
||||||
"id": "c1c4bec3-1044-4a80-957f-afe7ff49f421",
|
|
||||||
"name": "Test",
|
|
||||||
"position": 2
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f",
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f",
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"name": "Example Category"
|
"name": "Example Category"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c8407d2c-2353-4775-87c3-602f6f2684d5",
|
||||||
|
"name": "Test",
|
||||||
|
"position": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c1c4bec3-1044-4a80-957f-afe7ff49f421",
|
||||||
|
"name": "Test",
|
||||||
|
"position": 2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"wrappers": [
|
"wrappers": [
|
||||||
{
|
{
|
||||||
"id": "943f0681-a15b-4576-9a61-a74bd6fdd3ab",
|
"id": "5823c4d6-6baf-4436-b990-93fe77e1dc62",
|
||||||
"position": 1
|
"position": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "default",
|
"id": "943f0681-a15b-4576-9a61-a74bd6fdd3ab",
|
||||||
"position": 3
|
"position": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "default",
|
||||||
|
"position": 5
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"apps": [
|
"apps": [
|
||||||
{
|
|
||||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
|
|
||||||
"name": "Donate",
|
|
||||||
"url": "https://ko-fi.com/ajnart",
|
|
||||||
"behaviour": {
|
|
||||||
"onClickUrl": "https://ko-fi.com/ajnart",
|
|
||||||
"externalUrl": "https://ko-fi.com/ajnart",
|
|
||||||
"isOpeningNewTab": true
|
|
||||||
},
|
|
||||||
"network": {
|
|
||||||
"enabledStatusChecker": false,
|
|
||||||
"okStatus": [
|
|
||||||
200
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"appearance": {
|
|
||||||
"iconUrl": "https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png"
|
|
||||||
},
|
|
||||||
"integration": {
|
|
||||||
"type": null,
|
|
||||||
"properties": []
|
|
||||||
},
|
|
||||||
"area": {
|
|
||||||
"type": "sidebar",
|
|
||||||
"properties": {
|
|
||||||
"location": "left"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shape": {
|
|
||||||
"md": {
|
|
||||||
"location": {
|
|
||||||
"x": 1,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sm": {
|
|
||||||
"location": {
|
|
||||||
"x": 1,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lg": {
|
|
||||||
"location": {
|
|
||||||
"x": 1,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "76217a87-7151-42d0-b0cf-1b72aef63f83",
|
|
||||||
"name": "Small app",
|
|
||||||
"url": "https://homarr.dev",
|
|
||||||
"appearance": {
|
|
||||||
"iconUrl": "/imgs/logo/logo.png"
|
|
||||||
},
|
|
||||||
"network": {
|
|
||||||
"enabledStatusChecker": false,
|
|
||||||
"okStatus": []
|
|
||||||
},
|
|
||||||
"behaviour": {
|
|
||||||
"isOpeningNewTab": true,
|
|
||||||
"externalUrl": "https://homarr.dev"
|
|
||||||
},
|
|
||||||
"area": {
|
|
||||||
"type": "sidebar",
|
|
||||||
"properties": {
|
|
||||||
"location": "left"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shape": {
|
|
||||||
"md": {
|
|
||||||
"location": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sm": {
|
|
||||||
"location": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lg": {
|
|
||||||
"location": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integration": {
|
|
||||||
"type": null,
|
|
||||||
"properties": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e41a11f5-9c6e-41bc-ac0e-4c4c47582faa",
|
|
||||||
"name": "Haha",
|
|
||||||
"url": "https://homarr.dev",
|
|
||||||
"appearance": {
|
|
||||||
"iconUrl": "/imgs/logo/logo.png"
|
|
||||||
},
|
|
||||||
"network": {
|
|
||||||
"enabledStatusChecker": false,
|
|
||||||
"okStatus": []
|
|
||||||
},
|
|
||||||
"behaviour": {
|
|
||||||
"isOpeningNewTab": true,
|
|
||||||
"externalUrl": ""
|
|
||||||
},
|
|
||||||
"area": {
|
|
||||||
"type": "category",
|
|
||||||
"properties": {
|
|
||||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shape": {
|
|
||||||
"md": {
|
|
||||||
"location": {
|
|
||||||
"x": 5,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sm": {
|
|
||||||
"location": {
|
|
||||||
"x": 2,
|
|
||||||
"y": 2
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lg": {
|
|
||||||
"location": {
|
|
||||||
"x": 4,
|
|
||||||
"y": 2
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integration": {
|
|
||||||
"type": null,
|
|
||||||
"properties": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
|
||||||
"name": "Discord",
|
"name": "Discord",
|
||||||
@@ -234,8 +66,8 @@
|
|||||||
"shape": {
|
"shape": {
|
||||||
"md": {
|
"md": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 3,
|
"x": 2,
|
||||||
"y": 0
|
"y": 1
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 1,
|
"width": 1,
|
||||||
@@ -254,7 +86,7 @@
|
|||||||
},
|
},
|
||||||
"lg": {
|
"lg": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 0,
|
"x": 2,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -268,16 +100,23 @@
|
|||||||
"id": "5df743d9-5cb1-457c-85d2-64ff86855652",
|
"id": "5df743d9-5cb1-457c-85d2-64ff86855652",
|
||||||
"name": "Your app",
|
"name": "Your app",
|
||||||
"url": "https://homarr.dev",
|
"url": "https://homarr.dev",
|
||||||
"appearance": {
|
"behaviour": {
|
||||||
"iconUrl": "/imgs/logo/logo.png"
|
"onClickUrl": "https://homarr.dev",
|
||||||
|
"externalUrl": "https://homarr.dev",
|
||||||
|
"isOpeningNewTab": true
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"enabledStatusChecker": false,
|
"enabledStatusChecker": false,
|
||||||
"okStatus": []
|
"okStatus": [
|
||||||
|
200
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"behaviour": {
|
"appearance": {
|
||||||
"isOpeningNewTab": true,
|
"iconUrl": "/imgs/logo/logo.png"
|
||||||
"externalUrl": "https://homarr.dev"
|
},
|
||||||
|
"integration": {
|
||||||
|
"type": null,
|
||||||
|
"properties": []
|
||||||
},
|
},
|
||||||
"area": {
|
"area": {
|
||||||
"type": "category",
|
"type": "category",
|
||||||
@@ -288,8 +127,69 @@
|
|||||||
"shape": {
|
"shape": {
|
||||||
"md": {
|
"md": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 2,
|
"x": 0,
|
||||||
"y": 0
|
"y": 1
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 2,
|
||||||
|
"height": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sm": {
|
||||||
|
"location": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 2,
|
||||||
|
"height": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lg": {
|
||||||
|
"location": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 2,
|
||||||
|
"height": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
|
||||||
|
"name": "Donate",
|
||||||
|
"url": "https://ko-fi.com/ajnart",
|
||||||
|
"behaviour": {
|
||||||
|
"onClickUrl": "https://ko-fi.com/ajnart",
|
||||||
|
"externalUrl": "https://ko-fi.com/ajnart",
|
||||||
|
"isOpeningNewTab": true
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"enabledStatusChecker": false,
|
||||||
|
"okStatus": [
|
||||||
|
200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"iconUrl": "https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png"
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"type": null,
|
||||||
|
"properties": []
|
||||||
|
},
|
||||||
|
"area": {
|
||||||
|
"type": "category",
|
||||||
|
"properties": {
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shape": {
|
||||||
|
"md": {
|
||||||
|
"location": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 1
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 1,
|
"width": 1,
|
||||||
@@ -298,7 +198,7 @@
|
|||||||
},
|
},
|
||||||
"sm": {
|
"sm": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 2,
|
"x": 3,
|
||||||
"y": 1
|
"y": 1
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -308,8 +208,62 @@
|
|||||||
},
|
},
|
||||||
"lg": {
|
"lg": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 2,
|
"x": 3,
|
||||||
"y": 2
|
"y": 1
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 1,
|
||||||
|
"height": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e41a11f5-9c6e-41bc-ac0e-4c4c47582faa",
|
||||||
|
"name": "Your app",
|
||||||
|
"url": "https://homarr.dev",
|
||||||
|
"appearance": {
|
||||||
|
"iconUrl": "/imgs/logo/logo.png"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"enabledStatusChecker": false,
|
||||||
|
"okStatus": []
|
||||||
|
},
|
||||||
|
"behaviour": {
|
||||||
|
"isOpeningNewTab": true,
|
||||||
|
"externalUrl": ""
|
||||||
|
},
|
||||||
|
"area": {
|
||||||
|
"type": "sidebar",
|
||||||
|
"properties": {
|
||||||
|
"location": "left"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shape": {
|
||||||
|
"md": {
|
||||||
|
"location": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 1,
|
||||||
|
"height": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sm": {
|
||||||
|
"location": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 1,
|
||||||
|
"height": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lg": {
|
||||||
|
"location": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 1,
|
"width": 1,
|
||||||
@@ -333,12 +287,10 @@
|
|||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"enabledStatusChecker": false,
|
"enabledStatusChecker": false,
|
||||||
"okStatus": [
|
"okStatus": []
|
||||||
200
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/github.png"
|
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png"
|
||||||
},
|
},
|
||||||
"integration": {
|
"integration": {
|
||||||
"type": null,
|
"type": null,
|
||||||
@@ -354,7 +306,7 @@
|
|||||||
"md": {
|
"md": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 1
|
"y": 2
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 1,
|
"width": 1,
|
||||||
@@ -363,8 +315,8 @@
|
|||||||
},
|
},
|
||||||
"sm": {
|
"sm": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 1,
|
"x": 0,
|
||||||
"y": 1
|
"y": 2
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 1,
|
"width": 1,
|
||||||
@@ -373,7 +325,7 @@
|
|||||||
},
|
},
|
||||||
"lg": {
|
"lg": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 3,
|
"x": 0,
|
||||||
"y": 2
|
"y": 2
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -383,6 +335,64 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "76217a87-7151-42d0-b0cf-1b72aef63f83",
|
||||||
|
"name": "Small app",
|
||||||
|
"url": "https://homarr.dev",
|
||||||
|
"appearance": {
|
||||||
|
"iconUrl": "/imgs/logo/logo.png"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"enabledStatusChecker": false,
|
||||||
|
"okStatus": []
|
||||||
|
},
|
||||||
|
"behaviour": {
|
||||||
|
"isOpeningNewTab": true,
|
||||||
|
"externalUrl": "https://homarr.dev"
|
||||||
|
},
|
||||||
|
"area": {
|
||||||
|
"type": "category",
|
||||||
|
"properties": {
|
||||||
|
"id": "c8407d2c-2353-4775-87c3-602f6f2684d5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shape": {
|
||||||
|
"md": {
|
||||||
|
"location": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 6,
|
||||||
|
"height": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sm": {
|
||||||
|
"location": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 8,
|
||||||
|
"height": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lg": {
|
||||||
|
"location": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 8,
|
||||||
|
"height": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integration": {
|
||||||
|
"type": null,
|
||||||
|
"properties": []
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "615e43bd-f0aa-4117-ba49-b6495c039f3e",
|
"id": "615e43bd-f0aa-4117-ba49-b6495c039f3e",
|
||||||
"name": "Your app",
|
"name": "Your app",
|
||||||
@@ -418,7 +428,7 @@
|
|||||||
"md": {
|
"md": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 9
|
"y": 5
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 1,
|
"width": 1,
|
||||||
@@ -440,110 +450,49 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"properties": []
|
"properties": []
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33a",
|
|
||||||
"name": "Documentation",
|
|
||||||
"url": "https://homarr.dev",
|
|
||||||
"behaviour": {
|
|
||||||
"onClickUrl": "https://homarr.dev",
|
|
||||||
"externalUrl": "https://homarr.dev",
|
|
||||||
"isOpeningNewTab": true
|
|
||||||
},
|
|
||||||
"network": {
|
|
||||||
"enabledStatusChecker": false,
|
|
||||||
"okStatus": [
|
|
||||||
200
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"appearance": {
|
|
||||||
"iconUrl": "/imgs/logo/logo.png"
|
|
||||||
},
|
|
||||||
"integration": {
|
|
||||||
"type": null,
|
|
||||||
"properties": []
|
|
||||||
},
|
|
||||||
"area": {
|
|
||||||
"type": "category",
|
|
||||||
"properties": {
|
|
||||||
"id": "c1c4bec3-1044-4a80-957f-afe7ff49f421"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shape": {
|
|
||||||
"md": {
|
|
||||||
"location": {
|
|
||||||
"x": 2,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 2,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sm": {
|
|
||||||
"location": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 2,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lg": {
|
|
||||||
"location": {
|
|
||||||
"x": 1,
|
|
||||||
"y": 2
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"widgets": [
|
"widgets": [
|
||||||
{
|
{
|
||||||
"id": "date",
|
"id": "calendar",
|
||||||
"properties": {
|
"properties": {
|
||||||
"display24HourFormat": true
|
"sundayStart": false
|
||||||
},
|
},
|
||||||
"area": {
|
"area": {
|
||||||
"type": "category",
|
"type": "wrapper",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
"id": "default"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shape": {
|
"shape": {
|
||||||
"sm": {
|
|
||||||
"location": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 2
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 2,
|
|
||||||
"height": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"md": {
|
"md": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 4,
|
"x": 0,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 2,
|
"width": 12,
|
||||||
"height": 1
|
"height": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sm": {
|
||||||
|
"location": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 12,
|
||||||
|
"height": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lg": {
|
"lg": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 7,
|
"x": 0,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 5,
|
"width": 12,
|
||||||
"height": 2
|
"height": 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -577,7 +526,7 @@
|
|||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 3,
|
"width": 2,
|
||||||
"height": 1
|
"height": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -587,52 +536,52 @@
|
|||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 7,
|
"width": 2,
|
||||||
"height": 2
|
"height": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "calendar",
|
"id": "date",
|
||||||
"properties": {
|
"properties": {
|
||||||
"sundayStart": false
|
"display24HourFormat": true
|
||||||
},
|
},
|
||||||
"area": {
|
"area": {
|
||||||
"type": "wrapper",
|
"type": "category",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": "default"
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shape": {
|
"shape": {
|
||||||
"md": {
|
|
||||||
"location": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 4
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"width": 3,
|
|
||||||
"height": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sm": {
|
"sm": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 0,
|
"x": 2,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 3,
|
"width": 2,
|
||||||
"height": 5
|
"height": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"md": {
|
||||||
|
"location": {
|
||||||
|
"x": 2,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"width": 2,
|
||||||
|
"height": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lg": {
|
"lg": {
|
||||||
"location": {
|
"location": {
|
||||||
"x": 0,
|
"x": 2,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"width": 3,
|
"width": 2,
|
||||||
"height": 5
|
"height": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -647,8 +596,8 @@
|
|||||||
},
|
},
|
||||||
"customization": {
|
"customization": {
|
||||||
"layout": {
|
"layout": {
|
||||||
"enabledLeftSidebar": false,
|
"enabledLeftSidebar": true,
|
||||||
"enabledRightSidebar": false,
|
"enabledRightSidebar": true,
|
||||||
"enabledDocker": false,
|
"enabledDocker": false,
|
||||||
"enabledPing": false,
|
"enabledPing": false,
|
||||||
"enabledSearchbar": true
|
"enabledSearchbar": true
|
||||||
@@ -659,9 +608,9 @@
|
|||||||
"backgroundImageUrl": "",
|
"backgroundImageUrl": "",
|
||||||
"customCss": "",
|
"customCss": "",
|
||||||
"colors": {
|
"colors": {
|
||||||
"primary": "red",
|
"primary": "pink",
|
||||||
"secondary": "orange",
|
"secondary": "yellow",
|
||||||
"shade": 5
|
"shade": 4
|
||||||
},
|
},
|
||||||
"appOpacity": 100
|
"appOpacity": 100
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ module.exports = withBundleAnalyzer({
|
|||||||
images: {
|
images: {
|
||||||
domains: ['cdn.jsdelivr.net'],
|
domains: ['cdn.jsdelivr.net'],
|
||||||
},
|
},
|
||||||
reactStrictMode: false,
|
reactStrictMode: true,
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
i18n,
|
i18n,
|
||||||
});
|
});
|
||||||
|
|||||||
3
public/locales/en/layout/mobile/drawer.json
Normal file
3
public/locales/en/layout/mobile/drawer.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"title": "{{position}} sidebar"
|
||||||
|
}
|
||||||
@@ -46,11 +46,17 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"label": "Integration configuration",
|
"label": "Integration configuration",
|
||||||
"description": "Treats this app as the selected integration and provides you with per-app configuration",
|
"description": "Treats this app as the selected integration and provides you with per-app configuration",
|
||||||
"placeholder": "Select an integration"
|
"placeholder": "Select an integration",
|
||||||
|
"defined": "Defined",
|
||||||
|
"undefined": "Undefined",
|
||||||
|
"public": "Public",
|
||||||
|
"private": "Private",
|
||||||
|
"explanationPublic": "A private secret will be sent to the server. Once your browser has refreshed the page, it will never be sent to the client.",
|
||||||
|
"explanationPrivate": "A public secret will always be sent to the client and is accessible over the API. It should not contain any confidential values such as usernames, passwords, tokens, certificates and similar"
|
||||||
},
|
},
|
||||||
"secrets": {
|
"secrets": {
|
||||||
"description": "To update a secret, enter a value and click the save button. To remove a secret, use the clear button.",
|
"description": "To update a secret, enter a value and click the save button. To remove a secret, use the clear button.",
|
||||||
"warning": "Please note that Homarr removes secrets from the configuration for security reasons. Thus, you can only either define or unset any credentials. Your credentials act as the main access for your integrations and you should <strong>never</strong> share them with anybody else. Make sure to <strong>store and manage your secrets safely</strong>.",
|
"warning": "Your credentials act as the access for your integrations and you should <strong>never</strong> share them with anybody else. The official Homarr team will never ask for credentials. Make sure to <strong>store and manage your secrets safely</strong>.",
|
||||||
"clear": "Clear secret",
|
"clear": "Clear secret",
|
||||||
"save": "Save secret",
|
"save": "Save secret",
|
||||||
"update": "Update secret"
|
"update": "Update secret"
|
||||||
|
|||||||
@@ -26,5 +26,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tip": "You can select the search bar with the shortcut ",
|
"tip": "You can select the search bar with the shortcut ",
|
||||||
"switchedSearchEngine": "Switched to searching with {{searchEngine}}"
|
"switchedSearchEngine": "Switched to searching with {{searchEngine}}",
|
||||||
|
"configurationName": "Search engine configuration"
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,9 @@
|
|||||||
"description": "Displays a list of the torrent which are currently downloading",
|
"description": "Displays a list of the torrent which are currently downloading",
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings for BitTorrent integration",
|
"title": "Settings for BitTorrent integration",
|
||||||
|
"refreshInterval": {
|
||||||
|
"label": "Refresh interval (in seconds)"
|
||||||
|
},
|
||||||
"displayCompletedTorrents": {
|
"displayCompletedTorrents": {
|
||||||
"label": "Display completed torrents"
|
"label": "Display completed torrents"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"configSelect": {
|
"configSelect": {
|
||||||
"label": "Config loader"
|
"label": "Config loader",
|
||||||
|
"loadingNew": "Loading your config...",
|
||||||
|
"pleaseWait": "Please wait until your new config is loaded"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": "Choose the name of your new config",
|
"title": "Choose the name of your new config",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { CURRENT_VERSION } from '../../../data/constants';
|
|||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { useConfigStore } from '../../config/store';
|
import { useConfigStore } from '../../config/store';
|
||||||
import { usePrimaryGradient } from '../layout/useGradient';
|
import { usePrimaryGradient } from '../layout/useGradient';
|
||||||
|
import Credits from '../Settings/Common/Credits';
|
||||||
|
|
||||||
interface AboutModalProps {
|
interface AboutModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
@@ -113,6 +114,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
|||||||
Discord
|
Discord
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<Credits />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Center, Loader, Select, Tooltip } from '@mantine/core';
|
import { Center, Dialog, Loader, Notification, Select, Tooltip } from '@mantine/core';
|
||||||
|
import { useToggle } from '@mantine/hooks';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { setCookie } from 'cookies-next';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
@@ -11,19 +13,22 @@ export default function ConfigChanger() {
|
|||||||
|
|
||||||
const { data: configs, isLoading, isError } = useConfigsQuery();
|
const { data: configs, isLoading, isError } = useConfigsQuery();
|
||||||
const [activeConfig, setActiveConfig] = useState(configName);
|
const [activeConfig, setActiveConfig] = useState(configName);
|
||||||
|
const [isRefreshing, toggle] = useToggle();
|
||||||
|
|
||||||
const onConfigChange = (value: string) => {
|
const onConfigChange = (value: string) => {
|
||||||
// TODO: check what should happen here with @manuel-rw
|
// TODO: check what should happen here with @manuel-rw
|
||||||
// Wheter it should check for the current url and then load the new config only on index
|
// Wheter it should check for the current url and then load the new config only on index
|
||||||
// Or it should always load the selected config and open index or ? --> change url to page
|
// Or it should always load the selected config and open index or ? --> change url to page
|
||||||
setActiveConfig(value);
|
setCookie('config-name', value ?? 'default', {
|
||||||
/*
|
|
||||||
loadConfig(e ?? 'default');
|
|
||||||
setCookie('config-name', e ?? 'default', {
|
|
||||||
maxAge: 60 * 60 * 24 * 30,
|
maxAge: 60 * 60 * 24 * 30,
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
});
|
});
|
||||||
*/
|
setActiveConfig(value);
|
||||||
|
toggle();
|
||||||
|
// Use timeout to wait for the cookie to be set
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// If configlist is empty, return a loading indicator
|
// If configlist is empty, return a loading indicator
|
||||||
@@ -38,12 +43,26 @@ export default function ConfigChanger() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Select
|
<Select
|
||||||
label={t('configSelect.label')}
|
label={t('configSelect.label')}
|
||||||
value={activeConfig}
|
value={activeConfig}
|
||||||
onChange={onConfigChange}
|
onChange={onConfigChange}
|
||||||
data={configs}
|
data={configs}
|
||||||
/>
|
/>
|
||||||
|
<Dialog
|
||||||
|
position={{ top: 0, left: 0 }}
|
||||||
|
unstyled
|
||||||
|
opened={isRefreshing}
|
||||||
|
onClose={() => toggle()}
|
||||||
|
size="lg"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Notification loading title={t('configSelect.loadingNew')} radius="md" disallowClose>
|
||||||
|
{t('configSelect.pleaseWait')}
|
||||||
|
</Notification>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Group, Stack, Text, Title, useMantineTheme } from '@mantine/core';
|
|||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { IconCheck as Check, IconPhoto, IconUpload, IconX, IconX as X } from '@tabler/icons';
|
import { IconCheck as Check, IconPhoto, IconUpload, IconX, IconX as X } from '@tabler/icons';
|
||||||
|
import Consola from 'consola';
|
||||||
import { setCookie } from 'cookies-next';
|
import { setCookie } from 'cookies-next';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigStore } from '../../config/store';
|
import { useConfigStore } from '../../config/store';
|
||||||
@@ -36,9 +37,7 @@ export const LoadConfigComponent = () => {
|
|||||||
let newConfig: ConfigType = JSON.parse(fileText);
|
let newConfig: ConfigType = JSON.parse(fileText);
|
||||||
|
|
||||||
if (!newConfig.schemaVersion) {
|
if (!newConfig.schemaVersion) {
|
||||||
// client side logging
|
Consola.warn(
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn(
|
|
||||||
'a legacy configuration schema was deteced and migrated to the current schema'
|
'a legacy configuration schema was deteced and migrated to the current schema'
|
||||||
);
|
);
|
||||||
const oldConfig = JSON.parse(fileText) as Config;
|
const oldConfig = JSON.parse(fileText) as Config;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ActionIcon, createStyles } from '@mantine/core';
|
import { ActionIcon, createStyles, Space } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { IconChevronLeft, IconChevronRight } from '@tabler/icons';
|
import { IconChevronLeft, IconChevronRight } from '@tabler/icons';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
@@ -35,7 +35,9 @@ export const MobileRibbons = () => {
|
|||||||
location="left"
|
location="left"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : (
|
||||||
|
<Space />
|
||||||
|
)}
|
||||||
|
|
||||||
{layoutSettings.enabledRightSidebar ? (
|
{layoutSettings.enabledRightSidebar ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Drawer, Title } from '@mantine/core';
|
import { Drawer, Title } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import { DashboardSidebar } from '../../Wrappers/Sidebar/Sidebar';
|
import { DashboardSidebar } from '../../Wrappers/Sidebar/Sidebar';
|
||||||
|
|
||||||
interface MobileRibbonSidebarDrawerProps {
|
interface MobileRibbonSidebarDrawerProps {
|
||||||
@@ -10,16 +11,25 @@ interface MobileRibbonSidebarDrawerProps {
|
|||||||
export const MobileRibbonSidebarDrawer = ({
|
export const MobileRibbonSidebarDrawer = ({
|
||||||
location,
|
location,
|
||||||
...props
|
...props
|
||||||
}: MobileRibbonSidebarDrawerProps) => (
|
}: MobileRibbonSidebarDrawerProps) => {
|
||||||
|
const { t } = useTranslation('layout/mobile/drawer');
|
||||||
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
|
padding={10}
|
||||||
position={location}
|
position={location}
|
||||||
title={<Title order={4}>{location} sidebar</Title>}
|
title={<Title order={4}>{t('title', { position: location })}</Title>}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
|
styles={{
|
||||||
|
title: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<DashboardSidebar location={location} isGridstackReady />
|
<DashboardSidebar location={location} isGridstackReady />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const IconSelector = ({ onChange, allowAppNamePropagation, form }: IconSe
|
|||||||
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
||||||
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
|
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
|
||||||
converter: (item) => ({
|
converter: (item) => ({
|
||||||
url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${item.name}`,
|
url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/${item.name}`,
|
||||||
fileName: item.name,
|
fileName: item.name,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,15 +9,21 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { TablerIcon } from '@tabler/icons';
|
import { TablerIcon } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { AppIntegrationPropertyAccessabilityType } from '../../../../../../../../types/app';
|
||||||
|
|
||||||
interface GenericSecretInputProps {
|
interface GenericSecretInputProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
setIcon: TablerIcon;
|
setIcon: TablerIcon;
|
||||||
|
secretIsPresent: boolean;
|
||||||
|
type: AppIntegrationPropertyAccessabilityType;
|
||||||
onClickUpdateButton: (value: string | undefined) => void;
|
onClickUpdateButton: (value: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +31,8 @@ export const GenericSecretInput = ({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
setIcon,
|
setIcon,
|
||||||
|
secretIsPresent,
|
||||||
|
type,
|
||||||
onClickUpdateButton,
|
onClickUpdateButton,
|
||||||
...props
|
...props
|
||||||
}: GenericSecretInputProps) => {
|
}: GenericSecretInputProps) => {
|
||||||
@@ -36,17 +44,61 @@ export const GenericSecretInput = ({
|
|||||||
const { t } = useTranslation(['layout/modals/add-app', 'common']);
|
const { t } = useTranslation(['layout/modals/add-app', 'common']);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card withBorder>
|
<Card p="xs" withBorder>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.Col className={classes.alignSelfCenter} xs={12} md={6}>
|
<Grid.Col className={classes.alignSelfCenter} xs={12} md={6}>
|
||||||
<Group spacing="sm">
|
<Group spacing="sm">
|
||||||
<ThemeIcon color="green" variant="light">
|
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light" size="lg">
|
||||||
<Icon size={18} />
|
<Icon size={18} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0}>
|
||||||
|
<Group spacing="xs">
|
||||||
<Title className={classes.subtitle} order={6}>
|
<Title className={classes.subtitle} order={6}>
|
||||||
{t(label)}
|
{t(label)}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
|
<Group spacing="xs">
|
||||||
|
{secretIsPresent ? (
|
||||||
|
<Badge className={classes.textTransformUnset} color="green" variant="dot">
|
||||||
|
{t('integration.type.defined')}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge className={classes.textTransformUnset} color="red" variant="dot">
|
||||||
|
{t('integration.type.undefined')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{type === 'private' ? (
|
||||||
|
<Tooltip
|
||||||
|
label={t('integration.type.explanationPrivate')}
|
||||||
|
width={200}
|
||||||
|
multiline
|
||||||
|
withinPortal
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<Badge className={classes.textTransformUnset} color="orange" variant="dot">
|
||||||
|
{t('integration.type.private')}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip
|
||||||
|
label={t('integration.type.explanationPublic')}
|
||||||
|
width={200}
|
||||||
|
multiline
|
||||||
|
withinPortal
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<Badge className={classes.textTransformUnset} color="red" variant="dot">
|
||||||
|
{t('integration.type.public')}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{type === 'private'
|
||||||
|
? 'Private: Once saved, you cannot read out this value again'
|
||||||
|
: 'Public: Can be read out repeatedly'}
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
@@ -80,4 +132,7 @@ const useStyles = createStyles(() => ({
|
|||||||
alignSelfCenter: {
|
alignSelfCenter: {
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
|
textTransformUnset: {
|
||||||
|
textTransform: 'inherit',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
|||||||
label: 'Transmission',
|
label: 'Transmission',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'qbittorrent',
|
value: 'qBittorrent',
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/qbittorrent.png',
|
image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/qbittorrent.png',
|
||||||
label: 'qBittorrent',
|
label: 'qBittorrent',
|
||||||
},
|
},
|
||||||
@@ -100,16 +100,20 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
|||||||
placeholder={t('integration.type.placeholder')}
|
placeholder={t('integration.type.placeholder')}
|
||||||
itemComponent={SelectItemComponent}
|
itemComponent={SelectItemComponent}
|
||||||
data={data}
|
data={data}
|
||||||
maxDropdownHeight={150}
|
maxDropdownHeight={250}
|
||||||
dropdownPosition="bottom"
|
dropdownPosition="bottom"
|
||||||
clearable
|
clearable
|
||||||
variant="default"
|
variant="default"
|
||||||
searchable
|
searchable
|
||||||
|
filter={(value, item) =>
|
||||||
|
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
|
||||||
|
item.description?.toLowerCase().includes(value.toLowerCase().trim())
|
||||||
|
}
|
||||||
icon={
|
icon={
|
||||||
form.values.integration?.type && (
|
form.values.integration?.type && (
|
||||||
<img
|
<img
|
||||||
src={data.find((x) => x.value === form.values.integration?.type)?.image}
|
src={data.find((x) => x.value === form.values.integration?.type)?.image}
|
||||||
alt="test"
|
alt="integration"
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
@@ -119,6 +123,7 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
|||||||
form.setFieldValue('integration.properties', getNewProperties(value));
|
form.setFieldValue('integration.properties', getNewProperties(value));
|
||||||
inputProps.onChange(value);
|
inputProps.onChange(value);
|
||||||
}}
|
}}
|
||||||
|
withinPortal
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -126,17 +131,23 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
|||||||
|
|
||||||
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||||
image: string;
|
image: string;
|
||||||
|
description: string;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectItemComponent = forwardRef<HTMLDivElement, ItemProps>(
|
const SelectItemComponent = forwardRef<HTMLDivElement, ItemProps>(
|
||||||
({ image, label, ...others }: ItemProps, ref) => (
|
({ image, label, description, ...others }: ItemProps, ref) => (
|
||||||
<div ref={ref} {...others}>
|
<div ref={ref} {...others}>
|
||||||
<Group noWrap>
|
<Group noWrap>
|
||||||
<img src={image} alt="integration icon" width={20} height={20} />
|
<img src={image} alt="integration icon" width={20} height={20} />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm">{label}</Text>
|
<Text size="sm">{label}</Text>
|
||||||
|
{description && (
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
|
|||||||
const formValue = form.values.integration?.properties[indexInFormValue];
|
const formValue = form.values.integration?.properties[indexInFormValue];
|
||||||
|
|
||||||
const isPresent = formValue?.isDefined;
|
const isPresent = formValue?.isDefined;
|
||||||
|
const accessabilityType = formValue?.type;
|
||||||
|
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
return (
|
return (
|
||||||
@@ -57,6 +58,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
|
|||||||
secretIsPresent={isPresent}
|
secretIsPresent={isPresent}
|
||||||
setIcon={IconKey}
|
setIcon={IconKey}
|
||||||
value={formValue.value}
|
value={formValue.value}
|
||||||
|
type={accessabilityType}
|
||||||
{...form.getInputProps(`integration.properties.${index}.value`)}
|
{...form.getInputProps(`integration.properties.${index}.value`)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -72,6 +74,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
|
|||||||
value=""
|
value=""
|
||||||
secretIsPresent={isPresent}
|
secretIsPresent={isPresent}
|
||||||
setIcon={definition.icon}
|
setIcon={definition.icon}
|
||||||
|
type={accessabilityType}
|
||||||
{...form.getInputProps(`integration.properties.${index}.value`)}
|
{...form.getInputProps(`integration.properties.${index}.value`)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ export const AvailableElementTypes = ({
|
|||||||
iconUrl: '/imgs/logo/logo.png',
|
iconUrl: '/imgs/logo/logo.png',
|
||||||
},
|
},
|
||||||
network: {
|
network: {
|
||||||
enabledStatusChecker: false,
|
enabledStatusChecker: true,
|
||||||
okStatus: [],
|
okStatus: [200],
|
||||||
},
|
},
|
||||||
behaviour: {
|
behaviour: {
|
||||||
isOpeningNewTab: true,
|
isOpeningNewTab: true,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useModals } from '@mantine/modals';
|
import { useModals } from '@mantine/modals';
|
||||||
import { TablerIcon } from '@tabler/icons';
|
import { showNotification } from '@mantine/notifications';
|
||||||
|
import { IconChecks, TablerIcon } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../../../../../config/provider';
|
import { useConfigContext } from '../../../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../../../config/store';
|
import { useConfigStore } from '../../../../../../config/store';
|
||||||
@@ -83,8 +84,13 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement
|
|||||||
true,
|
true,
|
||||||
!isEditMode
|
!isEditMode
|
||||||
);
|
);
|
||||||
|
|
||||||
closeModal('selectElement');
|
closeModal('selectElement');
|
||||||
|
showNotification({
|
||||||
|
title: t('descriptor.name'),
|
||||||
|
message: t('descriptor.description'),
|
||||||
|
icon: <IconChecks stroke={1.5} />,
|
||||||
|
color: 'teal',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
import { Alert, Button, Group, MultiSelect, Stack, Switch, TextInput, Text } from '@mantine/core';
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
MultiSelect,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
TextInput,
|
||||||
|
Text,
|
||||||
|
NumberInput,
|
||||||
|
Slider,
|
||||||
|
} from '@mantine/core';
|
||||||
import { ContextModalProps } from '@mantine/modals';
|
import { ContextModalProps } from '@mantine/modals';
|
||||||
import { IconAlertTriangle } from '@tabler/icons';
|
import { IconAlertTriangle } from '@tabler/icons';
|
||||||
import { Trans, useTranslation } from 'next-i18next';
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
@@ -8,10 +19,12 @@ import type { IWidgetOptionValue } from '../../../../widgets/widgets';
|
|||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { IWidget } from '../../../../widgets/widgets';
|
import { IWidget } from '../../../../widgets/widgets';
|
||||||
|
import { useColorTheme } from '../../../../tools/color';
|
||||||
|
|
||||||
export type WidgetEditModalInnerProps = {
|
export type WidgetEditModalInnerProps = {
|
||||||
widgetId: string;
|
widgetId: string;
|
||||||
options: IWidget<string, any>['properties'];
|
options: IWidget<string, any>['properties'];
|
||||||
|
widgetOptions: IWidget<string, any>['properties'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type IntegrationOptionsValueType = IWidget<string, any>['properties'][string];
|
type IntegrationOptionsValueType = IWidget<string, any>['properties'][string];
|
||||||
@@ -23,7 +36,11 @@ export const WidgetsEditModal = ({
|
|||||||
}: ContextModalProps<WidgetEditModalInnerProps>) => {
|
}: ContextModalProps<WidgetEditModalInnerProps>) => {
|
||||||
const { t } = useTranslation([`modules/${innerProps.widgetId}`, 'common']);
|
const { t } = useTranslation([`modules/${innerProps.widgetId}`, 'common']);
|
||||||
const [moduleProperties, setModuleProperties] = useState(innerProps.options);
|
const [moduleProperties, setModuleProperties] = useState(innerProps.options);
|
||||||
const items = Object.entries(moduleProperties ?? {}) as [string, IntegrationOptionsValueType][];
|
// const items = Object.entries(moduleProperties ?? {}) as [string, IntegrationOptionsValueType][];
|
||||||
|
const items = Object.entries(innerProps.widgetOptions ?? {}) as [
|
||||||
|
string,
|
||||||
|
IntegrationOptionsValueType
|
||||||
|
][];
|
||||||
|
|
||||||
// Find the Key in the "Widgets" Object that matches the widgetId
|
// Find the Key in the "Widgets" Object that matches the widgetId
|
||||||
const currentWidgetDefinition = Widgets[innerProps.widgetId as keyof typeof Widgets];
|
const currentWidgetDefinition = Widgets[innerProps.widgetId as keyof typeof Widgets];
|
||||||
@@ -67,8 +84,9 @@ export const WidgetsEditModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
{items.map(([key, value], index) => {
|
{items.map(([key, defaultValue], index) => {
|
||||||
const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue;
|
const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue;
|
||||||
|
const value = moduleProperties[key] ?? defaultValue;
|
||||||
|
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return (
|
return (
|
||||||
@@ -83,7 +101,39 @@ export const WidgetsEditModal = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return WidgetOptionTypeSwitch(
|
||||||
|
option,
|
||||||
|
index,
|
||||||
|
t,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
handleChange,
|
||||||
|
getMutliselectData
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Group position="right">
|
||||||
|
<Button onClick={() => context.closeModal(id)} variant="light">
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>{t('common:save')}</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Widget switch
|
||||||
|
// Widget options are computed based on their type.
|
||||||
|
// here you can define new types for options (along with editing the widgets.d.ts file)
|
||||||
|
function WidgetOptionTypeSwitch(
|
||||||
|
option: IWidgetOptionValue,
|
||||||
|
index: number,
|
||||||
|
t: any,
|
||||||
|
key: string,
|
||||||
|
value: string | number | boolean | string[],
|
||||||
|
handleChange: (key: string, value: IntegrationOptionsValueType) => void,
|
||||||
|
getMutliselectData: (option: string) => any
|
||||||
|
) {
|
||||||
|
const { primaryColor, secondaryColor } = useColorTheme();
|
||||||
switch (option.type) {
|
switch (option.type) {
|
||||||
case 'switch':
|
case 'switch':
|
||||||
return (
|
return (
|
||||||
@@ -97,6 +147,7 @@ export const WidgetsEditModal = ({
|
|||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
color={primaryColor}
|
||||||
key={`${option.type}-${index}`}
|
key={`${option.type}-${index}`}
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
label={t(`descriptor.settings.${key}.label`)}
|
||||||
value={value as string}
|
value={value as string}
|
||||||
@@ -106,6 +157,7 @@ export const WidgetsEditModal = ({
|
|||||||
case 'multi-select':
|
case 'multi-select':
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
|
color={primaryColor}
|
||||||
key={`${option.type}-${index}`}
|
key={`${option.type}-${index}`}
|
||||||
data={getMutliselectData(key)}
|
data={getMutliselectData(key)}
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
label={t(`descriptor.settings.${key}.label`)}
|
||||||
@@ -113,16 +165,32 @@ export const WidgetsEditModal = ({
|
|||||||
onChange={(v) => handleChange(key, v)}
|
onChange={(v) => handleChange(key, v)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'number':
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
color={primaryColor}
|
||||||
|
key={`${option.type}-${index}`}
|
||||||
|
label={t(`descriptor.settings.${key}.label`)}
|
||||||
|
value={value as number}
|
||||||
|
onChange={(v) => handleChange(key, v!)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'slider':
|
||||||
|
return (
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
|
||||||
|
<Slider
|
||||||
|
color={primaryColor}
|
||||||
|
key={`${option.type}-${index}`}
|
||||||
|
value={value as number}
|
||||||
|
min={option.min}
|
||||||
|
max={option.max}
|
||||||
|
step={option.step}
|
||||||
|
onChange={(v) => handleChange(key, v)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})}
|
}
|
||||||
<Group position="right">
|
|
||||||
<Button onClick={() => context.closeModal(id)} variant="light">
|
|
||||||
{t('common:cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSave}>{t('common:save')}</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useWrapperColumnCount } from '../../Wrappers/gridstack/store';
|
|||||||
import { GenericTileMenu } from '../GenericTileMenu';
|
import { GenericTileMenu } from '../GenericTileMenu';
|
||||||
import { WidgetEditModalInnerProps } from './WidgetsEditModal';
|
import { WidgetEditModalInnerProps } from './WidgetsEditModal';
|
||||||
import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal';
|
import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal';
|
||||||
|
import WidgetsDefinitions from '../../../../widgets';
|
||||||
|
|
||||||
export type WidgetChangePositionModalInnerProps = {
|
export type WidgetChangePositionModalInnerProps = {
|
||||||
widgetId: string;
|
widgetId: string;
|
||||||
@@ -23,6 +24,14 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
|||||||
const wrapperColumnCount = useWrapperColumnCount();
|
const wrapperColumnCount = useWrapperColumnCount();
|
||||||
|
|
||||||
if (!widget || !wrapperColumnCount) return null;
|
if (!widget || !wrapperColumnCount) return null;
|
||||||
|
// Match widget.id with WidgetsDefinitions
|
||||||
|
// First get the keys
|
||||||
|
const keys = Object.keys(WidgetsDefinitions);
|
||||||
|
// Then find the key that matches the widget.id
|
||||||
|
const widgetDefinition = keys.find((key) => key === widget.id);
|
||||||
|
// Then get the widget definition
|
||||||
|
const widgetDefinitionObject =
|
||||||
|
WidgetsDefinitions[widgetDefinition as keyof typeof WidgetsDefinitions];
|
||||||
|
|
||||||
const handleDeleteClick = () => {
|
const handleDeleteClick = () => {
|
||||||
openContextModalGeneric<WidgetsRemoveModalInnerProps>({
|
openContextModalGeneric<WidgetsRemoveModalInnerProps>({
|
||||||
@@ -54,7 +63,10 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
|||||||
innerProps: {
|
innerProps: {
|
||||||
widgetId: integration,
|
widgetId: integration,
|
||||||
options: widget.properties,
|
options: widget.properties,
|
||||||
|
// Cast as the right type for the correct widget
|
||||||
|
widgetOptions: widgetDefinitionObject.options as any,
|
||||||
},
|
},
|
||||||
|
zIndex: 5,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Card } from '@mantine/core';
|
import { Card } from '@mantine/core';
|
||||||
import { RefObject } from 'react';
|
import { RefObject } from 'react';
|
||||||
|
import { useCardStyles } from '../../../layout/useCardStyles';
|
||||||
import { useGridstack } from '../gridstack/use-gridstack';
|
import { useGridstack } from '../gridstack/use-gridstack';
|
||||||
import { WrapperContent } from '../WrapperContent';
|
import { WrapperContent } from '../WrapperContent';
|
||||||
|
|
||||||
@@ -30,18 +31,23 @@ const SidebarInner = ({ location }: DashboardSidebarInnerProps) => {
|
|||||||
const { refs, apps, widgets } = useGridstack('sidebar', location);
|
const { refs, apps, widgets } = useGridstack('sidebar', location);
|
||||||
|
|
||||||
const minRow = useMinRowForFullHeight(refs.wrapper);
|
const minRow = useMinRowForFullHeight(refs.wrapper);
|
||||||
|
const {
|
||||||
|
cx,
|
||||||
|
classes: { card: cardClass },
|
||||||
|
} = useCardStyles(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Card withBorder mih="100%" p={0} radius="lg" className={cardClass} ref={refs.wrapper}>
|
||||||
<div
|
<div
|
||||||
className="grid-stack grid-stack-sidebar"
|
className="grid-stack grid-stack-sidebar"
|
||||||
style={{ transitionDuration: '0s', height: '100%' }}
|
style={{ transitionDuration: '0s', height: '100%' }}
|
||||||
data-sidebar={location}
|
data-sidebar={location}
|
||||||
// eslint-disable-next-line react/no-unknown-property
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
gs-min-row={minRow}
|
gs-min-row={minRow}
|
||||||
ref={refs.wrapper}
|
|
||||||
>
|
>
|
||||||
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
|
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Space, Stack, Text } from '@mantine/core';
|
import { ScrollArea, Space, Stack, Text } from '@mantine/core';
|
||||||
|
import { useViewportSize } from '@mantine/hooks';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
import ConfigChanger from '../../Config/ConfigChanger';
|
import ConfigChanger from '../../Config/ConfigChanger';
|
||||||
import ConfigActions from './Config/ConfigActions';
|
import ConfigActions from './Config/ConfigActions';
|
||||||
@@ -7,6 +8,7 @@ import { SearchEngineSelector } from './SearchEngine/SearchEngineSelector';
|
|||||||
|
|
||||||
export default function CommonSettings() {
|
export default function CommonSettings() {
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
|
const { height, width } = useViewportSize();
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return (
|
return (
|
||||||
@@ -15,14 +17,15 @@ export default function CommonSettings() {
|
|||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack mb="md" mr="sm">
|
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
|
||||||
|
<Stack>
|
||||||
<SearchEngineSelector searchEngine={config.settings.common.searchEngine} />
|
<SearchEngineSelector searchEngine={config.settings.common.searchEngine} />
|
||||||
<Space />
|
<Space />
|
||||||
<LanguageSelect />
|
<LanguageSelect />
|
||||||
<ConfigChanger />
|
<ConfigChanger />
|
||||||
<ConfigActions />
|
<ConfigActions />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default function ConfigActions() {
|
|||||||
closeModal={createCopyModal.close}
|
closeModal={createCopyModal.close}
|
||||||
initialConfigName={config.configProperties.name}
|
initialConfigName={config.configProperties.name}
|
||||||
/>
|
/>
|
||||||
<Flex gap="xs" justify="stretch">
|
<Flex gap="xs" mt="xs" justify="stretch">
|
||||||
<ActionIcon className={classes.actionIcon} onClick={handleDownload} variant="default">
|
<ActionIcon className={classes.actionIcon} onClick={handleDownload} variant="default">
|
||||||
<IconDownload size={20} />
|
<IconDownload size={20} />
|
||||||
<Text size="sm">{t('buttons.download')}</Text>
|
<Text size="sm">{t('buttons.download')}</Text>
|
||||||
|
|||||||
@@ -1,29 +1,11 @@
|
|||||||
import { Group, ActionIcon, Anchor, Text } from '@mantine/core';
|
import { Group, Anchor, Text } from '@mantine/core';
|
||||||
import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import { CURRENT_VERSION } from '../../../../data/constants';
|
|
||||||
|
|
||||||
export default function Credits() {
|
export default function Credits() {
|
||||||
const { t } = useTranslation('settings/common');
|
const { t } = useTranslation('settings/common');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group position="center" mt="xs">
|
<Group position="center" mt="xs">
|
||||||
<Group spacing={0}>
|
|
||||||
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
|
|
||||||
<IconBrandGithub size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
fontSize: '0.90rem',
|
|
||||||
color: 'gray',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{CURRENT_VERSION}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group spacing={1}>
|
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: '0.90rem',
|
fontSize: '0.90rem',
|
||||||
@@ -37,12 +19,9 @@ export default function Credits() {
|
|||||||
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||||
>
|
>
|
||||||
ajnart
|
ajnart
|
||||||
</Anchor>
|
</Anchor>{' '}
|
||||||
|
and you !
|
||||||
</Text>
|
</Text>
|
||||||
<ActionIcon<'a'> component="a" href="https://discord.gg/aCsmEV5RgA" size="lg">
|
|
||||||
<IconBrandDiscord size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const SearchEngineSelector = ({ searchEngine }: Props) => {
|
|||||||
/>
|
/>
|
||||||
<Paper p="md" py="sm" mb="md" withBorder>
|
<Paper p="md" py="sm" mb="md" withBorder>
|
||||||
<Title order={6} mb={0}>
|
<Title order={6} mb={0}>
|
||||||
Search engine configuration
|
{t('configurationName')}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<SearchNewTabSwitch defaultValue={searchEngine.properties.openInNewTab} />
|
<SearchNewTabSwitch defaultValue={searchEngine.properties.openInNewTab} />
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Button, ScrollArea, Stack } from '@mantine/core';
|
import { Button, ScrollArea, Stack } from '@mantine/core';
|
||||||
|
import { useViewportSize } from '@mantine/hooks';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigStore } from '../../../config/store';
|
||||||
import { LayoutSelector } from './Layout/LayoutSelector';
|
import { LayoutSelector } from './Layout/LayoutSelector';
|
||||||
@@ -14,20 +16,14 @@ import { ShadeSelector } from './Theme/ShadeSelector';
|
|||||||
|
|
||||||
export default function CustomizationSettings() {
|
export default function CustomizationSettings() {
|
||||||
const { config, name: configName } = useConfigContext();
|
const { config, name: configName } = useConfigContext();
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
const { height, width } = useViewportSize();
|
||||||
|
|
||||||
const { updateConfig } = useConfigStore();
|
const { updateConfig } = useConfigStore();
|
||||||
|
|
||||||
const saveConfiguration = () => {
|
|
||||||
if (!configName || !config) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConfig(configName, (_) => config, false, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack mb="md" mr="sm" mt="xs">
|
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
|
||||||
<ScrollArea style={{ height: '76vh' }} offsetScrollbars>
|
<Stack mt="xs" mb="md" spacing="xs">
|
||||||
<LayoutSelector defaultLayout={config?.settings.customization.layout} />
|
<LayoutSelector defaultLayout={config?.settings.customization.layout} />
|
||||||
<PageTitleChanger defaultValue={config?.settings.customization.pageTitle} />
|
<PageTitleChanger defaultValue={config?.settings.customization.pageTitle} />
|
||||||
<MetaTitleChanger defaultValue={config?.settings.customization.metaTitle} />
|
<MetaTitleChanger defaultValue={config?.settings.customization.metaTitle} />
|
||||||
@@ -45,11 +41,7 @@ export default function CustomizationSettings() {
|
|||||||
/>
|
/>
|
||||||
<ShadeSelector defaultValue={config?.settings.customization.colors.shade} />
|
<ShadeSelector defaultValue={config?.settings.customization.colors.shade} />
|
||||||
<OpacitySelector defaultValue={config?.settings.customization.appOpacity} />
|
<OpacitySelector defaultValue={config?.settings.customization.appOpacity} />
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
<Button onClick={saveConfiguration} variant="light">
|
|
||||||
Save Customizations
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function OpacitySelector({ defaultValue }: OpacitySelectorProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs" mb="md">
|
||||||
<Text>{t('label')}</Text>
|
<Text>{t('label')}</Text>
|
||||||
<Slider
|
<Slider
|
||||||
defaultValue={opacity}
|
defaultValue={opacity}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Drawer, ScrollArea, Tabs, Title } from '@mantine/core';
|
import { Drawer, Tabs, Title } from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useConfigContext } from '../../config/provider';
|
||||||
|
import { useConfigStore } from '../../config/store';
|
||||||
|
|
||||||
import CommonSettings from './Common/CommonSettings';
|
import CommonSettings from './Common/CommonSettings';
|
||||||
import Credits from './Common/Credits';
|
|
||||||
import CustomizationSettings from './Customization/CustomizationSettings';
|
import CustomizationSettings from './Customization/CustomizationSettings';
|
||||||
|
|
||||||
function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
|
function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
|
||||||
@@ -15,9 +16,7 @@ function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string })
|
|||||||
<Tabs.Tab value="customization">{t('tabs.customizations')}</Tabs.Tab>
|
<Tabs.Tab value="customization">{t('tabs.customizations')}</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Tabs.Panel data-autofocus value="common">
|
<Tabs.Panel data-autofocus value="common">
|
||||||
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
|
|
||||||
<CommonSettings />
|
<CommonSettings />
|
||||||
</ScrollArea>
|
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value="customization">
|
<Tabs.Panel value="customization">
|
||||||
<CustomizationSettings />
|
<CustomizationSettings />
|
||||||
@@ -37,6 +36,8 @@ export function SettingsDrawer({
|
|||||||
newVersionAvailable,
|
newVersionAvailable,
|
||||||
}: SettingsDrawerProps & { newVersionAvailable: string }) {
|
}: SettingsDrawerProps & { newVersionAvailable: string }) {
|
||||||
const { t } = useTranslation('settings/common');
|
const { t } = useTranslation('settings/common');
|
||||||
|
const { config, name: configName } = useConfigContext();
|
||||||
|
const { updateConfig } = useConfigStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
@@ -45,10 +46,16 @@ export function SettingsDrawer({
|
|||||||
position="right"
|
position="right"
|
||||||
title={<Title order={5}>{t('title')}</Title>}
|
title={<Title order={5}>{t('title')}</Title>}
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={closeDrawer}
|
onClose={() => {
|
||||||
|
closeDrawer();
|
||||||
|
if (!configName || !config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig(configName, (_) => config, false, true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SettingsMenu newVersionAvailable={newVersionAvailable} />
|
<SettingsMenu newVersionAvailable={newVersionAvailable} />
|
||||||
<Credits />
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ export const AddElementAction = ({ type }: AddElementActionProps) => {
|
|||||||
return (
|
return (
|
||||||
<Tooltip label={t('actionIcon.tooltip')} withinPortal withArrow>
|
<Tooltip label={t('actionIcon.tooltip')} withinPortal withArrow>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
|
||||||
radius="md"
|
radius="md"
|
||||||
color="blue"
|
variant="default"
|
||||||
style={{ height: 43 }}
|
style={{ height: 43 }}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openContextModal({
|
openContextModal({
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Consola from 'consola';
|
import Consola from 'consola';
|
||||||
import { ActionIcon, Button, Group, Popover, Text } from '@mantine/core';
|
import { ActionIcon, Button, Group, Title, Tooltip } from '@mantine/core';
|
||||||
import { IconEditCircle, IconEditCircleOff, IconX } from '@tabler/icons';
|
import { IconEditCircle, IconEditCircleOff } from '@tabler/icons';
|
||||||
import { getCookie } from 'cookies-next';
|
import { getCookie } from 'cookies-next';
|
||||||
import { Trans, useTranslation } from 'next-i18next';
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||||
import { useConfigContext } from '../../../../../config/provider';
|
import { useConfigContext } from '../../../../../config/provider';
|
||||||
import { useScreenSmallerThan } from '../../../../../hooks/useScreenSmallerThan';
|
import { useScreenSmallerThan } from '../../../../../hooks/useScreenSmallerThan';
|
||||||
|
|
||||||
@@ -13,7 +14,6 @@ import { AddElementAction } from '../AddElementAction/AddElementAction';
|
|||||||
|
|
||||||
export const ToggleEditModeAction = () => {
|
export const ToggleEditModeAction = () => {
|
||||||
const { enabled, toggleEditMode } = useEditModeStore();
|
const { enabled, toggleEditMode } = useEditModeStore();
|
||||||
const [popoverManuallyHidden, setPopoverManuallyHidden] = useState<boolean>();
|
|
||||||
|
|
||||||
const { t } = useTranslation('layout/header/actions/toggle-edit-mode');
|
const { t } = useTranslation('layout/header/actions/toggle-edit-mode');
|
||||||
|
|
||||||
@@ -29,21 +29,44 @@ export const ToggleEditModeAction = () => {
|
|||||||
|
|
||||||
const toggleButtonClicked = () => {
|
const toggleButtonClicked = () => {
|
||||||
toggleEditMode();
|
toggleEditMode();
|
||||||
|
if (!enabled) {
|
||||||
|
showNotification({
|
||||||
|
styles: (theme) => ({
|
||||||
|
root: {
|
||||||
|
backgroundColor: theme.colors.orange[7],
|
||||||
|
borderColor: theme.colors.orange[7],
|
||||||
|
|
||||||
setPopoverManuallyHidden(false);
|
'&::before': { backgroundColor: theme.white },
|
||||||
|
},
|
||||||
|
title: { color: theme.white },
|
||||||
|
description: { color: theme.white },
|
||||||
|
closeButton: {
|
||||||
|
color: theme.white,
|
||||||
|
'&:hover': { backgroundColor: theme.colors.orange[7] },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
radius: 'md',
|
||||||
|
id: 'toggle-edit-mode',
|
||||||
|
autoClose: false,
|
||||||
|
title: <Title order={4}>{t('popover.title')}</Title>,
|
||||||
|
message: <Trans i18nKey="layout/header/actions/toggle-edit-mode:popover.text" />,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
hideNotification('toggle-edit-mode');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ToggleButtonDesktop = () => (
|
const ToggleButtonDesktop = () => (
|
||||||
|
<Tooltip label={enabled ? t('button.enabled') : t('button.disabled')}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => toggleButtonClicked()}
|
onClick={() => toggleButtonClicked()}
|
||||||
leftIcon={enabled ? <IconEditCircleOff /> : <IconEditCircle />}
|
|
||||||
variant="default"
|
|
||||||
radius="md"
|
radius="md"
|
||||||
color="blue"
|
variant="default"
|
||||||
style={{ height: 43 }}
|
style={{ height: 43 }}
|
||||||
>
|
>
|
||||||
<Text>{enabled ? t('button.enabled') : t('button.disabled')}</Text>
|
{enabled ? <IconEditCircleOff /> : <IconEditCircle />}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ToggleActionIconMobile = () => (
|
const ToggleActionIconMobile = () => (
|
||||||
@@ -59,13 +82,7 @@ export const ToggleEditModeAction = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<>
|
||||||
opened={enabled && !smallerThanSm && !popoverManuallyHidden}
|
|
||||||
width="target"
|
|
||||||
transition="scale"
|
|
||||||
zIndex={199}
|
|
||||||
>
|
|
||||||
<Popover.Target>
|
|
||||||
{smallerThanSm ? (
|
{smallerThanSm ? (
|
||||||
enabled ? (
|
enabled ? (
|
||||||
<Group style={{ flexWrap: 'nowrap' }}>
|
<Group style={{ flexWrap: 'nowrap' }}>
|
||||||
@@ -83,21 +100,6 @@ export const ToggleEditModeAction = () => {
|
|||||||
) : (
|
) : (
|
||||||
<ToggleButtonDesktop />
|
<ToggleButtonDesktop />
|
||||||
)}
|
)}
|
||||||
</Popover.Target>
|
</>
|
||||||
|
|
||||||
<Popover.Dropdown p={4} px={6} mt={-5}>
|
|
||||||
<div style={{ position: 'absolute', top: 2, right: 2 }}>
|
|
||||||
<ActionIcon onClick={() => setPopoverManuallyHidden(true)}>
|
|
||||||
<IconX size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
</div>
|
|
||||||
<Text align="center" size="sm">
|
|
||||||
<Text weight="bold">{t('popover.title')}</Text>
|
|
||||||
<Text>
|
|
||||||
<Trans i18nKey="layout/header/actions/toggle-edit-mode:popover.text" />
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
</Popover.Dropdown>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CURRENT_VERSION, REPO_URL } from '../../../../data/constants';
|
|||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
import { Logo } from '../Logo';
|
import { Logo } from '../Logo';
|
||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../useCardStyles';
|
||||||
import DockerMenuButton from './Actions/Docker/DockerModule';
|
import DockerMenuButton from '../../../modules/Docker/DockerModule';
|
||||||
import { ToggleEditModeAction } from './Actions/ToggleEditMode/ToggleEditMode';
|
import { ToggleEditModeAction } from './Actions/ToggleEditMode/ToggleEditMode';
|
||||||
import { Search } from './Search';
|
import { Search } from './Search';
|
||||||
import { SettingsMenu } from './SettingsMenu';
|
import { SettingsMenu } from './SettingsMenu';
|
||||||
|
|||||||
@@ -55,9 +55,6 @@ export function Search() {
|
|||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [debounced, cancel] = useDebouncedValue(searchQuery, 250);
|
const [debounced, cancel] = useDebouncedValue(searchQuery, 250);
|
||||||
|
|
||||||
// TODO: ask manuel-rw about overseerr
|
|
||||||
// Answer: We can simply check if there is a app of the type overseer and display results if there is one.
|
|
||||||
// Overseerr is not use anywhere else, so it makes no sense to add a standalone toggle for displaying results
|
|
||||||
const isOverseerrEnabled = config?.apps.some(
|
const isOverseerrEnabled = config?.apps.some(
|
||||||
(x) => x.integration.type === 'overseerr' || x.integration.type === 'jellyseerr'
|
(x) => x.integration.type === 'overseerr' || x.integration.type === 'jellyseerr'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ActionIcon, Badge, Menu } from '@mantine/core';
|
import { Badge, Button, Menu } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { IconInfoCircle, IconMenu2, IconSettings } from '@tabler/icons';
|
import { IconInfoCircle, IconMenu2, IconSettings } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
@@ -15,9 +15,9 @@ export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: str
|
|||||||
<>
|
<>
|
||||||
<Menu width={250}>
|
<Menu width={250}>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<ActionIcon variant="default" radius="md" size="xl" color="blue">
|
<Button variant="default" radius="md" style={{ height: 43 }}>
|
||||||
<IconMenu2 />
|
<IconMenu2 />
|
||||||
</ActionIcon>
|
</Button>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<ColorSchemeSwitch />
|
<ColorSchemeSwitch />
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ interface smallAppItem {
|
|||||||
|
|
||||||
export default function SmallAppItem(props: any) {
|
export default function SmallAppItem(props: any) {
|
||||||
const { app }: { app: smallAppItem } = props;
|
const { app }: { app: smallAppItem } = props;
|
||||||
// TODO : Use Next/link
|
|
||||||
return (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
{app.icon && <Avatar src={app.icon} />}
|
{app.icon && <Avatar src={app.icon} />}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const POLLING_INTERVAL = 2000;
|
|||||||
|
|
||||||
interface TorrentsDataRequestParams {
|
interface TorrentsDataRequestParams {
|
||||||
appId: string;
|
appId: string;
|
||||||
|
refreshInterval: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGetTorrentData = (params: TorrentsDataRequestParams) =>
|
export const useGetTorrentData = (params: TorrentsDataRequestParams) =>
|
||||||
@@ -15,7 +16,7 @@ export const useGetTorrentData = (params: TorrentsDataRequestParams) =>
|
|||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
refetchInterval(_: any, query: Query) {
|
refetchInterval(_: any, query: Query) {
|
||||||
if (query.state.fetchFailureCount < 3) {
|
if (query.state.fetchFailureCount < 3) {
|
||||||
return 5000;
|
return params.refreshInterval;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TFunction } from 'react-i18next';
|
import { TFunction } from 'react-i18next';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { openContextModalGeneric } from '../../../../../tools/mantineModalManagerExtensions';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { AppType } from '../../../../../types/app';
|
import { tryMatchService } from '../../tools/addToHomarr';
|
||||||
|
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
||||||
|
import { AppType } from '../../types/app';
|
||||||
|
import { appTileDefinition } from '../../components/Dashboard/Tiles/Apps/AppTile';
|
||||||
|
|
||||||
let t: TFunction<'modules/docker', undefined>;
|
let t: TFunction<'modules/docker', undefined>;
|
||||||
|
|
||||||
@@ -68,6 +71,8 @@ export interface ContainerActionBarProps {
|
|||||||
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
|
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
|
||||||
t = useTranslation('modules/docker').t;
|
t = useTranslation('modules/docker').t;
|
||||||
const [isLoading, setisLoading] = useState(false);
|
const [isLoading, setisLoading] = useState(false);
|
||||||
|
const { name: configName, config } = useConfigContext();
|
||||||
|
const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
@@ -158,61 +163,40 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
radius="md"
|
radius="md"
|
||||||
disabled={selected.length === 0 || selected.length > 1}
|
disabled={selected.length === 0 || selected.length > 1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const app = tryMatchService(selected.at(0)!);
|
||||||
const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`;
|
const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`;
|
||||||
openContextModalGeneric<{ service: AppType }>({
|
openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({
|
||||||
modal: 'editService',
|
modal: 'editApp',
|
||||||
innerProps: {
|
innerProps: {
|
||||||
service: {
|
app: {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
name: selected[0].Names[0],
|
name: app.name ? app.name : selected[0].Names[0].substring(1),
|
||||||
url: containerUrl,
|
url: containerUrl,
|
||||||
appearance: {
|
appearance: {
|
||||||
iconUrl: '/imgs/logo/logo.png', // TODO: find icon automatically
|
iconUrl: app.icon ? app.icon : '/imgs/logo/logo.png',
|
||||||
},
|
},
|
||||||
network: {
|
network: {
|
||||||
enabledStatusChecker: false,
|
enabledStatusChecker: true,
|
||||||
okStatus: [],
|
okStatus: [200],
|
||||||
},
|
},
|
||||||
behaviour: {
|
behaviour: {
|
||||||
isOpeningNewTab: true,
|
isOpeningNewTab: true,
|
||||||
externalUrl: '',
|
externalUrl: '',
|
||||||
},
|
},
|
||||||
area: {
|
area: {
|
||||||
type: 'sidebar', // TODO: Set the wrapper automatically
|
type: 'wrapper',
|
||||||
properties: {
|
properties: {
|
||||||
location: 'right',
|
id: getLowestWrapper()?.id ?? 'default',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shape: {
|
shape: {
|
||||||
lg: {
|
|
||||||
location: {
|
location: {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
height: 1,
|
width: appTileDefinition.minWidth,
|
||||||
width: 1,
|
height: appTileDefinition.minHeight,
|
||||||
},
|
|
||||||
},
|
|
||||||
md: {
|
|
||||||
location: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
height: 1,
|
|
||||||
width: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sm: {
|
|
||||||
location: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
height: 1,
|
|
||||||
width: 1,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
@@ -220,7 +204,9 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
properties: [],
|
properties: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
allowAppNamePropagation: true,
|
||||||
},
|
},
|
||||||
|
size: 'xl',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import Docker from 'dockerode';
|
import Docker from 'dockerode';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useConfigContext } from '../../../../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
|
|
||||||
import ContainerActionBar from './ContainerActionBar';
|
import ContainerActionBar from './ContainerActionBar';
|
||||||
import DockerTable from './DockerTable';
|
import DockerTable from './DockerTable';
|
||||||
@@ -60,6 +60,7 @@ export default function DockerMenuButton(props: any) {
|
|||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
padding="xl"
|
padding="xl"
|
||||||
|
position="right"
|
||||||
size="full"
|
size="full"
|
||||||
title={<ContainerActionBar selected={selection} reload={reload} />}
|
title={<ContainerActionBar selected={selection} reload={reload} />}
|
||||||
>
|
>
|
||||||
@@ -118,7 +118,6 @@ export default function DockerTable({
|
|||||||
icon={<IconSearch size={14} />}
|
icon={<IconSearch size={14} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
disabled={usedContainers.length === 0}
|
|
||||||
/>
|
/>
|
||||||
<Table ref={ref} captionSide="bottom" highlightOnHover verticalSpacing="sm">
|
<Table ref={ref} captionSide="bottom" highlightOnHover verticalSpacing="sm">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -45,13 +45,21 @@ function Put(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
(previousProperty) => previousProperty.field === property.field
|
(previousProperty) => previousProperty.field === property.field
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (property.value !== undefined && property.value !== null) {
|
||||||
|
Consola.info(
|
||||||
|
'Detected credential change of private secret. Value will be overwritten in configuration'
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
field: property.field,
|
field: property.field,
|
||||||
type: property.type,
|
type: property.type,
|
||||||
value:
|
value: property.value,
|
||||||
property.value !== undefined || property.value === null
|
};
|
||||||
? property.value
|
}
|
||||||
: previousProperty?.value,
|
|
||||||
|
return {
|
||||||
|
field: property.field,
|
||||||
|
type: property.type,
|
||||||
|
value: previousProperty?.value,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getCookie } from 'cookies-next';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Consola from 'consola';
|
import Consola from 'consola';
|
||||||
import { getConfig } from '../../../../tools/config/getConfig';
|
import { getConfig } from '../../../../tools/config/getConfig';
|
||||||
import { MediaType } from '../../../../modules/overseerr/SearchResult';
|
import type { MediaType } from '../../../../modules/overseerr/SearchResult';
|
||||||
|
|
||||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// Get the slug of the request
|
// Get the slug of the request
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
} else if (error.code === 'ECONNABORTED') {
|
} else if (error.code === 'ECONNABORTED') {
|
||||||
res.status(408).json('Request Timeout');
|
res.status(408).json('Request Timeout');
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json('Server Error');
|
res.status(error.response ? error.response.status : 500).json('Server Error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// // Make a request to the URL
|
// // Make a request to the URL
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grid-stack > .grid-stack-item > .grid-stack-item-content {
|
.grid-stack > .grid-stack-item > .grid-stack-item-content {
|
||||||
overflow-y: hidden;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-stack.grid-stack-animate {
|
.grid-stack.grid-stack-animate {
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
export const StatusCodes = [
|
export const StatusCodes = [
|
||||||
{ value: '200', label: '200 - OK', group: 'Sucessful responses' },
|
{ value: 200, label: '200 - OK', group: 'Sucessful responses' },
|
||||||
{ value: '204', label: '204 - No Content', group: 'Sucessful responses' },
|
{ value: 204, label: '204 - No Content', group: 'Sucessful responses' },
|
||||||
{ value: '301', label: '301 - Moved Permanently', group: 'Redirection responses' },
|
{ value: 301, label: '301 - Moved Permanently', group: 'Redirection responses' },
|
||||||
{ value: '302', label: '302 - Found / Moved Temporarily', group: 'Redirection responses' },
|
{ value: 302, label: '302 - Found / Moved Temporarily', group: 'Redirection responses' },
|
||||||
{ value: '304', label: '304 - Not Modified', group: 'Redirection responses' },
|
{ value: 304, label: '304 - Not Modified', group: 'Redirection responses' },
|
||||||
{ value: '307', label: '307 - Temporary Redirect', group: 'Redirection responses' },
|
{ value: 307, label: '307 - Temporary Redirect', group: 'Redirection responses' },
|
||||||
{ value: '308', label: '308 - Permanent Redirect', group: 'Redirection responses' },
|
{ value: 308, label: '308 - Permanent Redirect', group: 'Redirection responses' },
|
||||||
{ value: '400', label: '400 - Bad Request', group: 'Client error responses' },
|
{ value: 400, label: '400 - Bad Request', group: 'Client error responses' },
|
||||||
{ value: '401', label: '401 - Unauthorized', group: 'Client error responses' },
|
{ value: 401, label: '401 - Unauthorized', group: 'Client error responses' },
|
||||||
{ value: '403', label: '403 - Forbidden', group: 'Client error responses' },
|
{ value: 403, label: '403 - Forbidden', group: 'Client error responses' },
|
||||||
{ value: '404', label: '404 - Not Found', group: 'Client error responses' },
|
{ value: 404, label: '404 - Not Found', group: 'Client error responses' },
|
||||||
{ value: '405', label: '405 - Method Not Allowed', group: 'Client error responses' },
|
{ value: 405, label: '405 - Method Not Allowed', group: 'Client error responses' },
|
||||||
{ value: '408', label: '408 - Request Timeout', group: 'Client error responses' },
|
{ value: 408, label: '408 - Request Timeout', group: 'Client error responses' },
|
||||||
{ value: '410', label: '410 - Gone', group: 'Client error responses' },
|
{ value: 410, label: '410 - Gone', group: 'Client error responses' },
|
||||||
{ value: '429', label: '429 - Too Many Requests', group: 'Client error responses' },
|
{ value: 429, label: '429 - Too Many Requests', group: 'Client error responses' },
|
||||||
{ value: '500', label: '500 - Internal Server Error', group: 'Server error responses' },
|
{ value: 500, label: '500 - Internal Server Error', group: 'Server error responses' },
|
||||||
{ value: '502', label: '502 - Bad Gateway', group: 'Server error responses' },
|
{ value: 502, label: '502 - Bad Gateway', group: 'Server error responses' },
|
||||||
{ value: '503', label: '503 - Service Unavailable', group: 'Server error responses' },
|
{ value: 503, label: '503 - Service Unavailable', group: 'Server error responses' },
|
||||||
{ value: '054', label: '504 - Gateway Timeout Error', group: 'Server error responses' },
|
{ value: 504, label: '504 - Gateway Timeout Error', group: 'Server error responses' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const dashboardNamespaces = [
|
|||||||
'layout/modals/change-position',
|
'layout/modals/change-position',
|
||||||
'layout/modals/about',
|
'layout/modals/about',
|
||||||
'layout/header/actions/toggle-edit-mode',
|
'layout/header/actions/toggle-edit-mode',
|
||||||
|
'layout/mobile/drawer',
|
||||||
'settings/common',
|
'settings/common',
|
||||||
'settings/general/theme-selector',
|
'settings/general/theme-selector',
|
||||||
'settings/general/config-changer',
|
'settings/general/config-changer',
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export const portmap = [
|
|||||||
{ name: 'nzbget', value: '6789' },
|
{ name: 'nzbget', value: '6789' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
//TODO: Fix this to be used in the docker add to homarr button
|
||||||
export const MatchingImages: {
|
export const MatchingImages: {
|
||||||
image: string;
|
image: string;
|
||||||
type: ServiceType;
|
type: ServiceType;
|
||||||
|
|||||||
@@ -52,12 +52,14 @@ export type ConfigAppIntegrationType = Omit<AppIntegrationType, 'properties'> &
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AppIntegrationPropertyType = {
|
export type AppIntegrationPropertyType = {
|
||||||
type: 'private' | 'public';
|
type: AppIntegrationPropertyAccessabilityType;
|
||||||
field: IntegrationField;
|
field: IntegrationField;
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
isDefined: boolean;
|
isDefined: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AppIntegrationPropertyAccessabilityType = 'private' | 'public';
|
||||||
|
|
||||||
type ConfigAppIntegrationPropertyType = Omit<AppIntegrationPropertyType, 'isDefined'>;
|
type ConfigAppIntegrationPropertyType = Omit<AppIntegrationPropertyType, 'isDefined'>;
|
||||||
|
|
||||||
export type IntegrationField = 'apiKey' | 'password' | 'username';
|
export type IntegrationField = 'apiKey' | 'password' | 'username';
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const BitTorrrentQueueItem = ({ torrent }: BitTorrentQueueItemProps) => {
|
|||||||
return (
|
return (
|
||||||
<tr key={torrent.id}>
|
<tr key={torrent.id}>
|
||||||
<td>
|
<td>
|
||||||
<Tooltip position="top" label={torrent.name}>
|
<Tooltip position="top" withinPortal label={torrent.name}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '30vw',
|
maxWidth: '30vw',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NormalizedTorrent, TorrentState } from '@ctrl/shared-torrent';
|
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
||||||
import {
|
import {
|
||||||
Center,
|
Center,
|
||||||
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
@@ -12,6 +13,9 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useElementSize } from '@mantine/hooks';
|
import { useElementSize } from '@mantine/hooks';
|
||||||
import { IconFileDownload } from '@tabler/icons';
|
import { IconFileDownload } from '@tabler/icons';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import duration from 'dayjs/plugin/duration';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
@@ -21,6 +25,9 @@ import { defineWidget } from '../helper';
|
|||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import { BitTorrrentQueueItem } from './BitTorrentQueueItem';
|
import { BitTorrrentQueueItem } from './BitTorrentQueueItem';
|
||||||
|
|
||||||
|
dayjs.extend(duration);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const downloadAppTypes: AppIntegrationType['type'][] = ['deluge', 'qBittorrent', 'transmission'];
|
const downloadAppTypes: AppIntegrationType['type'][] = ['deluge', 'qBittorrent', 'transmission'];
|
||||||
|
|
||||||
const definition = defineWidget({
|
const definition = defineWidget({
|
||||||
@@ -35,6 +42,13 @@ const definition = defineWidget({
|
|||||||
type: 'switch',
|
type: 'switch',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
|
refreshInterval: {
|
||||||
|
type: 'slider',
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 60,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
gridstack: {
|
gridstack: {
|
||||||
minWidth: 4,
|
minWidth: 4,
|
||||||
@@ -62,7 +76,10 @@ function BitTorrentTile({ widget }: BitTorrentTileProps) {
|
|||||||
[];
|
[];
|
||||||
|
|
||||||
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
|
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
|
||||||
const { data, isFetching, isError } = useGetTorrentData({ appId: selectedAppId! });
|
const { data, isError, isInitialLoading, dataUpdatedAt } = useGetTorrentData({
|
||||||
|
appId: selectedAppId!,
|
||||||
|
refreshInterval: widget.properties.refreshInterval * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedAppId && downloadApps.length) {
|
if (!selectedAppId && downloadApps.length) {
|
||||||
@@ -92,9 +109,15 @@ function BitTorrentTile({ widget }: BitTorrentTileProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFetching) {
|
if (isInitialLoading) {
|
||||||
return (
|
return (
|
||||||
<Stack align="center">
|
<Stack
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Loader />
|
<Loader />
|
||||||
<Stack align="center" spacing={0}>
|
<Stack align="center" spacing={0}>
|
||||||
<Text>{t('card.loading.title')}</Text>
|
<Text>{t('card.loading.title')}</Text>
|
||||||
@@ -124,8 +147,13 @@ function BitTorrentTile({ widget }: BitTorrentTileProps) {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const difference = new Date().getTime() - dataUpdatedAt;
|
||||||
|
const duration = dayjs.duration(difference, 'ms');
|
||||||
|
const humanizedDuration = duration.humanize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea sx={{ height: 300, width: '100%' }}>
|
<Flex direction="column" sx={{ height: '100%' }}>
|
||||||
|
<ScrollArea sx={{ height: '100%', width: '100%' }}>
|
||||||
<Table highlightOnHover p="sm">
|
<Table highlightOnHover p="sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -144,6 +172,10 @@ function BitTorrentTile({ widget }: BitTorrentTileProps) {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
<Text color="dimmed" size="xs">
|
||||||
|
Last updated {humanizedDuration} ago
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ function DashDotTile({ widget }: DashDotTileProps) {
|
|||||||
const { classes } = useDashDotTileStyles();
|
const { classes } = useDashDotTileStyles();
|
||||||
const { t } = useTranslation('modules/dashdot');
|
const { t } = useTranslation('modules/dashdot');
|
||||||
|
|
||||||
const dashDotUrl = widget?.properties.url;
|
const dashDotUrl = widget.properties.url;
|
||||||
|
|
||||||
const { data: info } = useDashDotInfo({ dashDotUrl });
|
const { data: info } = useDashDotInfo({
|
||||||
|
dashDotUrl,
|
||||||
|
});
|
||||||
|
|
||||||
const graphs = widget?.properties.graphs.map((g) => ({
|
const graphs = widget?.properties.graphs.map((g) => ({
|
||||||
id: g,
|
id: g,
|
||||||
@@ -112,6 +114,7 @@ function DashDotTile({ widget }: DashDotTileProps) {
|
|||||||
const useDashDotInfo = ({ dashDotUrl }: { dashDotUrl: string }) => {
|
const useDashDotInfo = ({ dashDotUrl }: { dashDotUrl: string }) => {
|
||||||
const { name: configName } = useConfigContext();
|
const { name: configName } = useConfigContext();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
refetchInterval: 50000,
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'dashdot/info',
|
'dashdot/info',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Center, Stack, Text, Title } from '@mantine/core';
|
import { Stack, Text, Title } from '@mantine/core';
|
||||||
import { IconClock } from '@tabler/icons';
|
import { IconClock } from '@tabler/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|||||||
@@ -5,6 +5,4 @@ import { IWidgetDefinition } from './widgets';
|
|||||||
// The options of IWidgetDefinition are so heavily typed that it even used 'true' as type
|
// The options of IWidgetDefinition are so heavily typed that it even used 'true' as type
|
||||||
export const defineWidget = <TKey extends string, TOptions extends IWidgetDefinition<TKey>>(
|
export const defineWidget = <TKey extends string, TOptions extends IWidgetDefinition<TKey>>(
|
||||||
options: TOptions
|
options: TOptions
|
||||||
) => {
|
) => options;
|
||||||
return options;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -46,16 +46,20 @@ const definition = defineWidget({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type IWeatherWidget = IWidget<typeof definition['id'], typeof definition>;
|
export type IUsenetWidget = IWidget<typeof definition['id'], typeof definition>;
|
||||||
|
|
||||||
interface UseNetTileProps {}
|
interface UseNetTileProps {
|
||||||
|
widget: IUsenetWidget;
|
||||||
|
}
|
||||||
|
|
||||||
function UseNetTile({}: UseNetTileProps) {
|
function UseNetTile({ widget }: UseNetTileProps) {
|
||||||
const { t } = useTranslation('modules/usenet');
|
const { t } = useTranslation('modules/usenet');
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
const downloadApps =
|
const downloadApps =
|
||||||
config?.apps.filter((x) => x.integration && downloadAppTypes.includes(x.integration.type)) ??
|
config?.apps.filter((x) => x.integration && downloadAppTypes.includes(x.integration.type)) ??
|
||||||
[];
|
[];
|
||||||
|
const { ref, width, height } = useElementSize();
|
||||||
|
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
||||||
|
|
||||||
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
|
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
|
||||||
const { data } = useGetUsenetInfo({ appId: selectedAppId! });
|
const { data } = useGetUsenetInfo({ appId: selectedAppId! });
|
||||||
@@ -84,9 +88,6 @@ function UseNetTile({}: UseNetTileProps) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ref, width, height } = useElementSize();
|
|
||||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs keepMounted={false} defaultValue="queue">
|
<Tabs keepMounted={false} defaultValue="queue">
|
||||||
<Tabs.List ref={ref} mb="md" style={{ flex: 1 }} grow>
|
<Tabs.List ref={ref} mb="md" style={{ flex: 1 }} grow>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
Code,
|
Code,
|
||||||
Group,
|
Group,
|
||||||
Pagination,
|
Pagination,
|
||||||
ScrollArea,
|
|
||||||
Skeleton,
|
Skeleton,
|
||||||
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
@@ -28,7 +28,7 @@ interface UsenetHistoryListProps {
|
|||||||
appId: string;
|
appId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 13;
|
||||||
|
|
||||||
export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ appId }) => {
|
export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ appId }) => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
@@ -39,7 +39,7 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ a
|
|||||||
const { data, isLoading, isError, error } = useGetUsenetHistory({
|
const { data, isLoading, isError, error } = useGetUsenetHistory({
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
offset: (page - 1) * PAGE_SIZE,
|
offset: (page - 1) * PAGE_SIZE,
|
||||||
appId: appId,
|
appId,
|
||||||
});
|
});
|
||||||
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);
|
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);
|
||||||
|
|
||||||
@@ -81,8 +81,7 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ a
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Stack justify="space-around" spacing="xs">
|
||||||
<ScrollArea style={{ flex: 1 }}>
|
|
||||||
<Table highlightOnHover style={{ tableLayout: 'fixed' }} ref={ref}>
|
<Table highlightOnHover style={{ tableLayout: 'fixed' }} ref={ref}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -122,9 +121,9 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ a
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</ScrollArea>
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
|
noWrap
|
||||||
size="sm"
|
size="sm"
|
||||||
position="center"
|
position="center"
|
||||||
mt="md"
|
mt="md"
|
||||||
@@ -133,6 +132,6 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ a
|
|||||||
onChange={setPage}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Alert,
|
Alert,
|
||||||
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Code,
|
Code,
|
||||||
Group,
|
Group,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
Progress,
|
Progress,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
@@ -30,7 +32,7 @@ interface UsenetQueueListProps {
|
|||||||
appId: string;
|
appId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 13;
|
||||||
|
|
||||||
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId }) => {
|
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId }) => {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
@@ -38,13 +40,13 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
|
|||||||
const progressbarBreakpoint = theme.breakpoints.xs;
|
const progressbarBreakpoint = theme.breakpoints.xs;
|
||||||
const progressBreakpoint = 400;
|
const progressBreakpoint = 400;
|
||||||
const sizeBreakpoint = 300;
|
const sizeBreakpoint = 300;
|
||||||
const { ref, width, height } = useElementSize();
|
const { ref, width } = useElementSize();
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const { data, isLoading, isError, error } = useGetUsenetDownloads({
|
const { data, isLoading, isError, error } = useGetUsenetDownloads({
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
offset: (page - 1) * PAGE_SIZE,
|
offset: (page - 1) * PAGE_SIZE,
|
||||||
appId: appId,
|
appId,
|
||||||
});
|
});
|
||||||
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);
|
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);
|
||||||
|
|
||||||
@@ -85,9 +87,9 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Set ScollArea dynamic height based on the widget size
|
||||||
return (
|
return (
|
||||||
<>
|
<Stack justify="space-around" spacing="xs">
|
||||||
<ScrollArea style={{ flex: 1 }}>
|
|
||||||
<Table highlightOnHover style={{ tableLayout: 'fixed' }} ref={ref}>
|
<Table highlightOnHover style={{ tableLayout: 'fixed' }} ref={ref}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -171,17 +173,16 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</ScrollArea>
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
|
noWrap
|
||||||
size="sm"
|
size="sm"
|
||||||
position="center"
|
position="center"
|
||||||
mt="md"
|
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
page={page}
|
page={page}
|
||||||
onChange={setPage}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
15
src/widgets/widgets.d.ts
vendored
15
src/widgets/widgets.d.ts
vendored
@@ -1,6 +1,5 @@
|
|||||||
import { IconSun, TablerIcon } from '@tabler/icons';
|
import { TablerIcon } from '@tabler/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BaseTileProps } from '../components/Dashboard/Tiles/type';
|
|
||||||
|
|
||||||
// Type of widgets which are safed to config
|
// Type of widgets which are safed to config
|
||||||
export type IWidget<TKey extends string, TDefinition extends IWidgetDefinition> = {
|
export type IWidget<TKey extends string, TDefinition extends IWidgetDefinition> = {
|
||||||
@@ -32,6 +31,7 @@ export type IWidgetOptionValue =
|
|||||||
| IMultiSelectOptionValue
|
| IMultiSelectOptionValue
|
||||||
| ISwitchOptionValue
|
| ISwitchOptionValue
|
||||||
| ITextInputOptionValue
|
| ITextInputOptionValue
|
||||||
|
| ISliderInputOptionValue
|
||||||
| INumberInputOptionValue;
|
| INumberInputOptionValue;
|
||||||
|
|
||||||
// will show a multi-select with specified data
|
// will show a multi-select with specified data
|
||||||
@@ -56,7 +56,16 @@ export type ITextInputOptionValue = {
|
|||||||
// will show a number-input
|
// will show a number-input
|
||||||
export type INumberInputOptionValue = {
|
export type INumberInputOptionValue = {
|
||||||
type: 'number';
|
type: 'number';
|
||||||
defaultValue: string;
|
defaultValue: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// will show a slider-input
|
||||||
|
export type ISliderInputOptionValue = {
|
||||||
|
type: 'slider';
|
||||||
|
defaultValue: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// is used to type the widget definitions which will be used to display all widgets
|
// is used to type the widget definitions which will be used to display all widgets
|
||||||
|
|||||||
Reference in New Issue
Block a user