mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
Compare commits
271 Commits
v4.1.3
...
refactor/w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
105dac5a17 | ||
|
|
694c3077b1 | ||
|
|
498583207c | ||
|
|
a9f76a2deb | ||
|
|
a91bef59c2 | ||
|
|
25b501675f | ||
|
|
cc44fbfbe3 | ||
|
|
07136ad235 | ||
|
|
aea90134ca | ||
|
|
e2bb6348eb | ||
|
|
7c925dc7f6 | ||
|
|
99eedc793d | ||
|
|
3940a43b4f | ||
|
|
aa247694db | ||
|
|
56ba2f1b0b | ||
|
|
4674d48ffb | ||
|
|
88fbb52606 | ||
|
|
18f80390d5 | ||
|
|
51f5032db2 | ||
|
|
12bb6595b4 | ||
|
|
96d8c80ca5 | ||
|
|
6edefb0365 | ||
|
|
2dd89fad4c | ||
|
|
7e75272991 | ||
|
|
2b464c2d1d | ||
|
|
9dba19e7f6 | ||
|
|
c29244e5f0 | ||
|
|
2bdc718e0a | ||
|
|
2fbd2f1304 | ||
|
|
5c58bde027 | ||
|
|
33d2e4ba63 | ||
|
|
23fc148755 | ||
|
|
ff2a5e5b2f | ||
|
|
91b234918d | ||
|
|
d5c3fb25bf | ||
|
|
7b64e5e08b | ||
|
|
fefeb59103 | ||
|
|
ec42ff8674 | ||
|
|
5367272a6b | ||
|
|
9f67c49bdd | ||
|
|
1e4f449bb1 | ||
|
|
3f6305d35a | ||
|
|
ff30595d25 | ||
|
|
31c8bc68ae | ||
|
|
4ec1d58fa8 | ||
|
|
ea2de14d18 | ||
|
|
828f50a4cd | ||
|
|
0e00c5baae | ||
|
|
e2fd05edd4 | ||
|
|
d21ca3e68d | ||
|
|
29ca3b1baa | ||
|
|
18624d080d | ||
|
|
780dce53f9 | ||
|
|
3658eb0a0c | ||
|
|
06d6fe357a | ||
|
|
9390ded8b6 | ||
|
|
9ec014ae54 | ||
|
|
600d6f3655 | ||
|
|
acadc5417d | ||
|
|
43210e7b9b | ||
|
|
0e2d535f96 | ||
|
|
2e39a5bceb | ||
|
|
4284dff515 | ||
|
|
8f9e664534 | ||
|
|
28e408c989 | ||
|
|
72edac039e | ||
|
|
0df33d4237 | ||
|
|
6722d4f50c | ||
|
|
ea97456020 | ||
|
|
f409e33adb | ||
|
|
31f29229da | ||
|
|
e2d5dbb155 | ||
|
|
daf5933a84 | ||
|
|
ada1fe615e | ||
|
|
c89696f3d5 | ||
|
|
ee7e49e929 | ||
|
|
5edd1c7bf2 | ||
|
|
46077f2641 | ||
|
|
bb7a4ccdb1 | ||
|
|
47168ee946 | ||
|
|
6004d727de | ||
|
|
d430b1566d | ||
|
|
e1e7472a39 | ||
|
|
3598823810 | ||
|
|
64024bc54d | ||
|
|
dde1af071c | ||
|
|
f5498c8b52 | ||
|
|
376ede6458 | ||
|
|
5c9c05c5e8 | ||
|
|
b76d2e4bbc | ||
|
|
17f5f0a936 | ||
|
|
33763b5809 | ||
|
|
1e0cb2ca68 | ||
|
|
2191cd8e9e | ||
|
|
9c7229e923 | ||
|
|
15efdeb554 | ||
|
|
0417e43c2d | ||
|
|
472e3fdba2 | ||
|
|
775b7fcd1e | ||
|
|
8cc86525e2 | ||
|
|
cdaadaf700 | ||
|
|
3b4c4ed552 | ||
|
|
c68d5e17f0 | ||
|
|
7f9412a758 | ||
|
|
a1b7ee0800 | ||
|
|
b5fb0860eb | ||
|
|
71768e05b7 | ||
|
|
5fc4d939bb | ||
|
|
b6324e41a3 | ||
|
|
04c17da21e | ||
|
|
312178129e | ||
|
|
efb838c87c | ||
|
|
3f32b532f4 | ||
|
|
410cb5b9ee | ||
|
|
706c71df10 | ||
|
|
a775026bf5 | ||
|
|
43b5293d97 | ||
|
|
f4f1b1b32c | ||
|
|
33fa5075e7 | ||
|
|
839664d1f5 | ||
|
|
330b86f227 | ||
|
|
d5b0efd1e4 | ||
|
|
58735fd807 | ||
|
|
de9b76a6a6 | ||
|
|
77ede914a2 | ||
|
|
eb2342f718 | ||
|
|
ccd776b319 | ||
|
|
2b73977a81 | ||
|
|
7b61f1ee54 | ||
|
|
00a50d317d | ||
|
|
115c60c44e | ||
|
|
57adbee68e | ||
|
|
6e58578de8 | ||
|
|
04655e1854 | ||
|
|
d5811e72e1 | ||
|
|
fa83b23875 | ||
|
|
6efb003056 | ||
|
|
8b1b93cf64 | ||
|
|
747bacb901 | ||
|
|
a39f879b3a | ||
|
|
dc4a05916e | ||
|
|
6b2d75dd9e | ||
|
|
75c583f012 | ||
|
|
3d0dbb1695 | ||
|
|
4f1e353d06 | ||
|
|
874a375779 | ||
|
|
3cd611ff38 | ||
|
|
7d3ed1d535 | ||
|
|
564456d91f | ||
|
|
4079c81128 | ||
|
|
079c54ece4 | ||
|
|
a740d029b0 | ||
|
|
e8de3c95b1 | ||
|
|
5446d28405 | ||
|
|
0afb6333ac | ||
|
|
81cb61c785 | ||
|
|
0cc53be948 | ||
|
|
45b3d07d61 | ||
|
|
c894f2bae5 | ||
|
|
99ed6d67ac | ||
|
|
6009d91d8e | ||
|
|
4e452b486e | ||
|
|
5b819fa409 | ||
|
|
9f2c857646 | ||
|
|
7b4afb86ba | ||
|
|
1180430d23 | ||
|
|
a952cd14ca | ||
|
|
6f6342a60b | ||
|
|
2b8ec1f661 | ||
|
|
3e8b617677 | ||
|
|
e7da6d4bbb | ||
|
|
7d04507f57 | ||
|
|
23e0423093 | ||
|
|
ecfe0ec2f9 | ||
|
|
10a573fb4d | ||
|
|
069924b2d2 | ||
|
|
8c7bf0e190 | ||
|
|
1a239b6914 | ||
|
|
0638120af8 | ||
|
|
59cda3a865 | ||
|
|
2042d8962b | ||
|
|
a9c859f022 | ||
|
|
f4b4271c91 | ||
|
|
4263749486 | ||
|
|
05dd10a38e | ||
|
|
cea10fceb2 | ||
|
|
122cd03427 | ||
|
|
274ca97d59 | ||
|
|
c1afb728df | ||
|
|
fec3390880 | ||
|
|
7d316fc1db | ||
|
|
8db52be416 | ||
|
|
379fe69813 | ||
|
|
0e86c3c071 | ||
|
|
9e2f6a8607 | ||
|
|
57bd93b3a2 | ||
|
|
67ae4dab05 | ||
|
|
2a2a16d2f4 | ||
|
|
52bbcbc984 | ||
|
|
d7829aadd1 | ||
|
|
b7be649326 | ||
|
|
7aa6e606f5 | ||
|
|
e1f1e3e72e | ||
|
|
1d53fedf11 | ||
|
|
1daf2e8b1f | ||
|
|
1e5904b92a | ||
|
|
27aed8186b | ||
|
|
834691f12b | ||
|
|
38ddd972ce | ||
|
|
9baca3a2a3 | ||
|
|
44ccb7abf0 | ||
|
|
314160a9ea | ||
|
|
ca2abadcd2 | ||
|
|
dbf8d14810 | ||
|
|
2e9145372e | ||
|
|
a633e2ff82 | ||
|
|
2d7e00bc3a | ||
|
|
fc5b6a03c6 | ||
|
|
f1130576a0 | ||
|
|
0a13756fa3 | ||
|
|
471721eaa9 | ||
|
|
0b81add407 | ||
|
|
42f94550ae | ||
|
|
107f4d1cf4 | ||
|
|
95acaa25dd | ||
|
|
262d3d1edd | ||
|
|
f07342f8d2 | ||
|
|
2c7e70b21e | ||
|
|
eac33e1a4e | ||
|
|
df7c5fc950 | ||
|
|
027d4b37f2 | ||
|
|
e6745b0ddb | ||
|
|
481e0a6e41 | ||
|
|
15b0277191 | ||
|
|
7cb3abeaeb | ||
|
|
00f5c8072e | ||
|
|
16a3e7faf0 | ||
|
|
8c279aef07 | ||
|
|
ae1013ca40 | ||
|
|
9404bdb580 | ||
|
|
4b27d54302 | ||
|
|
dc659a0c40 | ||
|
|
bf5b104e28 | ||
|
|
7d95552f33 | ||
|
|
6c4d5c63a0 | ||
|
|
fb1c9e074d | ||
|
|
d28b7aeac1 | ||
|
|
d8f165e234 | ||
|
|
a864c49f01 | ||
|
|
2217abbe26 | ||
|
|
ec56aa0f6c | ||
|
|
4912ceaca0 | ||
|
|
d46b9e5ec8 | ||
|
|
3303549565 | ||
|
|
3c69b1f0ad | ||
|
|
17893d78f9 | ||
|
|
23130f901a | ||
|
|
96d62618d6 | ||
|
|
e0a5978de7 | ||
|
|
d9788bbb22 | ||
|
|
ccbf9469e4 | ||
|
|
55f4a13ad4 | ||
|
|
bb4df5cf9b | ||
|
|
a27117a45d | ||
|
|
6498598553 | ||
|
|
551210d458 | ||
|
|
7239bc73da | ||
|
|
264fc11479 | ||
|
|
b6c95b2863 | ||
|
|
920c992834 | ||
|
|
b36828ca3b |
74
.github/workflows/lint-test-build-web.yml
vendored
Normal file
74
.github/workflows/lint-test-build-web.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Lint, Test, and Build Web Components
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint-web:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
touch .env
|
||||
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
|
||||
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
|
||||
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
|
||||
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
node-version-file: "web/.nvmrc"
|
||||
|
||||
- name: Installing node deps
|
||||
run: npm install
|
||||
|
||||
- name: Lint files
|
||||
run: npm run lint
|
||||
|
||||
build-web:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-web]
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
touch .env
|
||||
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
|
||||
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
|
||||
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
|
||||
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
node-version-file: "web/.nvmrc"
|
||||
|
||||
- name: Installing node deps
|
||||
run: npm install
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build to Github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: unraid-web
|
||||
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
|
||||
74
.github/workflows/main.yml
vendored
74
.github/workflows/main.yml
vendored
@@ -86,6 +86,37 @@ jobs:
|
||||
- name: Run Docker Compose
|
||||
run: docker-compose run builder npm run coverage
|
||||
|
||||
lint-web:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
touch .env
|
||||
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
|
||||
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
|
||||
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
|
||||
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
node-version-file: "web/.nvmrc"
|
||||
|
||||
- name: Installing node deps
|
||||
run: npm install
|
||||
|
||||
- name: Lint files
|
||||
run: npm run lint
|
||||
|
||||
build-api:
|
||||
defaults:
|
||||
run:
|
||||
@@ -144,6 +175,44 @@ jobs:
|
||||
name: unraid-api
|
||||
path: ${{ github.workspace }}/api/deploy/release/*.tgz
|
||||
|
||||
build-web:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-web]
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
touch .env
|
||||
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
|
||||
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
|
||||
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
|
||||
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
node-version-file: "web/.nvmrc"
|
||||
|
||||
- name: Installing node deps
|
||||
run: npm install
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build to Github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: unraid-web
|
||||
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
|
||||
|
||||
build-plugin:
|
||||
needs: [lint-api, test-api, build-api]
|
||||
defaults:
|
||||
@@ -157,6 +226,11 @@ jobs:
|
||||
timezoneLinux: "America/Los_Angeles"
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
- name: Download unraid web components
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: unraid-web
|
||||
path: ./plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
|
||||
- name: Build Plugin
|
||||
run: |
|
||||
cd source/dynamix.unraid.net
|
||||
|
||||
80
.github/workflows/pull-request-web.yml
vendored
Normal file
80
.github/workflows/pull-request-web.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Pull Request Web
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'web/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-web
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint-web:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
touch .env
|
||||
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
|
||||
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
|
||||
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
|
||||
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
node-version-file: "web/.nvmrc"
|
||||
|
||||
- name: Installing node deps
|
||||
run: npm install
|
||||
|
||||
- name: Lint files
|
||||
run: npm run lint
|
||||
|
||||
build-web:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-web]
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
touch .env
|
||||
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
|
||||
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
|
||||
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
|
||||
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
node-version-file: "web/.nvmrc"
|
||||
|
||||
- name: Installing node deps
|
||||
run: npm install
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build to Github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: unraid-web
|
||||
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
|
||||
87
.gitignore
vendored
Normal file
87
.gitignore
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
# Logs
|
||||
./logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
coverage-ts
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# Visual Studio Code workspace
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Temp dir for tests
|
||||
test/__temp__/*
|
||||
|
||||
# Built files
|
||||
dist
|
||||
|
||||
# Typescript
|
||||
typescript
|
||||
|
||||
# Ultra runner
|
||||
.ultra.cache.json
|
||||
|
||||
# Github actions
|
||||
RELEASE_NOTES.md
|
||||
|
||||
# Docker Deploy Folder
|
||||
deploy/*
|
||||
!deploy/.gitkeep
|
||||
|
||||
# pkg cache
|
||||
.pkg-cache
|
||||
|
||||
*.log*
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
.output
|
||||
.env*
|
||||
!.env.example
|
||||
30
.vscode/settings.json
vendored
Normal file
30
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": false,
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.activeBackground": "#78797d",
|
||||
"activityBar.background": "#78797d",
|
||||
"activityBar.foreground": "#e7e7e7",
|
||||
"activityBar.inactiveForeground": "#e7e7e799",
|
||||
"activityBarBadge.background": "#df9fac",
|
||||
"activityBarBadge.foreground": "#15202b",
|
||||
"commandCenter.border": "#e7e7e799",
|
||||
"sash.hoverBorder": "#78797d",
|
||||
"statusBar.background": "#5f6063",
|
||||
"statusBar.foreground": "#e7e7e7",
|
||||
"statusBarItem.hoverBackground": "#78797d",
|
||||
"statusBarItem.remoteBackground": "#5f6063",
|
||||
"statusBarItem.remoteForeground": "#e7e7e7",
|
||||
"titleBar.activeBackground": "#5f6063",
|
||||
"titleBar.activeForeground": "#e7e7e7",
|
||||
"titleBar.inactiveBackground": "#5f606399",
|
||||
"titleBar.inactiveForeground": "#e7e7e799"
|
||||
},
|
||||
"peacock.color": "#5f6063",
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales"
|
||||
],
|
||||
"i18n-ally.keystyle": "flat"
|
||||
}
|
||||
54
plugin/scripts/deploy-dev.sh
Executable file
54
plugin/scripts/deploy-dev.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Path to store the last used server name
|
||||
state_file="$HOME/.deploy_state"
|
||||
|
||||
# Read the last used server name from the state file
|
||||
if [[ -f "$state_file" ]]; then
|
||||
last_server_name=$(cat "$state_file")
|
||||
else
|
||||
last_server_name=""
|
||||
fi
|
||||
|
||||
# Read the server name from the command-line argument or use the last used server name as the default
|
||||
server_name="${1:-$last_server_name}"
|
||||
|
||||
# Check if the server name is provided
|
||||
if [[ -z "$server_name" ]]; then
|
||||
echo "Please provide the SSH server name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save the current server name to the state file
|
||||
echo "$server_name" > "$state_file"
|
||||
|
||||
# Source directory path
|
||||
source_directory="plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers"
|
||||
|
||||
# Destination directory path
|
||||
destination_directory="/usr/local/emhttp/plugins/dynamix.my.servers"
|
||||
|
||||
# Replace the value inside the rsync command with the user's input
|
||||
rsync_command="rsync -avz --progress --stats -m -e ssh \"$source_directory/\" \"root@${server_name}.local:$destination_directory/\""
|
||||
|
||||
echo "Executing the following command:"
|
||||
echo "$rsync_command"
|
||||
|
||||
# Execute the rsync command and capture the exit code
|
||||
eval "$rsync_command"
|
||||
exit_code=$?
|
||||
|
||||
# Play built-in sound based on the operating system
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
afplay /System/Library/Sounds/Submarine.aiff
|
||||
elif [[ "$OSTYPE" == "linux-gnu" ]]; then
|
||||
# Linux
|
||||
paplay /usr/share/sounds/freedesktop/stereo/complete.oga
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
||||
# Windows
|
||||
powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()"
|
||||
fi
|
||||
|
||||
# Exit with the rsync command's exit code
|
||||
exit $exit_code
|
||||
@@ -457,8 +457,6 @@ const isCommaSeparatedURLs = input =>
|
||||
</script>
|
||||
|
||||
<form id="UnraidNetSettings" markdown="1" name="UnraidNetSettings" method="POST" action="/update.htm" target="progressFrame">
|
||||
<div markdown="1" class="<?=$shade?>"><!-- begin Account section -->
|
||||
|
||||
<?
|
||||
/**
|
||||
* Allowed origins warning displayed when the current webGUI URL is NOT included in the known lists of allowed origins.
|
||||
@@ -480,6 +478,7 @@ if (stripos($allowedOrigins.",", "/".$host.",") === false) {
|
||||
}
|
||||
?>
|
||||
<? if ($allowedOriginsArr): ?>
|
||||
<div markdown="1" class="<?=$shade?>"><!-- begin allowedOrigins warning -->
|
||||
<dl>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<span class="orange-text"><i class='fa fa-warning fa-fw'></i> <strong>_(Warning)_</strong></span> <?= sprintf(_('Your current url **%s** is not in the list of allowed origins for this server'), $host) ?>.
|
||||
@@ -493,25 +492,8 @@ if (stripos($allowedOrigins.",", "/".$host.",") === false) {
|
||||
<dd>
|
||||
</div>
|
||||
</dl>
|
||||
<? endif ?>
|
||||
|
||||
|
||||
: <span>_(Questions? See <a href="https://docs.unraid.net/category/unraid-connect" target="_blank">the documentation</a>.)_</span>
|
||||
|
||||
|
||||
_(Account status)_:
|
||||
: <unraid-authed prop-registered="<? echo $isRegistered ?>"></unraid-authed>
|
||||
|
||||
<?if($isRegistered):?>
|
||||
_(Connected to Unraid Connect Cloud)_:
|
||||
<?if($isConnected):?>
|
||||
: _(Yes)_
|
||||
<?else:?>
|
||||
: <i class="fa fa-warning icon warning"></i> _(No)_
|
||||
<?endif // end check for ($isConnected) ?>
|
||||
<?endif // end check for ($isRegistered) ?>
|
||||
|
||||
</div><!-- end Account section -->
|
||||
<? endif ?>
|
||||
|
||||
<div markdown="1" class="<?=$shade?>"><!-- begin Remote Access section -->
|
||||
|
||||
@@ -544,7 +526,7 @@ _(Allow Remote Access)_:
|
||||
<?endif?>
|
||||
|
||||
|
||||
: <unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></unraid-wan-ip-check>
|
||||
: <unraid-i18n-host><unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></unraid-wan-ip-check></unraid-i18n-host>
|
||||
|
||||
<div markdown="1" id="wanpanel" style="display:'none'">
|
||||
|
||||
@@ -705,8 +687,26 @@ $(function() {
|
||||
|
||||
<!-- start unraid-api section -->
|
||||
<div markdown="1" class="js-unraidApiLogs <?=$shade?>">
|
||||
|
||||
: <span>_(Questions? See <a href="https://docs.unraid.net/category/unraid-connect" target="_blank">the documentation</a>.)_</span>
|
||||
|
||||
_(Account status)_:
|
||||
: <unraid-i18n-host><unraid-auth></unraid-auth></unraid-i18n-host>
|
||||
|
||||
<?if($isRegistered):?>
|
||||
_(Connected to Unraid Connect Cloud)_:
|
||||
<?if($isConnected):?>
|
||||
: _(Yes)_
|
||||
<?else:?>
|
||||
: <i class="fa fa-warning icon warning"></i> _(No)_
|
||||
<?endif // end check for ($isConnected) ?>
|
||||
<?endif // end check for ($isRegistered) ?>
|
||||
|
||||
_(Download unraid-api Logs)_:
|
||||
: <unraid-api-logs></unraid-api-logs>
|
||||
: <unraid-i18n-host><unraid-download-api-logs></unraid-download-api-logs></unraid-i18n-host>
|
||||
</div>
|
||||
<!-- end unraid-api section -->
|
||||
|
||||
<? require_once("$docroot/plugins/dynamix.my.servers/include/translations.php"); ?>
|
||||
$webComponentTranslations:
|
||||
: <pre><code><?= var_dump($webComponentTranslations); ?></code></pre>
|
||||
@@ -0,0 +1,262 @@
|
||||
Menu="About"
|
||||
Type="xmenu"
|
||||
Title="Registration"
|
||||
Icon="icon-registration"
|
||||
Tag="pencil"
|
||||
---
|
||||
<?PHP
|
||||
/* Copyright 2005-2023, Lime Technology
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License version 2,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*/
|
||||
?>
|
||||
<?
|
||||
function my_time_any($time) {
|
||||
return $time ? _(my_time($time),0) : _('Anytime');
|
||||
}
|
||||
function my_time_now($time) {
|
||||
return $time ? _(my_time($time),0) : _('Unknown');
|
||||
}
|
||||
|
||||
$regGen = (int)$var['regGen'] ?? 0;
|
||||
$trialExtensionEligible = $regGen === 0 || $regGen === 1;
|
||||
?>
|
||||
<style>
|
||||
span.thanks{padding-left:12px;color:#6FA239;font-weight:bold;}
|
||||
span.thanks.red{color:#F0000C;}
|
||||
div.device{padding:0 12px;font-weight:normal;font-style:italic;}
|
||||
div.remark{padding:0 12px;text-align:justify;}
|
||||
</style>
|
||||
|
||||
<?if ( (strstr($var['regTy'], "unregistered")) or ($var['regTy']=="Trial") or (strstr($var['regTy'], "no connection")) or (strstr($var['regTy'], "withdrawn")) or (strstr($var['regTy'], "expired")) ):?>
|
||||
|
||||
<span class="thanks">_(Thank you for trying Unraid OS)_!</span>
|
||||
|
||||
<?elseif ( ($var['regTy']=="Basic") or ($var['regTy']=="Plus") or ($var['regTy']=="Pro") ):?>
|
||||
|
||||
<span class="thanks">_(Thank you for choosing Unraid OS)_!</span>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if (strstr($var['regTy'], "unregistered")):?>
|
||||
|
||||
<div markdown="1" class="remark">
|
||||
:registration_1_plug:
|
||||
Your server will not be usable until you purchase a Registration key or install a free 30 day *Trial* key. A *Trial*
|
||||
key provides all the functionality of a *Pro* Registration key.
|
||||
|
||||
Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device
|
||||
at least 1GB in size (min 4GB recommended).
|
||||
|
||||
Note: USB memory card readers are generally **not** supported because most do not present unique serial numbers.
|
||||
:end
|
||||
</div>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if ($var['regTy']=="Trial"):?>
|
||||
|
||||
<div markdown="1" class="remark">
|
||||
:registration_3_plug:
|
||||
Your *Trial* key includes all the functionality and device support of a *Pro* Registration key.
|
||||
|
||||
After your *Trial* key has reached expiration, your server **still functions normally** until the next time you **Stop** the array.
|
||||
|
||||
At that point, you may either purchase a Registration key, or request a *Trial* extension.
|
||||
:end
|
||||
</div>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if (strstr($var['regTy'], "no connection")):?>
|
||||
|
||||
<div markdown="1" class="remark">
|
||||
<span class='red-text'>_(Cannot connect to key-server)_!</span>
|
||||
|
||||
_(Your *Trial* key requires an internet connection)_. _(Please check your)_ [_(Network Settings)_](NetworkSettings).
|
||||
</div>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if (strstr($var['regTy'], "withdrawn")):?>
|
||||
|
||||
<div markdown="1" class="remark">
|
||||
<span class='red-text'>_(Release has been withdrawn)_!</span>
|
||||
|
||||
_(This release has been withdrawn for use with *Trial* keys)_.
|
||||
</div>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if (strstr($var['regTy'], "expired")):?>
|
||||
|
||||
<div markdown="1" class="remark">
|
||||
<span class='red-text'>_(Your *Trial* key has expired)_.</span>
|
||||
|
||||
<? if ($trialExtensionEligible): ?>
|
||||
:registration_4_plug:
|
||||
To continue using Unraid OS you may purchase a Registration key. Alternately, you may request a *Trial* extension key.
|
||||
:end
|
||||
<? else: ?>
|
||||
:registration_trial_extension_ineligible_plug:
|
||||
You have used all your Trial extensions. To continue using Unraid OS you may purchase a Registration key.
|
||||
:end
|
||||
<? endif; ?>
|
||||
</div>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if (strstr($var['regTy'], "invalid installation")):?>
|
||||
|
||||
<span class='thanks red'>_(Invalid *Trial* Installation)_</span>
|
||||
<div markdown="1" class="remark">
|
||||
:registration_5_plug:
|
||||
It is not possible to use a *Trial* key with an existing Unraid OS installation.
|
||||
|
||||
You may purchase a Registration key corresponding to this USB Flash device to continue using this installation.
|
||||
|
||||
For more information, please [Contact Support](https://lime-technology.com/contact).
|
||||
:end
|
||||
</div>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if (strstr($var['regTy'], "missing")):?>
|
||||
|
||||
<span class='thanks red'>_(Missing Key File)_</span>
|
||||
<div markdown="1" class="remark">
|
||||
:registration_6_plug:
|
||||
It appears that your Registration key file is corrupted or missing. The key file should be located in the
|
||||
[config](/Registration/Browse?dir=/boot/config) directory on your USB Flash boot device.
|
||||
|
||||
If you do not have a backup copy of your Registration key file, [Contact Support](https://lime-technology.com/contact).
|
||||
|
||||
If this was a *Trial* installation, you may purchase a Registration key.
|
||||
:end
|
||||
</div>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if (strstr($var['regTy'], "invalid key")):?>
|
||||
|
||||
<span class='thanks red'>_(The registered GUID does not match the USB Flash boot device GUID)_</span>
|
||||
|
||||
<?if (strstr($var['regTy'], "Trial")):?>
|
||||
|
||||
<div markdown="1" class="remark">
|
||||
:registration_7_plug:
|
||||
*Trial* installations are only valid with the originally registered USB Flash device.
|
||||
|
||||
To continue using this installation with this USB Flash device, you may purchase a Registration key.
|
||||
:end
|
||||
</div>
|
||||
|
||||
<?else:?>
|
||||
|
||||
<div markdown="1" class="remark">
|
||||
:registration_8_plug:
|
||||
The Registration key file does not correspond to the USB Flash boot device.
|
||||
Please copy the correct key file to the [config](/Registration/Browse?dir=/boot/config) directory
|
||||
on your USB Flash boot device. If you do not have a backup copy of your key file, [Contact Support](https://lime-technology.com/contact).
|
||||
|
||||
If you want to replace your Registration key with a new key bound to this USB Flash device, click Replace Key below. An original key may be
|
||||
replaced anytime. Thereafter, a replacement key may be replaced again after one year has passed. If you require
|
||||
another replacement key sooner, [Contact Support](https://lime-technology.com/contact).
|
||||
|
||||
**Note:** Replacing a Registration key results in permanently *blacklisting* the previous USB Flash GUID.
|
||||
:end
|
||||
</div>
|
||||
|
||||
<?endif;?>
|
||||
<?endif;?>
|
||||
|
||||
<?if (strstr($var['regTy'], "blacklisted")):?>
|
||||
|
||||
<span class='thanks red'>_(Blacklisted USB Flash GUID)_</span>
|
||||
<div markdown="1" class="remark">
|
||||
:registration_9_plug:
|
||||
This USB Flash boot device has been *blacklisted*. This can occur as a result of transfering your Registration key to
|
||||
a replacement USB Flash device, and you are currently booted from your old USB Flash device.
|
||||
|
||||
A USB Flash device may also be *blacklisted* if there is no serial number, or if we discover the serial number
|
||||
is not unique (this is common with USB card readers).
|
||||
|
||||
For more information, please [Contact Support](https://lime-technology.com/contact).
|
||||
:end
|
||||
</div>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if ( ( !(strstr($var['regTy'], "invalid key")) and ((strstr($var['regTy'], "Trial"))) ) || (strstr($var['regTy'], "no connection")) || (strstr($var['regTy'], "withdrawn")) ):?>
|
||||
|
||||
_(***Trial*** key expires on)_:
|
||||
: <?=my_time_now($var['regTm2'])?>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if ( strstr($var['regTy'], "invalid installation") || ( (strstr($var['regTy'], "invalid key")) && (strstr($var['regTy'], "Trial")) )):?>
|
||||
|
||||
_(Expiration)_:
|
||||
: <?=my_time_now($var['regTm2'])?>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if ( (strstr($var['regTy'], "invalid installation")) || (strstr($var['regTy'], "invalid key")) || ($var['regTy']=="Basic") || ($var['regTy']=="Plus") || ($var['regTy']=="Pro") ):?>
|
||||
|
||||
_(Registered to)_:
|
||||
: <?=htmlspecialchars($var['regTo'])?>
|
||||
|
||||
_(Registered on)_:
|
||||
: <?=my_time_now($var['regTm'])?>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if ( (strstr($var['regTy'], "invalid installation")) or ( (strstr($var['regTy'], "invalid key")) and (!(strstr($var['regTy'], "Trial")))) ):?>
|
||||
|
||||
_(Registered GUID)_:
|
||||
: <?=$var['regGUID']?>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if (strstr($var['regTy'], "flash device error")):?>
|
||||
|
||||
<span class='thanks red'>_(Error accessing your physical USB Flash boot device)_</span>
|
||||
<div markdown="1" class="remark">
|
||||
_(There is a physical problem accessing your USB Flash boot device)_. _(Please)_ [Contact Support](https://lime-technology.com/contact).
|
||||
|
||||
_(Flash GUID)_:
|
||||
: _(Error code)_: <?=$var['regCheck']?>
|
||||
|
||||
<?else:?>
|
||||
|
||||
_(Flash GUID)_:
|
||||
: <?=$var['flashGUID']?>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
_(Flash Vendor)_:
|
||||
: <?=$var['flashVendor']?>
|
||||
|
||||
_(Flash Product)_:
|
||||
: <?=$var['flashProduct']?>
|
||||
|
||||
<?if ( ((strstr($var['regTy'], "invalid key")) and !(strstr($var['regTy'], "Trial"))) || ($var['regTy']=="Basic") || ($var['regTy']=="Plus") || ($var['regTy']=="Pro") ):?>
|
||||
|
||||
_(Replaceable)_:
|
||||
: <?=my_time_any($var['regTm2'])?>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if ( !(strstr($var['regTy'], "flash device error")) || !(strstr($var['regTy'], "blacklisted")) ):?>
|
||||
|
||||
<div class="device"><?=sprintf(_("This server has %s attached storage device".($var['deviceCount']==1?'.':'s.')),$var['deviceCount'])?></div>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
|
||||
: <unraid-i18n-host><unraid-key-actions></unraid-key-actions></unraid-i18n-host>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
||||
$var = (array)parse_ini_file('state/var.ini');
|
||||
|
||||
require_once "$docroot/webGui/include/Wrappers.php";
|
||||
require_once "$docroot/webGui/include/Helpers.php";
|
||||
extract(parse_plugin_cfg('dynamix',true));
|
||||
require_once "$docroot/plugins/dynamix.my.servers/include/state.php";
|
||||
|
||||
header('Content-type: application/json');
|
||||
|
||||
echo json_encode($serverState);
|
||||
@@ -0,0 +1,42 @@
|
||||
<style>
|
||||
#header {
|
||||
z-index: 102 !important;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
#header unraid-i18n-host {
|
||||
font-size: 16px;
|
||||
margin-left: auto;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
// Set the path for the local manifest file
|
||||
$localManifestFile = '/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/manifest.json';
|
||||
|
||||
// Load the local manifest
|
||||
$localManifest = json_decode(file_get_contents($localManifestFile), true);
|
||||
|
||||
$searchText = 'unraid-components.client.mjs';
|
||||
$fileValue = null;
|
||||
|
||||
foreach ($localManifest as $key => $value) {
|
||||
if (strpos($key, $searchText) !== false && isset($value["file"])) {
|
||||
$fileValue = $value["file"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($fileValue !== null) {
|
||||
$prefixedPath = '/plugins/dynamix.my.servers/unraid-components/';
|
||||
echo '<script src="' . $prefixedPath . $fileValue . '"></script>';
|
||||
} else {
|
||||
echo '<script>console.error("%cNo matching key containing \'' . $searchText . '\' found.", "font-weight: bold; color: white; background-color: red");</script>';
|
||||
}
|
||||
@@ -1,648 +1,25 @@
|
||||
<!-- myservers2 -->
|
||||
<?
|
||||
// add 'ipaddr' function for 6.9 backwards compatibility
|
||||
if (!function_exists('ipaddr')) {
|
||||
function ipaddr($ethX='eth0', $prot=4) {
|
||||
global $$ethX;
|
||||
switch ($$ethX['PROTOCOL:0']) {
|
||||
case 'ipv4':
|
||||
return $$ethX['IPADDR:0'];
|
||||
case 'ipv6':
|
||||
return $$ethX['IPADDR6:0'];
|
||||
case 'ipv4+ipv6':
|
||||
switch ($prot) {
|
||||
case 4: return $$ethX['IPADDR:0'];
|
||||
case 6: return $$ethX['IPADDR6:0'];
|
||||
default:return [$$ethX['IPADDR:0'],$$ethX['IPADDR6:0']];}
|
||||
default:
|
||||
return $$ethX['IPADDR:0'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$upc_translations = [
|
||||
($_SESSION['locale']) ? $_SESSION['locale'] : 'en_US' => [
|
||||
'getStarted' => _('Get Started'),
|
||||
'signIn' => _('Sign In'),
|
||||
'signUp' => _('Sign Up'),
|
||||
'signInUp' => _('Sign In / Up'),
|
||||
'signInUnraidNetAccount' => _('Sign In with Unraid.net Account'),
|
||||
'signOut' => _('Sign Out'),
|
||||
'error' => _('Error'),
|
||||
'fixError' => _('Fix Error'),
|
||||
'closeLaunchpad' => _('Close Launchpad and continue to webGUI'),
|
||||
'installPlugin' => _('Install Plugin'),
|
||||
'noThanks' => _('No thanks'),
|
||||
'closePromo' => _('Close Connect details and continue to webGUI'),
|
||||
'promoHeading' => _('Enhance your Unraid experience with these<br> Connect (BETA) features'),
|
||||
'learnMore' => _('Learn more'),
|
||||
'checkoutTheMyServersDocs' => _('Checkout the Connect docs'),
|
||||
'popUp' => _('Pop-up'),
|
||||
'close' => _('Close'),
|
||||
'backToPopUp' => sprintf(_('Back to %s'), _('Pop-up')),
|
||||
'closePopUp' => sprintf(_('Close %s'), _('Pop-up')),
|
||||
'contactSupport' => _('Contact Support'),
|
||||
'lanIp' => sprintf(_('Click to copy LAN IP %s'), '{0}'),
|
||||
'lanIpCopied' => _('LAN IP Copied'),
|
||||
'continueToUnraid' => _('Continue to Unraid'),
|
||||
'description' => _('Description'),
|
||||
'year' => _('year'),
|
||||
'years' => _('years'),
|
||||
'month' => _('month'),
|
||||
'months' => _('months'),
|
||||
'day' => _('day'),
|
||||
'days' => _('days'),
|
||||
'hour' => _('hour'),
|
||||
'hours' => _('hours'),
|
||||
'minute' => _('minute'),
|
||||
'minutes' => _('minutes'),
|
||||
'second' => _('second'),
|
||||
'seconds' => _('seconds'),
|
||||
'ago' => _('ago'),
|
||||
'basicPlusPro' => [
|
||||
'heading' => _('Thank you for choosing Unraid OS and Connect').'!',
|
||||
'message' => [
|
||||
'registered' => _('Register for Connect by signing in to Unraid.net'),
|
||||
'upgradeEligible' => _('To support more storage devices as your server grows click Upgrade Key'),
|
||||
],
|
||||
],
|
||||
'actions' => [
|
||||
'purchase' => _('Purchase Key'),
|
||||
'upgrade' => _('Upgrade Key'),
|
||||
'recover' => _('Recover Key'),
|
||||
'replace' => _('Replace Key'),
|
||||
'replaceIneligible' => _('Replace Key Ineligible'),
|
||||
'extend' => _('Extend Trial'),
|
||||
'startTrial' => _('Start Trial'),
|
||||
'signOutUnraidNet' => _('Sign Out of Unraid.net'),
|
||||
'redeemActivationCode' => _('Redeem Activation Code'),
|
||||
],
|
||||
'upc' => [
|
||||
'avatarAlt' => '{0} '._('Avatar'),
|
||||
'confirmClosure' => _('Confirm closure then continue to webGUI'),
|
||||
'closeDropdown' => _('Close dropdown'),
|
||||
'openDropdown' => _('Open dropdown'),
|
||||
'pleaseConfirmClosureYouHaveOpenPopUp' => _('Please confirm closure').'. '._('You have an open pop-up').'.',
|
||||
'trialHasExpiredSeeOptions' => _('Trial has expired see options below'),
|
||||
'errorCertRequiresSignIn' => _('Sign In before your Unraid.net SSL certificate expires'),
|
||||
'removeMyServersPlugin' => _('Remove Connect plugin'),
|
||||
'continueUsingMyServers' => _('Continue using Connect'),
|
||||
'confirmMyServersPluginRemoval' => _('Confirm Connect plugin removal'),
|
||||
'removingMyServersPlugin' => _('Removing Connect plugin…'),
|
||||
'enhanceYourExperienceWithMyServers' => _('Enhance your experience with Connect'),
|
||||
'lanIpCopied' => _('LAN IP Copied'),
|
||||
'installingMyServers' => _('Installing Connect (beta)'),
|
||||
"updatePlugin" => _('Update Plugin'),
|
||||
"updatingMyServers" => _('Updating Connect (beta)'),
|
||||
'thankYouForInstallingMyServers' => _('Thank you installing Connect') . '!',
|
||||
'connectYourUnraidnetAccountToGetStarted' => _('Connect your Unraid.net account to get started'),
|
||||
'noRemoteApikeyRegisteredWithPlg' => [
|
||||
'heading' => _('Connect Error'),
|
||||
'msg' => _('Unraid.net re-authentication required'),
|
||||
],
|
||||
'errorTooManyDisks' => [
|
||||
'heading' => 'Too many devices',
|
||||
'msg' => [
|
||||
'base' => 'You must upgrade your key to support more devices.',
|
||||
'basic' => 'Your Basic key supports 6 devices.',
|
||||
'plus' => 'Your Plus key supports 12 devices.',
|
||||
],
|
||||
],
|
||||
'extraLinks' => [
|
||||
'newTab' => sprintf(_('Opens %s in new tab'), '{0}'),
|
||||
'myServers' => _('Go to Connect'),
|
||||
'forums' => _('Unraid Forums'),
|
||||
'settings' => [
|
||||
'text' => _('Settings'),
|
||||
'title' => _('Settings > Management Access • Unraid.net'),
|
||||
],
|
||||
],
|
||||
'meta' => [
|
||||
'trial' => [
|
||||
'active' => [
|
||||
'date' => sprintf(_('Trial key expires at %s'), '{date}'),
|
||||
'timeDiff' => sprintf(_('Trial expires in %s'), '{timeDiff}'),
|
||||
],
|
||||
'expired' => [
|
||||
'date' => sprintf(_('Trial key expired at %s'), '{date}'),
|
||||
'timeDiff' => sprintf(_('Trial expired %s'), '{timeDiff}'),
|
||||
],
|
||||
],
|
||||
'uptime' => [
|
||||
'date' => sprintf(_('Server up since %s'), '{date}'),
|
||||
'readable' => sprintf(_('Uptime %s'), '{timeDiff}'),
|
||||
],
|
||||
],
|
||||
'myServers' => [
|
||||
'heading' => _('Connect'),
|
||||
'beta' => _('beta'),
|
||||
'restarting' => _('Restarting…'),
|
||||
'errors' => [
|
||||
'unraidApi' => [
|
||||
'heading' => _('Unraid API Error'),
|
||||
'message' => _('Failed to connect to Unraid API'),
|
||||
],
|
||||
'myServers' => [
|
||||
'heading' => _('Connect Error'),
|
||||
'message' => _('Please wait a moment and reload the page'),
|
||||
],
|
||||
],
|
||||
'closeDetails' => _('Close Details'),
|
||||
'loading' => _('Loading Connect data'),
|
||||
'displayingLastKnown' => _('Displaying last known server data'),
|
||||
'mothership' => [
|
||||
'connected' => _('Connected'),
|
||||
'notConnected' => _('Disconnected'),
|
||||
],
|
||||
'accessLabels' => [
|
||||
'current' => _('Current server'),
|
||||
'local' => _('Local access'),
|
||||
'offline' => _('Server Offline'),
|
||||
'remote' => _('Remote access'),
|
||||
'unavailable' => _('Access unavailable'),
|
||||
],
|
||||
'api' => [
|
||||
'start' => _('Restart unraid-api'),
|
||||
'startTitle' => _('Executes `unraid-api start`; no terminal needed'),
|
||||
'stop' => _('Stop unraid-api'),
|
||||
],
|
||||
],
|
||||
'opensNewHttpsWindow' => [
|
||||
'base' => sprintf(_('Opens new HTTPS window to %s'), '{0}'),
|
||||
'signIn' => sprintf(_('Opens new HTTPS window to %s'), _('Sign In')),
|
||||
'signOut' => sprintf(_('Opens new HTTPS window to %s'), _('Sign Out')),
|
||||
'purchase' => sprintf(_('Opens new HTTPS window to %s'), _('Purchase Key')),
|
||||
'upgrade' => sprintf(_('Opens new HTTPS window to %s'), _('Upgrade Key')),
|
||||
],
|
||||
'signInActions' => [
|
||||
'resolve' => _('Sign In to resolve'),
|
||||
'purchaseKey' => _('Sign In to Purchase Key'),
|
||||
'purchaseKeyOrExtendTrial' => '@:upc.signInActions.purchaseKey or @:actions.extend',
|
||||
],
|
||||
],
|
||||
'stateData' => [
|
||||
'ENOKEYFILE' => [
|
||||
'humanReadable' => _('No Keyfile'),
|
||||
'heading' => _("Let's unleash your hardware").'!',
|
||||
'message' => '<p>'._('Your server will not be usable until you purchase a Registration key or install a free 30-day Trial key').'. '._('A Trial key provides all the functionality of a Pro Registration key').'.</p><p>'._('Registration keys are bound to your USB Flash boot device serial number (GUID)').'. '._('Please use a high quality name brand device at least 1GB in size (min 4GB recommended)').'.</p><p>'._('Note: USB memory card readers are generally not supported because most do not present unique serial numbers').'.</p>',
|
||||
],
|
||||
'TRIAL' => [
|
||||
'humanReadable' => _('Trial'),
|
||||
'heading' => _('Thank you for choosing Unraid OS').'!',
|
||||
'message' => _('Your Trial key includes all the functionality and device support of a Pro key').'. '._('After your Trial has reached expiration your server still functions normally until the next time you Stop the array or reboot your server').'. '._('At that point you may either purchase a license key or request a Trial extension').'.',
|
||||
'_extraMsg' => sprintf(_('You have %s remaining on your Trial key'), '{parsedExpireTime}'),
|
||||
],
|
||||
'EEXPIRED' => [
|
||||
'humanReadable' => _('Trial Expired'),
|
||||
'heading' => _('Your Trial has expired'),
|
||||
'message' => [
|
||||
'base' => _('To continue using Unraid OS you may purchase a license key').'. ',
|
||||
'extensionNotEligible' => _('You have used all your Trial extensions').'. @:stateData.EEXPIRED.message.base',
|
||||
'extensionEligible' => '@:stateData.EEXPIRED.message.base '._('Alternately, you may request a Trial extension').'.',
|
||||
],
|
||||
],
|
||||
'BASIC' => [
|
||||
'humanReadable' => _('Basic'),
|
||||
],
|
||||
'PLUS' => [
|
||||
'humanReadable' => _('Plus'),
|
||||
],
|
||||
'PRO' => [
|
||||
'humanReadable' => _('Pro'),
|
||||
],
|
||||
'EGUID' => [
|
||||
'humanReadable' => _('GUID Error'),
|
||||
'error' => [
|
||||
'heading' => _('Registration key / GUID mismatch'),
|
||||
'message' => [
|
||||
'default' => _('The license key file does not correspond to the USB Flash boot device').'. '._('Please copy the correct key file to the */config* directory on your USB Flash boot device or choose Purchase Key').'.',
|
||||
'replacementIneligible' => _('Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months').'.',
|
||||
'replacementEligible' => _('The license key file does not correspond to the USB Flash boot device').'. '._('Please copy the correct key file to the */config* directory on your USB Flash boot device or choose Purchase Key or Replace Key').'.',
|
||||
'blacklisted' => _('Your Unraid registration key is ineligible for replacement as it is blacklisted') . '.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'ENOKEYFILE2' => [
|
||||
'humanReadable' => _('Missing key file'),
|
||||
'error' => [
|
||||
'heading' => '@:stateData.ENOKEYFILE2.humanReadable',
|
||||
'message' => _('It appears that your license key file is corrupted or missing').'. '._('The key file should be located in the */config* directory on your USB Flash boot device').'. '._('If you do not have a backup copy of your license key file you may install the Connect (beta) plugin to attempt to recover your key').'. '._('If this was an expired Trial installation, you may purchase a license key').'.',
|
||||
],
|
||||
],
|
||||
'ETRIAL' => [
|
||||
'humanReadable' => _('Invalid installation'),
|
||||
'error' => [
|
||||
'heading' => '@:stateData.ETRIAL.humanReadable',
|
||||
'message' => _('It is not possible to use a Trial key with an existing Unraid OS installation').'. '._('You may purchase a license key corresponding to this USB Flash device to continue using this installation').'.',
|
||||
],
|
||||
],
|
||||
'ENOKEYFILE1' => [
|
||||
'humanReadable' => _('No Keyfile'),
|
||||
'error' => [
|
||||
'heading' => _('No USB flash configuration data'),
|
||||
'message' => _('There is a problem with your USB Flash device'),
|
||||
],
|
||||
],
|
||||
'ENOFLASH' => [
|
||||
'humanReadable' => _('No Flash'),
|
||||
'error' => [
|
||||
'heading' => _('Cannot access your USB Flash boot device'),
|
||||
'message' => _('There is a physical problem accessing your USB Flash boot device'),
|
||||
],
|
||||
],
|
||||
'EGUID1' => [
|
||||
'humanReadable' => _('Multiple License Keys Present'),
|
||||
'error' => [
|
||||
'heading' => '@:stateData.EGUID1.humanReadable',
|
||||
'message' => _('There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device').'. '._('Please remove all key files except the one you want to replace from the */config* directory on your USB Flash boot device').'. '._('Alternately you may purchase a license key for this USB flash device').'. '._('If you want to replace one of your license keys with a new key bound to this USB Flash device please first remove all other key files first').'.',
|
||||
],
|
||||
],
|
||||
'EBLACKLISTED' => [
|
||||
'humanReadable' => _('BLACKLISTED'),
|
||||
'error' => [
|
||||
'heading' => _('Blacklisted USB Flash GUID'),
|
||||
'message' => _('This USB Flash boot device has been blacklisted').'. '._('This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device').'. '._('A USB Flash device may also be blacklisted if we discover the serial number is not unique – this is common with USB card readers').'.',
|
||||
],
|
||||
],
|
||||
'EBLACKLISTED1' => [
|
||||
'humanReadable' =>'@:stateData.EBLACKLISTED.humanReadable',
|
||||
'error' => [
|
||||
'heading' => _('USB Flash device error'),
|
||||
'message' => _('This USB Flash device has an invalid GUID').'. '._('Please try a different USB Flash device').'.',
|
||||
],
|
||||
],
|
||||
'EBLACKLISTED2' => [
|
||||
'humanReadable' => '@:stateData.EBLACKLISTED.humanReadable',
|
||||
'error' => [
|
||||
'heading' => _('USB Flash has no serial number'),
|
||||
'message' => '@:stateData.EBLACKLISTED.error.message',
|
||||
],
|
||||
],
|
||||
'ENOCONN' => [
|
||||
'humanReadable' => _('Trial Requires Internet Connection'),
|
||||
'error' => [
|
||||
'heading' => _('Cannot validate Unraid Trial key'),
|
||||
'message' => _('Your Trial key requires an internet connection').'. '._('Please check Settings > Network').'.',
|
||||
],
|
||||
],
|
||||
'STALE' => [
|
||||
'humanReadable' => _('Stale'),
|
||||
'error' => [
|
||||
'heading' => _('Stale Server'),
|
||||
'message' => _('Please refresh the page to ensure you load your latest configuration'),
|
||||
],
|
||||
],
|
||||
],
|
||||
'regWizPopUp' => [
|
||||
'regWiz' => _('Registration Wizard'),
|
||||
'toHome' => _('To Registration Wizard Home'),
|
||||
'continueTrial' => _('Continue Trial'),
|
||||
'serverInfoToggle' => _('Toggle server info visibility'),
|
||||
'youCanSafelyCloseThisWindow' => _('You can safely close this window'),
|
||||
'automaticallyClosingIn' => sprintf(_('Auto closing in %s'), '{0}'),
|
||||
'byeBye' => _('bye bye 👋'),
|
||||
'browserWillSelfDestructIn' => sprintf(_('Browser will self destruct in %s'), '{0}'),
|
||||
'closingPopUpMayLeadToErrors' => _('Closing this pop-up window while actions are being preformed may lead to unintended errors'),
|
||||
'goBack' => _('Go Back'),
|
||||
'shutDown' => _('Shut Down'),
|
||||
'haveAccountSignIn' => _('Already have an account').'? '._('Sign In'),
|
||||
'noAccountSignUp' => _('Do not have an account').'? '._('Sign Up'),
|
||||
'willConnectYourServerToMyServers' => _('This will register your server with Connect <sup>BETA</sup>'),
|
||||
'serverInfo' => [
|
||||
'flash' => _('Flash'),
|
||||
'product' => _('Product'),
|
||||
'GUID' => _('GUID'),
|
||||
'name' => _('Name'),
|
||||
'ip' => _('IP'),
|
||||
],
|
||||
'forms' => [
|
||||
'displayName' => _('Display Name'),
|
||||
'emailAddress' => _('Email Address'),
|
||||
'displayNameOrEmailAddress' => _('Display Name or Email Address'),
|
||||
'displayNameRootMessage' => _('Use your Unraid.net credentials, not your local server credentials'),
|
||||
'honeyPotCopy' => _('If you fill this field out then your email will not be sent'),
|
||||
'fieldRequired' => _('This field is required'),
|
||||
'submit' => _('Submit'),
|
||||
'submitting' => _('Submitting'),
|
||||
'notValid' => _('Form not valid'),
|
||||
'cancel' => _('Cancel'),
|
||||
'confirm' => _('Confirm'),
|
||||
'createMyAccount' => _('Create My Account'),
|
||||
'subject' => _('Subject'),
|
||||
'password' => _('Password'),
|
||||
'togglePasswordVisibility' => _('Toggle Password Visibility'),
|
||||
'message' => _('Message'),
|
||||
'confirmPassword' => _('Confirm Password'),
|
||||
'passwordMustMatch' => _('Password confirmation must match'),
|
||||
'passwordMinimum' => _('8 or more characters'),
|
||||
'comments' => _('comments'),
|
||||
'newsletterCopy' => _('Sign me up for the monthly Unraid newsletter').': '._('a digest of recent blog posts, community videos, popular forum threads, product announcements, and more'),
|
||||
'terms' => [
|
||||
'iAgree' => _('I agree to the'),
|
||||
'text' => _('Terms of Use'),
|
||||
],
|
||||
],
|
||||
'routes' => [
|
||||
'extendTrial' => [
|
||||
'heading' => [
|
||||
'loading' => _('Extending Trial'),
|
||||
'error' => _('Trial Extension Failed'),
|
||||
],
|
||||
'message' => _('Not ready to purchase?').'<br>'._('Receive an additional 15 days for your trial').'.',
|
||||
],
|
||||
'forgotPassword' => [
|
||||
'heading' => _('Forgot Password'),
|
||||
'subheading' => _("After resetting your password come back to the Registration Wizard pop-up window to Sign In and complete your server's registration"),
|
||||
'resetPasswordNow' => _('Reset Password Now'),
|
||||
'backToSignIn' => _('Back to Sign In'),
|
||||
],
|
||||
'signIn' => [
|
||||
'heading' => [
|
||||
'signIn' => _('Unraid.net Sign In'),
|
||||
'recover' => _('Unraid.net Sign In to Recover Key'),
|
||||
'replace' => _('Unraid.net Sign In to Replace Key'),
|
||||
],
|
||||
'form' => [
|
||||
'replacementConditions' => [
|
||||
'name' => _('Acknowledge Replacement Conditions'),
|
||||
'label' => _('I acknowledge that replacing a license key results in permanently blacklisting the previous USB Flash GUID'),
|
||||
],
|
||||
'label' => [
|
||||
'password' => [
|
||||
'replace' => _('Unraid.net account password'),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'signUp' => [
|
||||
'heading' => _('Create Unraid.net Account'),
|
||||
],
|
||||
'signOut' => [
|
||||
'heading' => _('Unraid.net Sign Out'),
|
||||
'warnings' => [
|
||||
'remoteAccessDisabled' => _('Remote access will be disabled'),
|
||||
'remoteAccessInaccessible' => sprintf(_('You will no longer have access to this server using <abbr title="%s" class="italic">this url</abbr>'), '{0}'),
|
||||
'disablingFlashBackup' => _('Automated flash backups will be disabled until you sign in again'),
|
||||
'downloadFlashBackup' => _('Download latest backup from Connect Dashboard before signing out'),
|
||||
],
|
||||
],
|
||||
'success' => [
|
||||
'heading' => [
|
||||
'username' => sprintf(_('Hi %s'), '{0}'),
|
||||
'default' => _('Success'),
|
||||
],
|
||||
'subheading' => [
|
||||
'extention' => _('Your trial will expire in 15 days'),
|
||||
'newTrial' => _('Your trial will expire in 30 days'),
|
||||
],
|
||||
'signIn' => [
|
||||
'tileTitle' => [
|
||||
'actionFail' => sprintf(_('%s was not signed in to your Unraid.net account'), '{0}'),
|
||||
'actionSuccess' => sprintf(_('%s is signed in to your Unraid.net account'), '{0}'),
|
||||
'loading' => sprintf(_('Signing in %s to Unraid.net account'), '{0}'),
|
||||
],
|
||||
],
|
||||
'signOut' => [
|
||||
'tileTitle' => [
|
||||
'actionFail' => sprintf(_('%s was not signed out of your Unraid.net account'), '{0}'),
|
||||
'actionSuccess' => sprintf(_('%s was signed out of your Unraid.net account'), '{0}'),
|
||||
'loading' => sprintf(_('Signing out %s from Unraid.net account'), '{0}'),
|
||||
],
|
||||
],
|
||||
'keys' => [
|
||||
'trial' => _('Trial'),
|
||||
'basic' => _('Basic'),
|
||||
'plus' => _('Plus'),
|
||||
'pro' => _('Pro'),
|
||||
],
|
||||
'extended' => sprintf(_('%s Key Extended'), '{0}'),
|
||||
'recovered' => sprintf(_('%s Key Recovered'), '{0}'),
|
||||
'replaced' => sprintf(_('%s Key Replaced'), '{0}'),
|
||||
'created' => sprintf(_('%s Key Created'), '{0}'),
|
||||
'install' => [
|
||||
'loading' => sprintf(_('Installing %s Key'), '{0}'),
|
||||
'error' => sprintf(_('%s Key Install Error'), '{0}'),
|
||||
'success' => sprintf(_('Installed %s Key'), '{0}'),
|
||||
'manualInstructions' => _('To manually install the key paste the key file url into the Key file URL field on the webGUI Tools > Registration page and then click Install Key') . '.',
|
||||
'copyFail' => _('Unable to copy'),
|
||||
'copySuccess' => _('Copied key url') . '!',
|
||||
'copyButton' => _('Copy Key URL'),
|
||||
'copyBeforeClose' => _('Please copy the Key URL before closing this window'),
|
||||
],
|
||||
'timeout' => sprintf(_('Communication with %s has timed out'), '{0}'),
|
||||
'loading1' => _('Please keep this window open'),
|
||||
'loading2' => _('Still working our magic'),
|
||||
'countdown' => [
|
||||
'success' => [
|
||||
'prefix' => sprintf(_('Auto closing in %s'), '{0}'),
|
||||
'text' => _('You can safely close this window'),
|
||||
],
|
||||
'error' => [
|
||||
'prefix' => sprintf(_('Auto redirecting in %s'), '{0}'),
|
||||
'text' => _('Back to Registration Home'),
|
||||
'complete' => _('Back in a flash ⚡️'),
|
||||
],
|
||||
],
|
||||
],
|
||||
'troubleshoot' => [
|
||||
'heading' => [
|
||||
'default' => _('Troubleshoot'),
|
||||
'success' => _('Thank you for contacting Unraid'),
|
||||
],
|
||||
'subheading' => [
|
||||
'default' => _("Forgot what Unraid.net account you used").'? '._("Have a USB flash device that already has an account associated with it").'? '._("Just give us the details about what happened and we'll do our best to get you up and running again").'.',
|
||||
'success' => _('We have received your e-mail and will respond in the order it was received').'. '._('While we strive to respond to all requests as quickly as possible please allow for up to 3 business days for a response').'.',
|
||||
],
|
||||
'relevantServerData' => _('Your USB Flash GUID and other relevant server data will also be sent'),
|
||||
],
|
||||
'verifyEmail' => [
|
||||
'heading' => _('Verify Email'),
|
||||
'subheading' => sprintf(_('We have sent a verifcation email to %s'), '{0}'),
|
||||
'form' => [
|
||||
'verificationCode' => _('verification code'),
|
||||
'verifyCode' => _('Paste or Enter code'),
|
||||
],
|
||||
'noCode' => _("Didn't get code?"),
|
||||
],
|
||||
'verifyEmailResend' => [
|
||||
'heading' => _('Resend Email Verification Code'),
|
||||
'goBack' => _("Have the code now? Go Back"),
|
||||
'resend' => _("Resend Code"),
|
||||
],
|
||||
'whatIsMyServers' => [
|
||||
'heading' => _('What is Unraid.net?'),
|
||||
'subheading' => _('Expand your servers capabilities'),
|
||||
'copy' => _('With an Unraid.net account you can start using Connect (beta) which gives you access to the following features:'),
|
||||
'features' => [
|
||||
"dynamicRemoteAccess" => [
|
||||
'heading' => _('Dynamic Remote Access'),
|
||||
'copy' => _('Toggle on/off server accessibility with dynamic remote access').'. '._('Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds').'.',
|
||||
],
|
||||
"manageWithinConnect" => [
|
||||
'heading' => _('Manage Your Server Within Connect'),
|
||||
'copy' => _('Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI').'. '._('Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window').'.',
|
||||
],
|
||||
"deepLinking" => [
|
||||
'heading' => _('Deep Linking'),
|
||||
'copy' => _('The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections').'.',
|
||||
],
|
||||
"onlineFlashBackup" => [
|
||||
'heading' => _('Online Flash Backup'),
|
||||
'copy' => _('Never ever be left without a backup of your config').'. '._('If you need to change flash drives, generate a backup from Connect and be up and running in minutes').'.',
|
||||
],
|
||||
"realTimeMonitoring" => [
|
||||
'heading' => _('Real-time Monitoring'),
|
||||
'copy' => _("Get an overview of your server's state, storage space, apps and VMs status, and more").'.',
|
||||
],
|
||||
"customizableDashboardTitles" => [
|
||||
'heading' => _('Customizable Dashboard Tiles'),
|
||||
'copy' => _("Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard").'.',
|
||||
],
|
||||
"licenseManagement" => [
|
||||
'heading' => _('License Management'),
|
||||
'copy' => _('Manage your license keys at any time via the My Keys section').'.',
|
||||
],
|
||||
'plusMore' => [
|
||||
'heading' => _('Plus more on the way'),
|
||||
'copy' => _('All you need is an active internet connection, an Unraid.net account, and the Connect plugin').'. '._('Get started by installing the plugin') . '.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'replaceKey' => [
|
||||
'subheading' => [
|
||||
'registered' => 'A record of your replacement will be sent to your Unraid.net account email address',
|
||||
'notRegistered' => 'A record of your replacement will be sent to this email',
|
||||
],
|
||||
],
|
||||
'notFound' => [
|
||||
'subheading' => _('Page Not Found'),
|
||||
],
|
||||
'notAllowed' => [
|
||||
'subheading' => _('Page Not Allowed'),
|
||||
],
|
||||
],
|
||||
],
|
||||
'wanIpCheck' => [
|
||||
'checking' => _('Checking Wan IPs'),
|
||||
'match' => sprintf(_('Remark: your WAN IPv4 is **%s**'), '{0}'),
|
||||
'mismatch' => sprintf(_("Remark: Unraid's WAN IPv4 **%1s** does not match your client's WAN IPv4 **%2s**"), '{0}', '{1}').'. '._('This may indicate a complex network that will not work with this Remote Access solution').'. '._('Ignore this message if you are currently connected via Remote Access or VPN').'.',
|
||||
'resolveError' => _('DNS issue, unable to resolve wanip4.unraid.net'),
|
||||
],
|
||||
'upcTrigger' => [
|
||||
'upgrade' => _('To support more storage devices as your server grows click the *Open Dropdown* button').'.',
|
||||
'default' => _('Key management is done via the dropdown in the top right of the webGUI on every page').'.',
|
||||
'open' => _('Open Dropdown'),
|
||||
],
|
||||
'yargYePirate' => _('Oh no! Are you pirating Unraid OS?<br>Are you ready to buy a real license?'),
|
||||
'keyFileNotValid' => _('Key file not valid'),
|
||||
'installFailed' => [
|
||||
'heading' => _('Connect plugin install failed'),
|
||||
'message' => _('The Connect plugin install is incomplete').'. '._('Please uninstall and reinstall the Connect plugin').'. '._('Be sure to let the install complete before you close the window').'.',
|
||||
],
|
||||
"downloadUnraidApiLogs" => _('Download unraid-api Logs'),
|
||||
"download" => _('Download'),
|
||||
"pleaseWait" => _('Please wait…')
|
||||
],
|
||||
];
|
||||
// note: $myservers variable defined in myservers1.php, by parsing myservers.cfg
|
||||
|
||||
$configErrorEnum = [ // used to map $var['configValid'] value to mimic unraid-api's `configError` ENUM
|
||||
"error" => 'UNKNOWN_ERROR',
|
||||
"invalid" => 'INVALID',
|
||||
"nokeyserver" => 'NO_KEY_SERVER',
|
||||
"withdrawn" => 'WITHDRAWN',
|
||||
];
|
||||
$nginx = parse_ini_file('/var/local/emhttp/nginx.ini');
|
||||
|
||||
$plgInstalled = '';
|
||||
if (!file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net') && !file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) {
|
||||
$plgInstalled = ''; // base OS only, plugin not installed • show ad for plugin
|
||||
} else {
|
||||
// plugin is installed but if the unraid-api file doesn't fully install it's a failed install
|
||||
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net')) $plgInstalled = 'dynamix.unraid.net.plg';
|
||||
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) $plgInstalled = 'dynamix.unraid.net.staging.plg';
|
||||
// plugin install failed • append failure detected so we can show warning about failed install via UPC
|
||||
if (!file_exists('/usr/local/sbin/unraid-api')) $plgInstalled = $plgInstalled . '_installFailed';
|
||||
}
|
||||
|
||||
// read flashbackup ini file
|
||||
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
|
||||
$flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
|
||||
|
||||
$serverstate = [ // feeds server vars to Vuex store in a slightly different array than state.php
|
||||
"avatar" => (!empty($myservers['remote']['avatar']) && $plgInstalled) ? $myservers['remote']['avatar'] : '',
|
||||
"config" => [
|
||||
'valid' => $var['configValid'] === 'yes',
|
||||
'error' => $var['configValid'] !== 'yes'
|
||||
? (array_key_exists($var['configValid'], $configErrorEnum) ? $configErrorEnum[$var['configValid']] : 'UNKNOWN_ERROR')
|
||||
: null,
|
||||
],
|
||||
"deviceCount" => $var['deviceCount'],
|
||||
"email" => $myservers['remote']['email'] ?? '',
|
||||
"extraOrigins" => explode(',', $myservers['api']['extraOrigins']??''),
|
||||
"flashproduct" => $var['flashProduct'],
|
||||
"flashvendor" => $var['flashVendor'],
|
||||
"flashBackupActivated" => empty($flashbackup_status['activated']) ? '' : 'true',
|
||||
"guid" => $var['flashGUID'],
|
||||
"hasRemoteApikey" => !empty($myservers['remote']['apikey']),
|
||||
"internalip" => ipaddr(),
|
||||
"internalport" => $_SERVER['SERVER_PORT'],
|
||||
"keyfile" => empty($var['regFILE'])? "" : str_replace(['+','/','='], ['-','_',''], trim(base64_encode(@file_get_contents($var['regFILE'])))),
|
||||
"osVersion" => $var['version'],
|
||||
"plgVersion" => $plgversion = file_exists('/var/log/plugins/dynamix.unraid.net.plg')
|
||||
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null'))
|
||||
: ( file_exists('/var/log/plugins/dynamix.unraid.net.staging.plg')
|
||||
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null'))
|
||||
: 'base-'.$var['version'] ),
|
||||
"plgInstalled" => $plgInstalled,
|
||||
"protocol" => $_SERVER['REQUEST_SCHEME'],
|
||||
"reggen" => (int)$var['regGen'],
|
||||
"regGuid" => $var['regGUID'],
|
||||
"registered" => (!empty($myservers['remote']['username']) && $plgInstalled),
|
||||
"servername" => $var['NAME'],
|
||||
"site" => $_SERVER['REQUEST_SCHEME']."://".$_SERVER['HTTP_HOST'],
|
||||
"state" => strtoupper(empty($var['regCheck']) ? $var['regTy'] : $var['regCheck']),
|
||||
"ts" => time(),
|
||||
"username" => (!empty($myservers['remote']['username']) && $plgInstalled) ? $myservers['remote']['username'] : '',
|
||||
"wanFQDN" => $nginx['NGINX_WANFQDN'] ?? '',
|
||||
];
|
||||
/** @TODO - prop refactor needed. The issue is because the prop names share the same name as the vuex store variables
|
||||
* if we remove the props and deployed a UPC that doesn't rely on props anymore uses that don't have an updated version
|
||||
* of this file will have a non-working UPC.
|
||||
* apikey
|
||||
* apiVersion
|
||||
* csrf
|
||||
* expiretime
|
||||
* hideMyServers
|
||||
* plgPath
|
||||
* regWizTime
|
||||
* sendCrashInfo
|
||||
* serverdesc
|
||||
* servermodel
|
||||
* serverupdate
|
||||
* uptime
|
||||
*/
|
||||
<?php
|
||||
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
||||
require_once("$docroot/plugins/dynamix.my.servers/include/state.php");
|
||||
require_once("$docroot/plugins/dynamix.my.servers/include/translations.php");
|
||||
?>
|
||||
<unraid-user-profile
|
||||
apikey="<?=$myservers['upc']['apikey'] ?? ''?>"
|
||||
api-version="<?=$myservers['api']['version'] ?? ''?>"
|
||||
banner="<?=$display['banner'] ?? ''?>"
|
||||
bgcolor="<?=($backgnd) ? '#'.$backgnd : ''?>"
|
||||
csrf="<?=$var['csrf_token']?>"
|
||||
displaydesc="<?=($display['headerdescription']??''!='no') ? 'true' : ''?>"
|
||||
expiretime="<?=1000*($var['regTy']=='Trial'||strstr($var['regTy'],'expired')?$var['regTm2']:0)?>"
|
||||
hide-my-servers="<?=$plgInstalled ? '' : 'yes' ?>"
|
||||
locale="<?=($_SESSION['locale']) ? $_SESSION['locale'] : 'en_US'?>"
|
||||
locale-messages="<?=rawurlencode(json_encode($upc_translations, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE))?>"
|
||||
metacolor="<?=($display['headermetacolor']??'') ? '#'.$display['headermetacolor'] : ''?>"
|
||||
plg-path="dynamix.my.servers"
|
||||
reg-wiz-time="<?=$myservers['remote']['regWizTime'] ?? ''?>"
|
||||
serverdesc="<?=$var['COMMENT']?>"
|
||||
servermodel="<?=$var['SYS_MODEL']?>"
|
||||
serverstate="<?=rawurlencode(json_encode($serverstate, JSON_UNESCAPED_SLASHES))?>"
|
||||
show-banner-gradient="<?=$display['showBannerGradient'] ?? 'yes'?>"
|
||||
textcolor="<?=($header) ? '#'.$header : ''?>"
|
||||
theme="<?=$display['theme']?>"
|
||||
uptime="<?=1000*(time() - round(strtok(exec("cat /proc/uptime"),' ')))?>"
|
||||
></unraid-user-profile>
|
||||
<!-- /myservers2 -->
|
||||
<script>
|
||||
window.LOCALE_DATA = '<?= rawurlencode(json_encode($webComponentTranslations, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE)) ?>';
|
||||
/**
|
||||
* So we're not needing to modify DefaultLayout with an additional include, we'll add the Modals web component to the bottom of the body.
|
||||
*/
|
||||
const i18nHostWebComponent = 'unraid-i18n-host';
|
||||
const modalsWebComponent = 'unraid-modals';
|
||||
if (!document.getElementsByTagName(modalsWebComponent).length) {
|
||||
const $body = document.getElementsByTagName('body')[0];
|
||||
const $i18nHost = document.createElement(i18nHostWebComponent);
|
||||
const $modals = document.createElement(modalsWebComponent);
|
||||
$body.appendChild($i18nHost);
|
||||
$i18nHost.appendChild($modals);
|
||||
}
|
||||
</script>
|
||||
<?
|
||||
echo "
|
||||
<unraid-i18n-host>
|
||||
<unraid-user-profile server='" . json_encode($serverState) . "'></unraid-user-profile>
|
||||
</unraid-i18n-host>";
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
// read flashbackup ini file
|
||||
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
|
||||
$flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
|
||||
|
||||
$nginx = parse_ini_file('/var/local/emhttp/nginx.ini');
|
||||
|
||||
// base OS only, plugin not installed • show ad for plugin
|
||||
$connectPluginInstalled = '';
|
||||
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net')) $connectPluginInstalled = 'dynamix.unraid.net.plg';
|
||||
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) $connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
|
||||
// plugin install failed if the unraid-api file doesn't fully install • append failure detected so we can show warning about failed install via UPC
|
||||
if ($connectPluginInstalled && !file_exists('/usr/local/sbin/unraid-api')) $connectPluginInstalled .= '_installFailed';
|
||||
|
||||
$connectPluginVersion = file_exists('/var/log/plugins/dynamix.unraid.net.plg')
|
||||
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null'))
|
||||
: (file_exists('/var/log/plugins/dynamix.unraid.net.staging.plg')
|
||||
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null'))
|
||||
: 'base-' . $var['version']);
|
||||
|
||||
$myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg';
|
||||
$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : [];
|
||||
|
||||
$configErrorEnum = [
|
||||
"error" => 'UNKNOWN_ERROR',
|
||||
"invalid" => 'INVALID',
|
||||
"nokeyserver" => 'NO_KEY_SERVER',
|
||||
"withdrawn" => 'WITHDRAWN',
|
||||
];
|
||||
|
||||
$registered = !empty($myservers['remote']['username']) && $connectPluginInstalled;
|
||||
|
||||
$serverState = [
|
||||
"apiKey" => $registered ? $myservers['upc']['apikey'] ?? '' : '',
|
||||
"apiVersion" => $myservers['api']['version'] ?? '',
|
||||
"avatar" => (!empty($myservers['remote']['avatar']) && $connectPluginInstalled) ? $myservers['remote']['avatar'] : '',
|
||||
"config" => [
|
||||
'valid' => ($var['configValid'] === 'yes'),
|
||||
'error' => isset($configErrorEnum[$var['configValid']]) ? $configErrorEnum[$var['configValid']] : 'UNKNOWN_ERROR',
|
||||
],
|
||||
"connectPluginInstalled" => $connectPluginInstalled,
|
||||
"connectPluginVersion" => $connectPluginVersion,
|
||||
"csrf" => $var['csrf_token'],
|
||||
"description" => $var['COMMENT'] ?? '',
|
||||
"deviceCount" => $var['deviceCount'],
|
||||
"email" => $myservers['remote']['email'] ?? '',
|
||||
"expireTime" => 1000 * (($var['regTy'] === 'Trial' || strstr($var['regTy'], 'expired')) ? $var['regTm2'] : 0),
|
||||
"extraOrigins" => explode(',', $myservers['api']['extraOrigins'] ?? ''),
|
||||
"flashProduct" => $var['flashProduct'],
|
||||
"flashVendor" => $var['flashVendor'],
|
||||
"flashBackupActivated" => empty($flashbackup_status['activated']) ? '' : 'true',
|
||||
"guid" => $var['flashGUID'],
|
||||
"hasRemoteApikey" => !empty($myservers['remote']['apikey']),
|
||||
"internalPort" => $_SERVER['SERVER_PORT'],
|
||||
"keyfile" => empty($var['regFILE']) ? '' : str_replace(['+', '/', '='], ['-', '_', ''], trim(base64_encode(@file_get_contents($var['regFILE'])))),
|
||||
"lanIp" => ipaddr(),
|
||||
"locale" => ($_SESSION['locale']) ? $_SESSION['locale'] : 'en_US',
|
||||
"model" => $var['SYS_MODEL'],
|
||||
"name" => $var['NAME'],
|
||||
"osVersion" => $var['version'],
|
||||
"protocol" => $_SERVER['REQUEST_SCHEME'],
|
||||
"regGen" => (int)$var['regGen'],
|
||||
"regGuid" => $var['regGUID'],
|
||||
"registered" => $registered,
|
||||
"registeredTime" => $myservers['remote']['regWizTime'] ?? '',
|
||||
"site" => $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'],
|
||||
"state" => strtoupper(empty($var['regCheck']) ? $var['regTy'] : $var['regCheck']),
|
||||
"theme" => [
|
||||
"banner" => !empty($display['banner']),
|
||||
"bannerGradient" => $display['showBannerGradient'] === 'yes' ?? false,
|
||||
"bgColor" => ($display['background']) ? '#' . $display['background'] : '',
|
||||
"descriptionShow" => (!empty($display['headerdescription']) && $display['headerdescription'] !== 'no'),
|
||||
"metaColor" => ($display['headermetacolor'] ?? '') ? '#' . $display['headermetacolor'] : '',
|
||||
"name" => $display['theme'],
|
||||
"textColor" => ($display['header']) ? '#' . $display['header'] : '',
|
||||
],
|
||||
"ts" => time(),
|
||||
"uptime" => 1000 * (time() - round(strtok(exec("cat /proc/uptime"), ' '))),
|
||||
"username" => $registered ? $myservers['remote']['username'] : '',
|
||||
"wanFQDN" => $nginx['NGINX_WANFQDN'] ?? '',
|
||||
];
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
$webComponentTranslations = [
|
||||
($_SESSION['locale']) ? $_SESSION['locale'] : 'en_US' => [
|
||||
'LAN IP' => _('LAN IP'),
|
||||
'LAN IP {0}' => sprintf(_('LAN IP %s'), '{0}'),
|
||||
'LAN IP Copied' => _('LAN IP Copied'),
|
||||
'Click to Copy LAN IP {0}' => sprintf(_('Click to copy LAN IP %s'), '{0}'),
|
||||
'Trial Key Expired at {0}' => sprintf(_('Trial Key Expired at %s'), '{0}'),
|
||||
'Trial Key Expires at {0}' => sprintf(_('Trial Key Expires at %s'), '{0}'),
|
||||
'Trial Key Expired {0}' => sprintf(_('Trial Key Expired %s'), '{0}'),
|
||||
'Trial Key Expires in {0}' => sprintf(_('Trial Key Expires in %s'), '{0}'),
|
||||
'Server Up Since {0}' => sprintf(_('Server Up Since %s'), '{0}'),
|
||||
'Uptime {0}' => sprintf(_('Uptime %s'), '{0}'),
|
||||
'year' => sprintf(_('%s year'), '{n}') . ' | ' . sprintf(_('%s years'), '{n}'),
|
||||
'month' => sprintf(_('%s month'), '{n}') . ' | ' . sprintf(_('%s months'), '{n}'),
|
||||
'day' => sprintf(_('%s day'), '{n}') . ' | ' . sprintf(_('%s days'), '{n}'),
|
||||
'hour' => sprintf(_('%s hour'), '{n}') . ' | ' . sprintf(_('%s hours'), '{n}'),
|
||||
'minute' => sprintf(_('%s minute'), '{n}') . ' | ' . sprintf(_('%s minutes'), '{n}'),
|
||||
'second' => sprintf(_('%s second'), '{n}') . ' | ' . sprintf(_('%s seconds'), '{n}'),
|
||||
'ago' => _('ago'),
|
||||
'Purchase' => _('Purchase'),
|
||||
'Upgrade' => _('Upgrade'),
|
||||
'Fix Error' => _('Fix Error'),
|
||||
'Get Started' => _('Get Started'),
|
||||
'Trial Expired, see options below' => _('Trial Expired, see options below'),
|
||||
'Learn more about the error' => _('Learn more about the error'),
|
||||
'Close Dropdown' => _('Close Dropdown'),
|
||||
'Open Dropdown' => _('Open Dropdown'),
|
||||
'Thank you for installing Connect!' => _('Thank you for installing Connect!'),
|
||||
'Sign In to your Unraid.net account to get started' => _('Sign In to your Unraid.net account to get started'),
|
||||
'Go to Connect' => _('Go to Connect'),
|
||||
'Opens Connect in new tab' => _('Opens Connect in new tab'),
|
||||
'Manage Unraid.net Account' => _('Manage Unraid.net Account'),
|
||||
'Manage Unraid.net Account in new tab' => _('Manage Unraid.net Account in new tab'),
|
||||
'Settings' => _('Settings'),
|
||||
'Go to Connect plugin settings' => _('Go to Connect plugin settings'),
|
||||
'Enhance your Unraid experience with Connect' => _('Enhance your Unraid experience with Connect'),
|
||||
'Beta' => _('Beta'),
|
||||
'Loading' => _('Loading'),
|
||||
'Restarting unraid-api…' => _('Restarting unraid-api…'),
|
||||
'unraid-api is offline' => _('unraid-api is offline'),
|
||||
'Introducing Unraid Connect' => _('Introducing Unraid Connect'),
|
||||
'Enhance your Unraid experience' => _('Enhance your Unraid experience'),
|
||||
'Connected' => _('Connected'),
|
||||
'Dynamic Remote Access' => _('Dynamic Remote Access'),
|
||||
'Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.' => _('Toggle on/off server accessibility with dynamic remote access.') . ' ' . _('Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.'),
|
||||
'Manage Your Server Within Connect' => _('Manage Your Server Within Connect'),
|
||||
'Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.' => _('Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI.') . ' ' . _('Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.'),
|
||||
'Deep Linking' => _('Deep Linking'),
|
||||
'The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.' => _('The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.'),
|
||||
'Online Flash Backup' => _('Online Flash Backup'),
|
||||
'Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.' => _('Never ever be left without a backup of your config.') . ' ' . _('If you need to change flash drives, generate a backup from Connect and be up and running in minutes.'),
|
||||
'Real-time Monitoring' => _('Real-time Monitoring'),
|
||||
'Get an overview of your server\'s state, storage space, apps and VMs status, and more.' => _('Get an overview of your server\'s state, storage space, apps and VMs status, and more.'),
|
||||
'Customizable Dashboard Tiles' => _('Customizable Dashboard Tiles'),
|
||||
'Set custom server tiles how you like and automatically display your server\'s banner image on your Connect Dashboard.' => _('Set custom server tiles how you like and automatically display your server\'s banner image on your Connect Dashboard.'),
|
||||
'License Management' => _('License Management'),
|
||||
'Manage your license keys at any time via the My Keys section.' => _('Manage your license keys at any time via the My Keys section.'),
|
||||
'Plus more on the way' => _('Plus more on the way'),
|
||||
'All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.' => _('All you need is an active internet connection, an Unraid.net account, and the Connect plugin.') . ' ' . _('Get started by installing the plugin.'),
|
||||
'Checkout the Connect Documentation' => _('Checkout the Connect Documentation'),
|
||||
'No thanks' => _('No thanks'),
|
||||
'Learn more' => _('Learn more'),
|
||||
'Install Connect' => _('Install Connect'),
|
||||
'Close Modal' => _('Close Modal'),
|
||||
'Close' => _('Close'),
|
||||
'Reload' => _('Reload'),
|
||||
'Unraid logo animating with a wave like effect' => _('Unraid logo animating with a wave like effect'),
|
||||
'Click to close modal' => _('Click to close modal'),
|
||||
'Error' => _('Error'),
|
||||
'Performing actions' => _('Performing actions'),
|
||||
'Success!' => _('Success!'),
|
||||
'Something went wrong' => _('Something went wrong'),
|
||||
'Please keep this window open while we perform some actions' => _('Please keep this window open while we perform some actions'),
|
||||
'You\'re one step closer to enhancing your Unraid experience' => _('You\'re one step closer to enhancing your Unraid experience'),
|
||||
'Thank you for purchasing an Unraid {0} Key!' => sprintf(_('Thank you for purchasing an Unraid %s Key!'), '{0}'),
|
||||
'Your {0} Key has been replaced!' => sprintf(_('Your %s Key has been replaced!'), '{0}'),
|
||||
'Your Trial key has been extended!' => _('Your Trial key has been extended!'),
|
||||
'Copied' => _('Copied'),
|
||||
'Copy Key URL' => _('Copy Key URL'),
|
||||
'Copy your Key URL: {0}' => sprintf(_('Copy your Key URL: %s'), '{0}'),
|
||||
'Then go to Tools > Registration to manually install it' => _('Then go to Tools > Registration to manually install it'),
|
||||
'Enhance your experience with Unraid Connect' => _('Enhance your experience with Unraid Connect'),
|
||||
'Sign In to utilize Unraid Connect' => _('Sign In to utilize Unraid Connect'),
|
||||
'Configure Connect Features' => _('Configure Connect Features'),
|
||||
'The primary method of support for Unraid Connect is through our forums and Discord.' => _('The primary method of support for Unraid Connect is through our forums and Discord.'),
|
||||
'If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.' => _('If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.'),
|
||||
'The logs may contain sensitive information so do not post them publicly.' => _('The logs may contain sensitive information so do not post them publicly.'),
|
||||
'Download unraid-api Logs' => _('Download unraid-api Logs'),
|
||||
'Unraid Connect Forums' => _('Unraid Connect Forums'),
|
||||
'Unraid Discord' => _('Unraid Discord'),
|
||||
'Unraid Contact Page' => _('Unraid Contact Page'),
|
||||
'DNS issue, unable to resolve wanip4.unraid.net' => _('DNS issue, unable to resolve wanip4.unraid.net'),
|
||||
'Unable to fetch client WAN IPv4' => _('Unable to fetch client WAN IPv4'),
|
||||
'Checking WAN IPs…' => _('Checking WAN IPs…'),
|
||||
'Remark: your WAN IPv4 is {0}' => sprintf(_('Remark: your WAN IPv4 is %s'), '{0}'),
|
||||
'Remark: Unraid\'s WAN IPv4 {0} does not match your client\'s WAN IPv4 {1}.' => sprintf(_('Remark: Unraid\'s WAN IPv4 %1s does not match your client\'s WAN IPv4 %2s.'), '{0}', '{1}'),
|
||||
'This may indicate a complex network that will not work with this Remote Access solution.' => _('This may indicate a complex network that will not work with this Remote Access solution.'),
|
||||
'Ignore this message if you are currently connected via Remote Access or VPN.' => _('Ignore this message if you are currently connected via Remote Access or VPN.'),
|
||||
'Ready to update Connect account configuration' => _('Ready to update Connect account configuration'),
|
||||
'Signing in {0}…' => sprintf(_('Signing in %s…'), '{0}'),
|
||||
'Signing out {0}…' => sprintf(_('Signing out %s…'), '{0}'),
|
||||
'{0} Signed In Successfully' => sprintf(_('%s Signed In Successfully'), '{0}'),
|
||||
'{0} Signed Out Successfully' => sprintf(_('%s Signed Out Successfully'), '{0}'),
|
||||
'Sign In Failed' => _('Sign In Failed'),
|
||||
'Sign Out Failed' => _('Sign Out Failed'),
|
||||
'Failed to update Connect account configuration' => _('Failed to update Connect account configuration'),
|
||||
'Callback redirect type not present or incorrect' => _('Callback redirect type not present or incorrect'),
|
||||
'Failed to install key' => _('Failed to install key'),
|
||||
'Ready to Install Key' => _('Ready to Install Key'),
|
||||
'Installing Extended Trial' => _('Installing Extended Trial'),
|
||||
'Installing Recovered' => _('Installing Recovered'),
|
||||
'Installing Replaced' => _('Installing Replaced'),
|
||||
'{0} {1} Key…' => sprintf(_('%1s %2s Key…'), '{0}', '{1}'),
|
||||
'{1} Key {0} Successfully' => sprintf(_('%2s Key %1s Successfully'), '{0}', '{1}'),
|
||||
'Failed to {0} {1} Key' => sprintf(_('Failed to %1s %2s Key'), '{0}', '{1}'),
|
||||
'Purchase Key' => _('Purchase Key'),
|
||||
'Upgrade Key' => _('Upgrade Key'),
|
||||
'Recover Key' => _('Recover Key'),
|
||||
'Redeem Activation Code' => _('Redeem Activation Code'),
|
||||
'Replace Key' => _('Replace Key'),
|
||||
'Sign In with Unraid.net Account' => _('Sign In with Unraid.net Account'),
|
||||
'Sign Out of Unraid.net' => _('Sign Out of Unraid.net'),
|
||||
'Extend Trial' => _('Extend Trial'),
|
||||
'Start Free 30 Day Trial' => _('Start Free 30 Day Trial'),
|
||||
'Go to Management Access Now' => _('Go to Management Access Now'),
|
||||
'Contact Support' => _('Contact Support'),
|
||||
'Learn More' => _('Learn More'),
|
||||
'No Keyfile' => _('No Keyfile'),
|
||||
'Let\'s Unleash your Hardware!' => _('Let\'s Unleash your Hardware!'),
|
||||
'<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class="list-disc pl-16px"><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>' => '<p>' . _('Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key.') . ' ' . _('A <em>Trial</em> key provides all the functionality of a Pro Registration key.') . '</p><p>' . _('Registration keys are bound to your USB Flash boot device serial number (GUID).') . ' ' . _('Please use a high quality name brand device at least 1GB in size.') . '</p><p>' . _('Note: USB memory card readers are generally not supported because most do not present unique serial numbers.') . '</p><p>' . _('*Important:*') . '</p><ul class="list-disc pl-16px"><li>' . _('Please make sure your server time is accurate to within 5 minutes') . '</li><li>' . _('Please make sure there is a DNS server specified') . '</li>>' . '</ul>',
|
||||
'Trial' => _('Trial'),
|
||||
'Thank you for choosing Unraid OS!' => _('Thank you for choosing Unraid OS!'),
|
||||
'<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>' => '<p>' . _('Your **Trial** key includes all the functionality and device support of a **Pro** key') . '</p><p>' . _('After your **Trial** has reached expiration, your server *still functions normally* until the next time you Stop the array or reboot your server') . '</p><p>' . _('At that point you may either purchase a license key or request a *Trial* extension.') . '</p>',
|
||||
'Trial Expired' => _('Trial Expired'),
|
||||
'Your Trial has expired' => _('Your Trial has expired'),
|
||||
'<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>' => '<p>' . _('To continue using Unraid OS you may purchase a license key.') . ' ' . _('Alternately, you may request a Trial extension.') . '</p>',
|
||||
'<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>' => '<p>' . _('You have used all your Trial extensions.') . ' ' . _('To continue using Unraid OS you may purchase a license key.') . '</p>',
|
||||
'Basic' => _('Basic'),
|
||||
'<p>Register for Connect by signing in to your Unraid.net account</p>' => '<p>' . _('Register for Connect by signing in to your Unraid.net account') . '</p>',
|
||||
'<p>To support more storage devices as your server grows, click Upgrade Key.</p>' => '<p>' . _('To support more storage devices as your server grows, click Upgrade Key.') . '</p>',
|
||||
'Plus' => _('Plus'),
|
||||
'Pro' => _('Pro'),
|
||||
'Flash GUID Error' => _('Flash GUID Error'),
|
||||
'Registration key / USB Flash GUID mismatch' => _('Registration key / USB Flash GUID mismatch'),
|
||||
'<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>' => '<p>' . _('Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.') . '</p>',
|
||||
'<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>' => '<p>' . _('The license key file does not correspond to the USB Flash boot device.') . ' ' . _('Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key') . '</p><p>' . _('Your Unraid registration key is ineligible for replacement as it is blacklisted.') . '</p>',
|
||||
'<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>' => '<p>' . _('The license key file does not correspond to the USB Flash boot device.') . ' ' . _('Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key') . '</p><p>' . _('Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.') . '</p>',
|
||||
'<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>' => '<p>' . _('The license key file does not correspond to the USB Flash boot device.') . ' ' . _('Please copy the correct key file to the /config directory on your USB Flash boot device') . '</p><p>' . _('You may also attempt to Purchase or Replace your key.') . '</p>',
|
||||
'Multiple License Keys Present' => _('Multiple License Keys Present'),
|
||||
'<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>' => '<p>' . _('There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device.') . ' ' . _('Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device') . '</p><p>' . _('Alternately you may purchase a license key for this USB flash device') . '</p><p>' . _('If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.') . '</p>',
|
||||
'Missing key file' => _('Missing key file'),
|
||||
'<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>' => '<p>' . _('Your license key file is corrupted or missing.') . ' ' . _('The key file should be located in the /config directory on your USB Flash boot device') . '</p><p>' . _('If you do not have a backup copy of your license key file you may attempt to recover your key with your Unraid.net account') . '</p><p>' . _('If this was an expired Trial installation, you may purchase a license key.') . '</p>',
|
||||
'<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>' => '<p>' . _('Your license key file is corrupted or missing.') . ' ' . _('The key file should be located in the /config directory on your USB Flash boot device') . '</p><p>' . _('If you do not have a backup copy of your license key file you may attempt to recover your key with your Unraid.net account') . '</p><p>' . _('If this was an expired Trial installation, you may purchase a license key.') . '</p>',
|
||||
'Invalid installation' => _('Invalid installation'),
|
||||
'<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>' => '<p>' . _('It is not possible to use a Trial key with an existing Unraid OS installation') . '</p><p>' . _('You may purchase a license key corresponding to this USB Flash device to continue using this installation.') . '</p>',
|
||||
|
||||
'No USB flash configuration data' => _('No USB flash configuration data'),
|
||||
'<p>There is a problem with your USB Flash device</p>' => '<p>' . _('There is a problem with your USB Flash device') . '</p>',
|
||||
'No Flash' => _('No Flash'),
|
||||
'Cannot access your USB Flash boot device' => _('Cannot access your USB Flash boot device'),
|
||||
'<p>There is a physical problem accessing your USB Flash boot device</p>' => '<p>' . _('There is a physical problem accessing your USB Flash boot device') . '</p>',
|
||||
'BLACKLISTED' => _('BLACKLISTED'),
|
||||
'Blacklisted USB Flash GUID' => _('Blacklisted USB Flash GUID'),
|
||||
'<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique – this is common with USB card readers.</p>' => '<p>' . _('This USB Flash boot device has been blacklisted.') . ' ' . _('This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.') . '</p><p>' . _('A USB Flash device may also be blacklisted if we discover the serial number is not unique – this is common with USB card readers.') . '</p>',
|
||||
'USB Flash device error' => _('USB Flash device error'),
|
||||
'<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>' => '<p>' . _('This USB Flash device has an invalid GUID. Please try a different USB Flash device') . '</p>',
|
||||
'USB Flash has no serial number' => _('USB Flash has no serial number'),
|
||||
'Trial Requires Internet Connection' => _('Trial Requires Internet Connection'),
|
||||
'Cannot validate Unraid Trial key' => _('Cannot validate Unraid Trial key'),
|
||||
'<p>Your Trial key requires an internet connection.</p><p><a href="/Settings/NetworkSettings" class="underline">Please check Settings > Network</a></p>' => '<p>' . _('Your Trial key requires an internet connection') . '</p><p><a href="/Settings/NetworkSettings" class="underline">' . _('Please check Settings > Network') . '</a></p>',
|
||||
'Stale' => _('Stale'),
|
||||
'Stale Server' => _('Stale Server'),
|
||||
'<p>Please refresh the page to ensure you load your latest configuration</p>' => '<p>' . _('Please refresh the page to ensure you load your latest configuration') . '</p>',
|
||||
'Invalid API Key' => _('Invalid API Key'),
|
||||
'Please sign out then sign back in to refresh your API key.' => _('Please sign out then sign back in to refresh your API key.'),
|
||||
'Invalid API Key Format' => _('Invalid API Key Format'),
|
||||
'Too Many Devices' => _('Too Many Devices'),
|
||||
'You have exceeded the number of devices allowed for your license. Please remove a device before adding another.' => _('You have exceeded the number of devices allowed for your license. Please remove a device before adding another.'),
|
||||
'Unraid Connect Install Failed' => _('Unraid Connect Install Failed'),
|
||||
'Rebooting will likely solve this.' => _('Rebooting will likely solve this.'),
|
||||
'SSL certificates for unraid.net deprecated' => _('SSL certificates for unraid.net deprecated'),
|
||||
'On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.' => _('On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.'),
|
||||
'Unraid Connect Error' => _('Unraid Connect Error'),
|
||||
'Trial Key Creation Failed' => _('Trial Key Creation Failed'),
|
||||
'Error creatiing a trial key. Please try again later.' => _('Error creatiing a trial key. Please try again later.'),
|
||||
'Extending your free trial by 15 days' => _('Extending your free trial by 15 days'),
|
||||
'Please keep this window open' => _('Please keep this window open'),
|
||||
'Starting your free 30 day trial' => _('Starting your free 30 day trial'),
|
||||
'Trial Key Created' => _('Trial Key Created'),
|
||||
'Please wait while the page reloads to install your trial key' => _('Please wait while the page reloads to install your trial key'),
|
||||
'A Trial key provides all the functionality of a Pro Registration key' => _('A Trial key provides all the functionality of a Pro Registration key'),
|
||||
'Extension Installed' => _('Extension Installed'),
|
||||
'Recovered' => _('Recovered'),
|
||||
'Replaced' => _('Replaced'),
|
||||
'Installing' => _('Installing'),
|
||||
'Installed' => _('Installed'),
|
||||
'Install' => _('Install'),
|
||||
'Install Extended' => _('Install Extended'),
|
||||
'Install Recovered' => _('Install Recovered'),
|
||||
'Install Replaced' => _('Install Replaced'),
|
||||
'Your free Trial key provides all the functionality of a Pro Registration key' => _('Your free Trial key provides all the functionality of a Pro Registration key'),
|
||||
],
|
||||
];
|
||||
4
web/.env.example
Normal file
4
web/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
VITE_ACCOUNT=https://localhost:8008
|
||||
VITE_CONNECT=https://connect.myunraid.net
|
||||
VITE_UNRAID_NET=https://unraid.ddev.site
|
||||
VITE_CALLBACK_KEY=aNotSoSecretKeyUsedToObfuscateQueryParams
|
||||
25
web/.eslintrc.cjs
Normal file
25
web/.eslintrc.cjs
Normal file
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { readFileSync } = require('fs');
|
||||
const { parse } = require('dotenv');
|
||||
|
||||
const envConfig = parse(readFileSync('.env'));
|
||||
for (const k in envConfig) {
|
||||
process.env[k] = envConfig[k];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extends: ['@nuxtjs/eslint-config-typescript'],
|
||||
ignorePatterns: ['composables/gql/'],
|
||||
rules: {
|
||||
'comma-dangle': ['warn', 'only-multiline'],
|
||||
semi: ['error', 'always'],
|
||||
quotes: ['warn', 'single'],
|
||||
'no-console': (process.env.NODE_ENV === 'production' ? 'error' : 'off'),
|
||||
'no-debugger': (process.env.NODE_ENV === 'production' ? 'error' : 'off'),
|
||||
'@typescript-eslint/no-unused-vars': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
|
||||
'max-len': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/v-on-event-hyphenation': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
}
|
||||
};
|
||||
2
web/.npmrc
Normal file
2
web/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
1
web/.nvmrc
Normal file
1
web/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
18.16.0
|
||||
43
web/README.md
Normal file
43
web/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# connect-components via Nuxt 3
|
||||
|
||||
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:4321`
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
|
||||
## Build for Unraid webgui [@TODO]
|
||||
Instructions to come
|
||||
|
||||
## Interfacing with `unraid-api`
|
||||
|
||||
@todo https://v4.apollo.vuejs.org/guide-composable/fragments.html#colocating-fragments
|
||||
78
web/_data/serverState.ts
Normal file
78
web/_data/serverState.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Server, ServerState } from '~/types/server';
|
||||
|
||||
function makeid (length: number) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
|
||||
const charactersLength = characters.length;
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); }
|
||||
return result;
|
||||
}
|
||||
|
||||
const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
|
||||
// const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered
|
||||
// const regWizTime = `1616711990500_${randomGuid}`;
|
||||
// const blacklistedGuid = '154B-00EE-0700-9B50CF819816';
|
||||
|
||||
// ENOKEYFILE
|
||||
// TRIAL
|
||||
// BASIC
|
||||
// PLUS
|
||||
// PRO
|
||||
// EEXPIRED
|
||||
// EGUID
|
||||
// EGUID1
|
||||
// ETRIAL
|
||||
// ENOKEYFILE2
|
||||
// ENOKEYFILE1
|
||||
// ENOFLASH
|
||||
// EBLACKLISTED
|
||||
// EBLACKLISTED1
|
||||
// EBLACKLISTED2
|
||||
// ENOCONN
|
||||
const state: ServerState = 'TRIAL';
|
||||
|
||||
const uptime = Date.now() - 60 * 60 * 1000; // 1 hour ago
|
||||
let expireTime = 0;
|
||||
if (state === 'TRIAL') { expireTime = Date.now() + 60 * 60 * 1000; } // in 1 hour
|
||||
if (state === 'EEXPIRED') { expireTime = uptime; } // 1 hour ago
|
||||
|
||||
export const serverState: Server = {
|
||||
apiKey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
|
||||
avatar: 'https://source.unsplash.com/300x300/?portrait',
|
||||
config: {
|
||||
// error: 'INVALID',
|
||||
valid: true,
|
||||
},
|
||||
description: 'DevServer9000',
|
||||
deviceCount: 3,
|
||||
expireTime,
|
||||
flashProduct: 'SanDisk_3.2Gen1',
|
||||
flashVendor: 'USB',
|
||||
guid: randomGuid,
|
||||
// "guid": "0781-5583-8355-81071A2B0211",
|
||||
inIframe: false,
|
||||
keyfile: 'DUMMY_KEYFILE',
|
||||
lanIp: '192.168.254.36',
|
||||
license: '',
|
||||
locale: 'en_US', // en_US, ja
|
||||
name: 'fuji',
|
||||
// connectPluginInstalled: 'dynamix.unraid.net.staging.plg',
|
||||
connectPluginInstalled: '',
|
||||
registered: true,
|
||||
regGen: 0,
|
||||
// "regGuid": "0781-5583-8355-81071A2B0211",
|
||||
site: 'http://localhost:4321',
|
||||
state,
|
||||
theme: {
|
||||
banner: false,
|
||||
bannerGradient: false,
|
||||
bgColor: '',
|
||||
descriptionShow: true,
|
||||
metaColor: '',
|
||||
name: 'black',
|
||||
textColor: ''
|
||||
},
|
||||
uptime,
|
||||
username: 'zspearmint',
|
||||
wanFQDN: ''
|
||||
};
|
||||
9
web/_webGui/setup.sh
Normal file
9
web/_webGui/setup.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
cd /usr/local/emhttp/plugins/dynamix.my.servers/ || exit
|
||||
# replace Connect.page
|
||||
# replace includes/myservers{1|2}.php
|
||||
mkdir webComponents
|
||||
mkdir webComponents/_nuxt
|
||||
cd webComponents || exit
|
||||
touch manifest.json
|
||||
# create _nuxt/{filename}.js
|
||||
159
web/_webGui/testWebComponents.page
Normal file
159
web/_webGui/testWebComponents.page
Normal file
@@ -0,0 +1,159 @@
|
||||
Menu="ManagementAccess:200"
|
||||
Title="Callback Tests"
|
||||
Icon="icon-u-globe"
|
||||
Tag="globe"
|
||||
---
|
||||
<?php
|
||||
/**
|
||||
* @todo create web component env switcher liker upcEnv(). If we utilize manifest.json then we'll be switching its path.
|
||||
*/
|
||||
$myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg';
|
||||
$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : [];
|
||||
// print_r($mystatus);
|
||||
// extract web component JS file from manifest
|
||||
$jsonManifest = file_get_contents('/usr/local/emhttp/plugins/dynamix.my.servers/webComponents/manifest.json');
|
||||
$jsonManifestData = json_decode($jsonManifest, true);
|
||||
$webComponentJsFile = $jsonManifestData["connect-components.client.mjs"]["file"];
|
||||
// web component
|
||||
$localSourceBasePath = '/plugins/dynamix.my.servers/webComponents/';
|
||||
$localSourceJs = $localSourceBasePath . $webComponentJsFile;
|
||||
// add the web component source to the DOM
|
||||
echo '<script id="unraid-webcomponents" defer src="' . $localSourceJs . '"></script>';
|
||||
|
||||
/**
|
||||
* Build vars for user profile prop
|
||||
*/
|
||||
// add 'ipaddr' function for 6.9 backwards compatibility
|
||||
if (!function_exists('ipaddr')) {
|
||||
function ipaddr($ethX='eth0', $prot=4) {
|
||||
global $$ethX;
|
||||
switch ($$ethX['PROTOCOL:0']) {
|
||||
case 'ipv4':
|
||||
return $$ethX['IPADDR:0'];
|
||||
case 'ipv6':
|
||||
return $$ethX['IPADDR6:0'];
|
||||
case 'ipv4+ipv6':
|
||||
switch ($prot) {
|
||||
case 4: return $$ethX['IPADDR:0'];
|
||||
case 6: return $$ethX['IPADDR6:0'];
|
||||
default:return [$$ethX['IPADDR:0'],$$ethX['IPADDR6:0']];}
|
||||
default:
|
||||
return $$ethX['IPADDR:0'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$configErrorEnum = [ // used to map $var['configValid'] value to mimic unraid-api's `configError` ENUM
|
||||
"error" => 'UNKNOWN_ERROR',
|
||||
"invalid" => 'INVALID',
|
||||
"nokeyserver" => 'NO_KEY_SERVER',
|
||||
"withdrawn" => 'WITHDRAWN',
|
||||
];
|
||||
// read flashbackup ini file
|
||||
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
|
||||
$flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
|
||||
|
||||
$nginx = parse_ini_file('/var/local/emhttp/nginx.ini');
|
||||
|
||||
$connectPluginInstalled = '';
|
||||
if (!file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net') && !file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) {
|
||||
$connectPluginInstalled = ''; // base OS only, plugin not installed • show ad for plugin
|
||||
} else {
|
||||
// plugin is installed but if the unraid-api file doesn't fully install it's a failed install
|
||||
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net')) $connectPluginInstalled = 'dynamix.unraid.net.plg';
|
||||
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) $connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
|
||||
// plugin install failed • append failure detected so we can show warning about failed install via UPC
|
||||
if (!file_exists('/usr/local/sbin/unraid-api')) $connectPluginInstalled = $connectPluginInstalled . '_installFailed';
|
||||
}
|
||||
|
||||
$serverData = [
|
||||
"apiKey" => $myservers['upc']['apikey'] ?? '',
|
||||
"apiVersion" => $myservers['api']['version'] ?? '',
|
||||
"avatar" => (!empty($myservers['remote']['avatar']) && $connectPluginInstalled) ? $myservers['remote']['avatar'] : '',
|
||||
"banner" => $display['banner'] ?? '',
|
||||
"bannerGradient" => $display['showBannerGradient'] ?? 'yes',
|
||||
"bgColor" => ($backgnd) ? '#'.$backgnd : '',
|
||||
"config" => [
|
||||
'valid' => $var['configValid'] === 'yes',
|
||||
'error' => $var['configValid'] !== 'yes'
|
||||
? (array_key_exists($var['configValid'], $configErrorEnum) ? $configErrorEnum[$var['configValid']] : 'UNKNOWN_ERROR')
|
||||
: null,
|
||||
],
|
||||
"csrf" => $var['csrf_token'],
|
||||
"description" => $var['COMMENT'],
|
||||
"descriptionShow" => ($display['headerdescription']??''!='no') ? 'true' : '',
|
||||
"deviceCount" => $var['deviceCount'],
|
||||
"email" => $myservers['remote']['email'] ?? '',
|
||||
"expireTime" => 1000*($var['regTy']=='Trial'||strstr($var['regTy'],'expired')?$var['regTm2']:0),
|
||||
"extraOrigins" => explode(',', $myservers['api']['extraOrigins']??''),
|
||||
"flashProduct" => $var['flashProduct'],
|
||||
"flashVendor" => $var['flashVendor'],
|
||||
"flashBackupActivated" => empty($flashbackup_status['activated']) ? '' : 'true',
|
||||
"guid" => $var['flashGUID'],
|
||||
"hasRemoteApikey" => !empty($myservers['remote']['apikey']),
|
||||
"lanIp" => ipaddr(),
|
||||
"internalPort" => $_SERVER['SERVER_PORT'],
|
||||
"keyfile" => empty($var['regFILE'])? "" : str_replace(['+','/','='], ['-','_',''], trim(base64_encode(@file_get_contents($var['regFILE'])))),
|
||||
"locale" => ($_SESSION['locale']) ? $_SESSION['locale'] : 'en_US',
|
||||
"metaColor" => ($display['headermetacolor']??'') ? '#'.$display['headermetacolor'] : '',
|
||||
"model" => $var['SYS_MODEL'],
|
||||
"name" => $var['NAME'],
|
||||
"osVersion" => $var['version'],
|
||||
"plgVersion" => $plgversion = file_exists('/var/log/plugins/dynamix.unraid.net.plg')
|
||||
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null'))
|
||||
: ( file_exists('/var/log/plugins/dynamix.unraid.net.staging.plg')
|
||||
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null'))
|
||||
: 'base-'.$var['version'] ),
|
||||
"connectPluginInstalled" => $connectPluginInstalled,
|
||||
"protocol" => $_SERVER['REQUEST_SCHEME'],
|
||||
"regGen" => (int)$var['regGen'],
|
||||
"regGuid" => $var['regGUID'],
|
||||
"registered" => (!empty($myservers['remote']['username']) && $connectPluginInstalled),
|
||||
"registeredTime" => $myservers['remote']['regWizTime'] ?? '',
|
||||
"site" => $_SERVER['REQUEST_SCHEME']."://".$_SERVER['HTTP_HOST'],
|
||||
"state" => strtoupper(empty($var['regCheck']) ? $var['regTy'] : $var['regCheck']),
|
||||
"textColor" => ($header) ? '#'.$header : '',
|
||||
"theme" => $display['theme'],
|
||||
"ts" => time(),
|
||||
"uptime" => 1000*(time() - round(strtok(exec("cat /proc/uptime"),' '))),
|
||||
"username" => (!empty($myservers['remote']['username']) && $connectPluginInstalled) ? $myservers['remote']['username'] : '',
|
||||
"wanFQDN" => $nginx['NGINX_WANFQDN'] ?? '',
|
||||
];
|
||||
|
||||
$themeBg = '#111';
|
||||
if ($display['theme'] === 'black' || $display['theme'] === 'azure') {
|
||||
$themeBg = '#fff';
|
||||
}
|
||||
?>
|
||||
<style>
|
||||
.ComponentWrapper {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
<div class="ComponentWrapper" style="background-color: <?=$themeBg?>;">
|
||||
<?="<unraid-user-profile server='" . json_encode($serverData) . "'></connect-user-profile>"?>
|
||||
</div>
|
||||
<div class="ComponentWrapper">
|
||||
<unraid-auth></connect-auth>
|
||||
</div>
|
||||
<div class="ComponentWrapper">
|
||||
<unraid-download-api-logs></connect-download-api-logs>
|
||||
</div>
|
||||
<div class="ComponentWrapper">
|
||||
<unraid-key-actions></connect-key-actions>
|
||||
</div>
|
||||
<div class="ComponentWrapper">
|
||||
<unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></connect-wan-ip-check>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* So we're not needing to modify DefaultLayout with an additional include,
|
||||
* we'll add the Modals web component to the bottom of the body
|
||||
*/
|
||||
const modalsWebComponent = 'connect-modals';
|
||||
if (!document.getElementsByTagName(modalsWebComponent).length) {
|
||||
const $body = document.getElementsByTagName('body')[0];
|
||||
const $modals = document.createElement(modalsWebComponent);
|
||||
$body.appendChild($modals);
|
||||
}
|
||||
</script>
|
||||
12
web/app.vue
Normal file
12
web/app.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
const nuxtApp = useNuxtApp();
|
||||
onBeforeMount(() => {
|
||||
nuxtApp.$customElements.registerEntry('UnraidComponents');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
28
web/assets/main.css
Normal file
28
web/assets/main.css
Normal file
@@ -0,0 +1,28 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
/*
|
||||
darkTheme
|
||||
alpha: '#1c1b1b',
|
||||
beta: '#f2f2f2',
|
||||
gamma: '#999999',
|
||||
|
||||
lightTheme
|
||||
alpha: '#f2f2f2',
|
||||
beta: '#1c1b1b',
|
||||
gamma: '#999999',
|
||||
*/
|
||||
|
||||
body {
|
||||
--color-alpha: #1c1b1b;
|
||||
--color-beta: #f2f2f2;
|
||||
--color-gamma: #999999;
|
||||
--color-customgradient-start: rgba(242, 242, 242, .0);
|
||||
--color-customgradient-end: rgba(242, 242, 242, .85);
|
||||
--shadow-beta: 0 25px 50px -12px rgba(242, 242, 242, .15);
|
||||
--ring-offset-shadow: 0 0 --var(--color-beta);
|
||||
--ring-shadow: 0 0 --var(--color-beta);
|
||||
}
|
||||
|
||||
/* Ensure this is always at the bottom – @see https://tailwindcss.com/docs/content-configuration#working-with-third-party-libraries */
|
||||
@tailwind utilities;
|
||||
47
web/codegen.ts
Normal file
47
web/codegen.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { CodegenConfig } from '@graphql-codegen/cli';
|
||||
|
||||
const config: CodegenConfig = {
|
||||
overwrite: true,
|
||||
documents: ['./**/**/*.ts'],
|
||||
ignoreNoDocuments: false,
|
||||
config: {
|
||||
namingConvention: {
|
||||
typeNames: './fix-array-type.ts',
|
||||
},
|
||||
scalars: {
|
||||
DateTime: 'string',
|
||||
Long: 'number',
|
||||
JSON: 'string',
|
||||
URL: 'URL',
|
||||
Port: 'number',
|
||||
UUID: 'string',
|
||||
},
|
||||
},
|
||||
generates: {
|
||||
'composables/gql/': {
|
||||
preset: 'client',
|
||||
config: {
|
||||
useTypeImports: true,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
'http://localhost:3001/graphql': {
|
||||
headers: {
|
||||
origin: '/var/run/unraid-php.sock',
|
||||
'x-api-key': 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
add: {
|
||||
content: '/* eslint-disable */',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
36
web/components/Auth.ce.vue
Normal file
36
web/components/Auth.ce.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useServerStore } from '~/store/server';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const serverStore = useServerStore();
|
||||
const { authAction, stateData } = storeToRefs(serverStore);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="whitespace-normal flex flex-col gap-y-16px max-w-3xl">
|
||||
<span v-if="stateData.error" class="text-unraid-red font-semibold">
|
||||
<h3 class="text-16px mb-8px">{{ t(stateData.heading) }}</h3>
|
||||
<span class="text-14px" v-html="t(stateData.message)" />
|
||||
</span>
|
||||
<span v-if="authAction">
|
||||
<BrandButton
|
||||
:disabled="authAction?.disabled"
|
||||
:icon="authAction.icon"
|
||||
:text="t(authAction.text)"
|
||||
@click="authAction.click()"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
31
web/components/Brand/Avatar.vue
Normal file
31
web/components/Brand/Avatar.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
export interface Props {
|
||||
gradientStart?: string;
|
||||
gradientStop?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
});
|
||||
|
||||
const serverStore = useServerStore();
|
||||
const { avatar, connectPluginInstalled, registered, username } = storeToRefs(serverStore);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<figure class="group relative z-0 flex items-center justify-center w-36px h-36px rounded-full bg-gradient-to-r from-unraid-red to-orange">
|
||||
<img
|
||||
v-if="avatar && connectPluginInstalled && registered"
|
||||
:src="avatar"
|
||||
:alt="username"
|
||||
class="absolute z-10 inset-0 w-36px h-36px rounded-full overflow-hidden"
|
||||
>
|
||||
<template v-else>
|
||||
<BrandMark gradient-start="#fff" gradient-stop="#fff" class="opacity-100 absolute z-10 w-36px px-4px" />
|
||||
</template>
|
||||
</figure>
|
||||
</template>
|
||||
52
web/components/Brand/Button.vue
Normal file
52
web/components/Brand/Button.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
||||
export interface ButtonProps {
|
||||
btnStyle?: 'fill' | 'outline' | 'underline';
|
||||
btnType?: 'button' | 'submit' | 'reset';
|
||||
click?: () => void;
|
||||
disabled?: boolean;
|
||||
download?: boolean;
|
||||
external?: boolean;
|
||||
href?: string;
|
||||
icon?: typeof XCircleIcon;
|
||||
text?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
btnStyle: 'fill',
|
||||
btnType: 'button',
|
||||
click: undefined,
|
||||
href: undefined,
|
||||
icon: undefined,
|
||||
text: undefined,
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
|
||||
const classes = computed(() => {
|
||||
switch (props.btnStyle) {
|
||||
case 'fill':
|
||||
return 'text-white bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60';
|
||||
case 'outline':
|
||||
return 'text-orange bg-gradient-to-r from-transparent to-transparent border-2 border-solid border-orange hover:text-white focus:text-white hover:from-unraid-red hover:to-orange focus:from-unraid-red focus:to-orange hover:border-transparent focus:border-transparent';
|
||||
case 'underline':
|
||||
return 'opacity-75 hover:opacity-100 focus:opacity-100 underline transition hover:text-alpha hover:bg-beta focus:text-alpha focus:bg-beta';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="href ? 'a' : 'button'"
|
||||
:disabled="disabled ?? null"
|
||||
:href="href"
|
||||
:rel="external ? 'noopener noreferrer' : ''"
|
||||
:target="external ? '_blank' : ''"
|
||||
:type="!href ? btnType : ''"
|
||||
class="text-14px text-center font-semibold flex-none flex flex-row items-center justify-center gap-x-8px px-8px py-8px cursor-pointer rounded-md disabled:opacity-50 disabled:hover:opacity-50 disabled:focus:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="classes"
|
||||
@click="click ?? $emit('click')"
|
||||
>
|
||||
<component :is="icon" v-if="icon" class="flex-shrink-0 w-14px" />
|
||||
{{ text }}
|
||||
</component>
|
||||
</template>
|
||||
142
web/components/Brand/Loading.vue
Normal file
142
web/components/Brand/Loading.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
export interface Props {
|
||||
gradientStart?: string;
|
||||
gradientStop?: string;
|
||||
title?: string,
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
title: 'Loading',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 133.52 76.97"
|
||||
:class="`unraid_mark`"
|
||||
role="img"
|
||||
>
|
||||
<title>{{ title }}</title>
|
||||
<desc>Unraid logo animating with a wave like effect</desc>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="unraidLoadingGradient"
|
||||
x1="23.76"
|
||||
y1="81.49"
|
||||
x2="109.76"
|
||||
y2="-4.51"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" :stop-color="gradientStart" />
|
||||
<stop offset="1" :stop-color="gradientStop" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="m70,19.24zm57,0l6.54,0l0,38.49l-6.54,0l0,-38.49z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_9"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm47.65,11.9l-6.55,0l0,-23.79l6.55,0l0,23.79z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_8"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm31.77,-4.54l-6.54,0l0,-14.7l6.54,0l0,14.7z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_7"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm15.9,11.9l-6.54,0l0,-23.79l6.54,0l0,23.79z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_6"
|
||||
/>
|
||||
<path
|
||||
d="m63.49,19.24l6.51,0l0,38.49l-6.51,0l0,-38.49z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_5"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm-22.38,26.6l6.54,0l0,23.78l-6.54,0l0,-23.78z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_4"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm-38.26,43.03l6.55,0l0,14.73l-6.55,0l0,-14.73z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_3"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm-54.13,26.6l6.54,0l0,23.78l-6.54,0l0,-23.78z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_2"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm-63.46,38.49l-6.54,0l0,-38.49l6.54,0l0,38.49z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_1"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
.unraid_mark_2,
|
||||
.unraid_mark_4 {
|
||||
animation: mark_2 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_3 {
|
||||
animation: mark_3 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_6,
|
||||
.unraid_mark_8 {
|
||||
animation: mark_6 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_7 {
|
||||
animation: mark_7 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes mark_2 {
|
||||
50% {
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_3 {
|
||||
50% {
|
||||
transform: translateY(-62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_6 {
|
||||
50% {
|
||||
transform: translateY(40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_7 {
|
||||
50% {
|
||||
transform: translateY(62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
38
web/components/Brand/Logo.vue
Normal file
38
web/components/Brand/Logo.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
gradientStart?: string;
|
||||
gradientStop?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 222.36 39.04"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="unraidLogo"
|
||||
x1="47.53"
|
||||
y1="79.1"
|
||||
x2="170.71"
|
||||
y2="-44.08"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" :stop-color="gradientStart" />
|
||||
<stop offset="1" :stop-color="gradientStop" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<title>Unraid Logo</title>
|
||||
<path
|
||||
d="M146.7,29.47H135l-3,9h-6.49L138.93,0h8l13.41,38.49h-7.09L142.62,6.93l-5.83,16.88h8ZM29.69,0V25.4c0,8.91-5.77,13.64-14.9,13.64S0,34.31,0,25.4V0H6.54V25.4c0,5.17,3.19,7.92,8.25,7.92s8.36-2.75,8.36-7.92V0ZM50.86,12v26.5H44.31V0h6.11l17,26.5V0H74V38.49H67.9ZM171.29,0h6.54V38.49h-6.54Zm51.07,24.69c0,9-5.88,13.8-15.17,13.8H192.67V0H207.3c9.18,0,15.06,4.78,15.06,13.8ZM215.82,13.8c0-5.28-3.3-8.14-8.52-8.14h-8.08V32.77h8c5.33,0,8.63-2.8,8.63-8.08ZM108.31,23.92c4.34-1.6,6.93-5.28,6.93-11.55C115.24,3.68,110.18,0,102.48,0H88.84V38.49h6.55V5.66h6.87c3.8,0,6.21,1.82,6.21,6.71s-2.41,6.76-6.21,6.76H98.88l9.21,19.36h7.53Z"
|
||||
fill="url(#unraidLogo)"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
44
web/components/Brand/LogoConnect.vue
Normal file
44
web/components/Brand/LogoConnect.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
gradientStart?: string;
|
||||
gradientStop?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" data-name="Layer 1" viewBox="0 0 954.29 142.4">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="a"
|
||||
x1="-57.82"
|
||||
x2="923.39"
|
||||
y1="71.2"
|
||||
y2="71.2"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" :stop-color="gradientStart" />
|
||||
<stop offset="1" :stop-color="gradientStop" />
|
||||
</linearGradient>
|
||||
<linearGradient id="b" xlink:href="#a" x2="923.39" />
|
||||
<linearGradient id="c" xlink:href="#a" x2="923.39" y1="71.2" y2="71.2" />
|
||||
<linearGradient id="d" xlink:href="#a" x2="923.39" y1="71.2" y2="71.2" />
|
||||
<linearGradient id="e" xlink:href="#a" x2="923.39" y1="71.2" y2="71.2" />
|
||||
<linearGradient id="f" xlink:href="#a" x2="923.39" />
|
||||
<linearGradient id="g" xlink:href="#a" y1="12.16" y2="12.16" />
|
||||
<linearGradient id="h" xlink:href="#a" x2="923.39" y1="86.94" y2="86.94" />
|
||||
</defs>
|
||||
<path fill="url(#a)" d="M54.39 0C20.96 0 0 17.4 0 49.84v42.52c0 32.63 20.96 50.04 53.99 50.04s53.8-16.81 53.8-48.06v-.99H84.25v.99c0 17.8-11.47 27.49-30.26 27.49s-30.46-10.28-30.46-29.47V49.84c0-18.99 11.67-29.47 30.85-29.47s29.86 9.89 29.86 27.69v.79h23.54v-.79C107.79 16.81 87.02 0 54.39 0Z" />
|
||||
<path fill="url(#b)" d="M197.58 0c-33.42 0-54.59 17.4-54.59 49.84v42.52c0 32.63 21.16 50.04 54.19 50.04s54.59-17.4 54.59-50.04V49.84C251.77 17.4 230.61 0 197.58 0Zm30.66 92.36c0 19.18-11.87 29.47-31.05 29.47s-30.66-10.28-30.66-29.47V49.84c0-18.99 11.87-29.47 31.05-29.47s30.66 10.48 30.66 29.47v42.52Z" />
|
||||
<path fill="url(#c)" d="M373.8 97.31 312.49 1.98h-21.95v138.44h23.53V45.09l61.32 95.33h21.95V1.98H373.8v95.33z" />
|
||||
<path fill="url(#d)" d="M521.35 97.31 460.04 1.98h-21.96v138.44h23.54V45.09l61.31 95.33h21.95V1.98h-23.53v95.33z" />
|
||||
<path fill="url(#e)" d="M585.63 140.42h92.95v-20.57h-69.42V81.29h59.54V60.92h-59.54V22.35h69.42V1.98h-92.95v138.44z" />
|
||||
<path fill="url(#f)" d="M766.8 0c-33.43 0-54.39 17.4-54.39 49.84v42.52c0 32.63 20.96 50.04 53.99 50.04s53.8-16.81 53.8-48.06v-.99h-23.54v.99c0 17.8-11.47 27.49-30.26 27.49s-30.46-10.28-30.46-29.47V49.84c0-18.99 11.67-29.47 30.85-29.47s29.86 9.89 29.86 27.69v.79h23.54v-.79c0-31.25-20.77-48.06-53.4-48.06Z" />
|
||||
<path fill="url(#g)" d="M846.11 1.98h108.18v20.37H846.11z" />
|
||||
<path fill="url(#h)" d="M888.43 33.45h23.54v106.97h-23.54z" />
|
||||
</svg>
|
||||
</template>
|
||||
37
web/components/Brand/Mark.vue
Normal file
37
web/components/Brand/Mark.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
gradientStart?: string;
|
||||
gradientStop?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 133.52 76.97"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="unraid-mark"
|
||||
x1="23.76"
|
||||
y1="81.49"
|
||||
x2="109.76"
|
||||
y2="-4.51"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" :stop-color="gradientStart" />
|
||||
<stop offset="1" :stop-color="gradientStop" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill="url(#unraid-mark)"
|
||||
d="M63.49,19.24H70V57.73H63.49ZM6.54,57.73H0V19.24H6.54Zm25.2,4.54h6.55V77H31.74ZM15.87,45.84h6.54V69.62H15.87Zm31.75,0h6.54V69.62H47.62ZM127,19.24h6.54V57.73H127ZM101.77,14.7H95.23V0h6.54Zm15.88,16.44H111.1V7.35h6.55Zm-31.75,0H79.36V7.35H85.9Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
22
web/components/CallbackHandler.ce.vue
Normal file
22
web/components/CallbackHandler.ce.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
import { useCallbackStore } from '~/store/callbackActions';
|
||||
|
||||
const callbackStore = useCallbackStore();
|
||||
|
||||
onBeforeMount(() => {
|
||||
callbackStore.watcher();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
59
web/components/DownloadApiLogs.ce.vue
Normal file
59
web/components/DownloadApiLogs.ce.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ArrowDownTrayIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { DEV_GRAPH_URL, CONNECT_FORUMS, CONTACT, DISCORD } from '~/helpers/urls';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { apiKey } = storeToRefs(useServerStore());
|
||||
|
||||
const downloadUrl = computed(() => new URL(`/graphql/api/logs?apiKey=${apiKey.value}`, DEV_GRAPH_URL.toString() || window.location.origin));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="whitespace-normal flex flex-col gap-y-16px max-w-3xl">
|
||||
<span>
|
||||
{{ t('The primary method of support for Unraid Connect is through our forums and Discord.') }}
|
||||
{{ t('If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.') }}
|
||||
{{ t('The logs may contain sensitive information so do not post them publicly.') }}
|
||||
</span>
|
||||
<span class="flex flex-col gap-y-16px">
|
||||
<div class="flex">
|
||||
<BrandButton
|
||||
class="grow-0 shrink-0"
|
||||
download
|
||||
:external="true"
|
||||
:href="downloadUrl.toString()"
|
||||
:icon="ArrowDownTrayIcon"
|
||||
:text="t('Download unraid-api Logs')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-baseline gap-8px">
|
||||
<a :href="CONNECT_FORUMS.toString()" target="_blank" rel="noopener noreferrer" class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px">
|
||||
{{ t('Unraid Connect Forums') }}
|
||||
<ArrowTopRightOnSquareIcon class="w-16px" />
|
||||
</a>
|
||||
<a :href="DISCORD.toString()" target="_blank" rel="noopener noreferrer" class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px">
|
||||
{{ t('Unraid Discord') }}
|
||||
<ArrowTopRightOnSquareIcon class="w-16px" />
|
||||
</a>
|
||||
<a :href="CONTACT.toString()" target="_blank" rel="noopener noreferrer" class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px">
|
||||
{{ t('Unraid Contact Page') }}
|
||||
<ArrowTopRightOnSquareIcon class="w-16px" />
|
||||
</a>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
46
web/components/I18nHost.ce.vue
Normal file
46
web/components/I18nHost.ce.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import { provide } from 'vue';
|
||||
import { createI18n, I18nInjectionKey } from 'vue-i18n';
|
||||
|
||||
import en_US from '~/locales/en_US.json'; // eslint-disable-line camelcase
|
||||
// import ja from '~/locales/ja.json';
|
||||
|
||||
const defaultLocale = 'en_US'; // ja, en_US
|
||||
let parsedLocale = '';
|
||||
let parsedMessages = {};
|
||||
let nonDefaultLocale = false;
|
||||
/**
|
||||
* In myservers2.php, we have a script tag that sets window.LOCALE_DATA to a stringified JSON object.
|
||||
* Unfortunately, this was the only way I could get the data from PHP to vue-i18n :(
|
||||
* I tried using i18n.setLocaleMessage() but it didn't work no matter what I tried.
|
||||
*/
|
||||
const windowLocaleData = (window as any).LOCALE_DATA || null;
|
||||
if (windowLocaleData) {
|
||||
console.debug('[I18nHost] parsing messages');
|
||||
try {
|
||||
parsedMessages = JSON.parse(decodeURIComponent(windowLocaleData));
|
||||
parsedLocale = Object.keys(parsedMessages)[0];
|
||||
nonDefaultLocale = parsedLocale !== defaultLocale;
|
||||
console.debug('[I18nHost] messages parsed. Now setting up vue-i18n', nonDefaultLocale, parsedLocale, parsedMessages);
|
||||
} catch (error) {
|
||||
console.error('[I18nHost] error parsing messages', error);
|
||||
}
|
||||
}
|
||||
|
||||
const i18n = createI18n<false>({
|
||||
legacy: false, // must set to `false`
|
||||
locale: nonDefaultLocale ? parsedLocale : defaultLocale,
|
||||
fallbackLocale: defaultLocale,
|
||||
messages: {
|
||||
en_US, // eslint-disable-line camelcase
|
||||
// ja,
|
||||
...(nonDefaultLocale ? parsedMessages : {}),
|
||||
}
|
||||
});
|
||||
|
||||
provide(I18nInjectionKey, i18n);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
35
web/components/KeyActions.ce.vue
Normal file
35
web/components/KeyActions.ce.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useServerStore } from '~/store/server';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { keyActions } = storeToRefs(useServerStore());
|
||||
|
||||
console.log('[keyActions]', keyActions.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul v-if="keyActions" class="flex flex-col gap-y-8px">
|
||||
<li v-for="action in keyActions" :key="action.name">
|
||||
<BrandButton
|
||||
class="w-full max-w-300px"
|
||||
:disabled="action?.disabled"
|
||||
:external="action?.external"
|
||||
:href="action?.href"
|
||||
:icon="action.icon"
|
||||
:text="t(action.text)"
|
||||
@click="action.click()"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
125
web/components/Modal.vue
Normal file
125
web/components/Modal.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { TransitionChild, TransitionRoot } from '@headlessui/vue';
|
||||
import { XMarkIcon } from '@heroicons/vue/24/outline';
|
||||
import useFocusTrap from '~/composables/useFocusTrap';
|
||||
|
||||
export interface Props {
|
||||
description?: string;
|
||||
error?: boolean;
|
||||
maxWidth?: string;
|
||||
open?: boolean;
|
||||
showCloseX?: boolean;
|
||||
success?: boolean;
|
||||
t: any;
|
||||
title?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
description: '',
|
||||
error: false,
|
||||
maxWidth: 'sm:max-w-lg',
|
||||
open: false,
|
||||
showCloseX: false,
|
||||
success: false,
|
||||
title: '',
|
||||
});
|
||||
watchEffect(() => {
|
||||
// toggle body scrollability
|
||||
return props.open
|
||||
? document.body.style.setProperty('overflow', 'hidden')
|
||||
: document.body.style.removeProperty('overflow');
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const closeModal = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
const ariaLablledById = computed((): string|undefined => props.title ? `ModalTitle-${Math.random()}`.replace('0.', '') : undefined);
|
||||
|
||||
/**
|
||||
* @todo when providing custom colors for theme we should invert text-beta bg-alpha to text-alpha bg-beta
|
||||
*/
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TransitionRoot appear :show="open" as="template">
|
||||
<div
|
||||
ref="trapRef"
|
||||
class="fixed inset-0 z-10 overflow-y-auto"
|
||||
role="dialog"
|
||||
aria-dialog="true"
|
||||
:aria-labelledby="ariaLablledById"
|
||||
tabindex="-1"
|
||||
@keyup.esc="closeModal"
|
||||
>
|
||||
<TransitionChild
|
||||
appear
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-0 bg-black bg-opacity-80 transition-opacity"
|
||||
:title="t('Click to close modal')"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</TransitionChild>
|
||||
<div class="text-center flex min-h-full items-center justify-center p-4 md:p-0">
|
||||
<TransitionChild
|
||||
appear
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0 scale-95"
|
||||
enter-to="opacity-100 scale-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100 scale-100"
|
||||
leave-to="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
maxWidth,
|
||||
error ? 'shadow-unraid-red/30 border-unraid-red/10' : '',
|
||||
success ? 'shadow-green-600/30 border-green-600/10' : '',
|
||||
!error && !success ? 'shadow-orange/10 border-white/10' : '',
|
||||
]"
|
||||
class="text-16px text-beta bg-alpha text-left relative flex flex-col justify-around p-16px my-24px sm:p-24px border-2 border-solid shadow-xl transform overflow-hidden rounded-lg transition-all sm:w-full"
|
||||
>
|
||||
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 hidden pt-2 pr-2 sm:block">
|
||||
<button type="button" class="rounded-md text-beta bg-alpha p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" @click="closeModal">
|
||||
<span class="sr-only">{{ t('Close') }}</span>
|
||||
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<header class="text-center">
|
||||
<template v-if="!$slots['header']">
|
||||
<h1 v-if="title" :id="ariaLablledById" class="text-24px font-semibold flex flex-wrap justify-center gap-x-1">
|
||||
{{ title }}
|
||||
<slot name="headerTitle" />
|
||||
</h1>
|
||||
<h2 v-if="description" class="text-20px opacity-75">
|
||||
{{ description }}
|
||||
</h2>
|
||||
</template>
|
||||
<slot name="header" />
|
||||
</header>
|
||||
<slot name="main" />
|
||||
|
||||
<footer v-if="$slots['footer']" class="text-14px relative -mx-16px -mb-16px sm:-mx-24px sm:-mb-24px p-4 sm:p-6">
|
||||
<div class="absolute z-0 inset-0 opacity-10 bg-beta" />
|
||||
<div class="relative z-10">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
31
web/components/Modals.ce.vue
Normal file
31
web/components/Modals.ce.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useTrialStore } from '~/store/trial';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { callbackStatus } = storeToRefs(useCallbackActionsStore());
|
||||
const { trialModalVisible } = storeToRefs(useTrialStore());
|
||||
// import { usePromoStore } from '~/store/promo';
|
||||
// const { promoVisible } = storeToRefs(usePromoStore());
|
||||
// <UpcPromo :t="t" :open="promoVisible" />
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative z-[99999]">
|
||||
<UpcCallbackFeedback :t="t" :open="callbackStatus !== 'ready'" />
|
||||
<UpcTrial :t="t" :open="trialModalVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
188
web/components/UserProfile.ce.vue
Normal file
188
web/components/UserProfile.ce.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<script lang="ts" setup>
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useCallbackStore } from '~/store/callbackActions';
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
import type { Server } from '~/types/server';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
export interface Props {
|
||||
server?: Server | string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const callbackStore = useCallbackStore();
|
||||
const dropdownStore = useDropdownStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const { dropdownVisible } = storeToRefs(dropdownStore);
|
||||
const { name, description, lanIp, state, connectPluginInstalled } = storeToRefs(serverStore);
|
||||
const { bannerGradient, theme } = storeToRefs(useThemeStore());
|
||||
|
||||
const hideDropdown = computed(() => state.value === 'PRO' && !connectPluginInstalled.value);
|
||||
|
||||
/**
|
||||
* Close dropdown when clicking outside
|
||||
* @note If in testing you have two variants of the component on a page the clickOutside will fire twice making it seem like it doesn't work
|
||||
*/
|
||||
const clickOutsideTarget = ref();
|
||||
const clickOutsideIgnoreTarget = ref();
|
||||
const outsideDropdown = () => {
|
||||
if (dropdownVisible.value) { return dropdownStore.dropdownToggle(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy LAN IP on server name click
|
||||
*/
|
||||
let copyIpInterval: string | number | NodeJS.Timeout | undefined;
|
||||
const { copy, copied, isSupported } = useClipboard({ source: lanIp.value ?? '' });
|
||||
const showCopyNotSupported = ref<boolean>(false);
|
||||
const copyLanIp = () => {
|
||||
if (!isSupported) { showCopyNotSupported.value = true; }
|
||||
copy(lanIp.value ?? '');
|
||||
};
|
||||
watch(showCopyNotSupported, (newVal, oldVal) => {
|
||||
if (newVal && oldVal === false) {
|
||||
clearTimeout(copyIpInterval);
|
||||
copyIpInterval = setTimeout(() => {
|
||||
showCopyNotSupported.value = false;
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets the server store and locale messages then listen for callbacks
|
||||
*/
|
||||
onBeforeMount(() => {
|
||||
if (!props.server) {
|
||||
throw new Error('Server data not present');
|
||||
}
|
||||
|
||||
if (typeof props.server === 'object') { // Handles the testing dev Vue component
|
||||
serverStore.setServer(props.server);
|
||||
} else if (typeof props.server === 'string') { // Handle web component
|
||||
const parsedServerProp = JSON.parse(props.server);
|
||||
serverStore.setServer(parsedServerProp);
|
||||
}
|
||||
|
||||
callbackStore.watcher();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="UserProfile" class="text-alpha relative z-20 flex flex-col h-full gap-y-4px pt-4px pr-16px pl-40px">
|
||||
<div v-if="bannerGradient" class="absolute z-0 w-[125%] top-0 bottom-0 right-0" :style="bannerGradient" />
|
||||
|
||||
<div class="text-gamma text-10px xs:text-12px text-right font-semibold leading-normal relative z-10 flex flex-col items-end justify-end gap-x-4px xs:flex-row xs:items-baseline xs:gap-x-12px">
|
||||
<UpcUptimeExpire :t="t" />
|
||||
<span class="hidden xs:block">•</span>
|
||||
<UpcServerState :t="t" />
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 flex flex-row items-center justify-end gap-x-16px h-full">
|
||||
<h1 class="text-alpha text-14px sm:text-18px relative flex flex-col-reverse items-end md:flex-row border-0">
|
||||
<template v-if="description && theme?.descriptionShow">
|
||||
<span class="text-right text-12px sm:text-18px hidden 2xs:block">{{ description }}</span>
|
||||
<span class="text-gamma hidden md:inline-block px-8px">•</span>
|
||||
</template>
|
||||
<button :title="t('Click to Copy LAN IP {0}', [lanIp])" @click="copyLanIp()">
|
||||
{{ name }}
|
||||
</button>
|
||||
<span
|
||||
v-show="copied || showCopyNotSupported"
|
||||
class="text-white text-12px leading-none py-4px px-8px absolute top-full right-0 bg-gradient-to-r from-unraid-red to-orange text-center block rounded"
|
||||
>
|
||||
<template v-if="copied">{{ t('LAN IP Copied') }}</template>
|
||||
<template v-else>{{ t('LAN IP {0}', [lanIp]) }}</template>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<template v-if="!hideDropdown">
|
||||
<div class="block w-2px h-24px bg-gamma" />
|
||||
|
||||
<OnClickOutside class="flex items-center justify-end h-full" :options="{ ignore: [clickOutsideIgnoreTarget] }" @trigger="outsideDropdown">
|
||||
<UpcDropdownTrigger ref="clickOutsideIgnoreTarget" :t="t" />
|
||||
<UpcDropdown ref="clickOutsideTarget" :t="t" />
|
||||
</OnClickOutside>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.DropdownWrapper_blip {
|
||||
box-shadow: var(--ring-offset-shadow), var(--ring-shadow), var(--shadow-beta);
|
||||
|
||||
&::before {
|
||||
@apply absolute z-20 block;
|
||||
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: -10px;
|
||||
right: 42px;
|
||||
border-right: 11px solid transparent;
|
||||
border-bottom: 11px solid var(--color-alpha);
|
||||
border-left: 11px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.unraid_mark_2,
|
||||
.unraid_mark_4 {
|
||||
animation: mark_2 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_3 {
|
||||
animation: mark_3 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_6,
|
||||
.unraid_mark_8 {
|
||||
animation: mark_6 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_7 {
|
||||
animation: mark_7 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes mark_2 {
|
||||
50% {
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_3 {
|
||||
50% {
|
||||
transform: translateY(-62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_6 {
|
||||
50% {
|
||||
transform: translateY(40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_7 {
|
||||
50% {
|
||||
transform: translateY(62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
18
web/components/UserProfile/Beta.vue
Normal file
18
web/components/UserProfile/Beta.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
colorClasses?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
colorClasses: 'text-grey-mid border-grey-mid',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="text-10px uppercase py-4px px-6px border-2 rounded-full"
|
||||
:class="colorClasses"
|
||||
>
|
||||
{{ 'Beta' }}
|
||||
</span>
|
||||
</template>
|
||||
340
web/components/UserProfile/CallbackFeedback.vue
Normal file
340
web/components/UserProfile/CallbackFeedback.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<script lang="ts" setup>
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { ClipboardIcon, CogIcon, InformationCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
import { PLUGIN_SETTINGS } from '~/helpers/urls';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useInstallKeyStore } from '~/store/installKey';
|
||||
import { usePromoStore } from '~/store/promo';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const callbackActionsStore = useCallbackActionsStore();
|
||||
const installKeyStore = useInstallKeyStore();
|
||||
const promoStore = usePromoStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const {
|
||||
accountAction,
|
||||
accountActionHide,
|
||||
accountActionStatus,
|
||||
accountActionType,
|
||||
} = storeToRefs(accountStore);
|
||||
const {
|
||||
callbackStatus,
|
||||
} = storeToRefs(callbackActionsStore);
|
||||
const {
|
||||
keyActionType,
|
||||
keyUrl,
|
||||
keyInstallStatus,
|
||||
keyType,
|
||||
} = storeToRefs(installKeyStore);
|
||||
const {
|
||||
connectPluginInstalled,
|
||||
registered,
|
||||
authAction,
|
||||
refreshServerStateStatus,
|
||||
username,
|
||||
} = storeToRefs(serverStore);
|
||||
|
||||
/**
|
||||
* Post sign in success state:
|
||||
* If we're on the Connect settings page in the webGUI
|
||||
* the modal should close instead of redirecting to the
|
||||
* settings page.
|
||||
*
|
||||
* @todo figure out the difference between document.location and window.location in relation to the webGUI and webGUI being iframed
|
||||
*/
|
||||
const isSettingsPage = ref<boolean>(document.location.pathname === '/Settings/ManagementAccess');
|
||||
|
||||
const showPromoCta = computed(() => callbackStatus.value === 'success' && !connectPluginInstalled.value);
|
||||
const showSignInCta = computed(() => connectPluginInstalled.value && !registered.value && authAction.value?.name === 'signIn' && accountActionType.value !== 'signIn');
|
||||
|
||||
const heading = computed(() => {
|
||||
switch (callbackStatus.value) {
|
||||
case 'error':
|
||||
return props.t('Error');
|
||||
case 'loading':
|
||||
return props.t('Performing actions');
|
||||
case 'success':
|
||||
return props.t('Success!');
|
||||
}
|
||||
});
|
||||
const subheading = computed(() => {
|
||||
if (callbackStatus.value === 'error') {
|
||||
return props.t('Something went wrong'); /** @todo show actual error messages */
|
||||
}
|
||||
if (callbackStatus.value === 'loading') { return props.t('Please keep this window open while we perform some actions'); }
|
||||
if (callbackStatus.value === 'success') {
|
||||
if (accountActionType.value === 'signIn') { return props.t('You\'re one step closer to enhancing your Unraid experience'); }
|
||||
if (keyActionType.value === 'purchase') { return props.t('Thank you for purchasing an Unraid {0} Key!', [keyType.value]); }
|
||||
if (keyActionType.value === 'replace') { return props.t('Your {0} Key has been replaced!', [keyType.value]); }
|
||||
if (keyActionType.value === 'trialExtend') { return props.t('Your Trial key has been extended!'); }
|
||||
if (keyActionType.value === 'trialStart') { return props.t('Your free Trial key provides all the functionality of a Pro Registration key'); }
|
||||
if (keyActionType.value === 'upgrade') { return props.t('Thank you for upgrading to an Unraid {0} Key!', [keyType.value]); }
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const closeText = computed(() => {
|
||||
const txt = !connectPluginInstalled.value ? props.t('No thanks') : props.t('Close');
|
||||
return refreshServerStateStatus.value === 'done' ? txt : props.t('Reload');
|
||||
});
|
||||
const close = () => {
|
||||
if (callbackStatus.value === 'loading') { return console.debug('[close] not allowed'); }
|
||||
return refreshServerStateStatus.value === 'done'
|
||||
? callbackActionsStore.setCallbackStatus('ready')
|
||||
: window.location.reload();
|
||||
};
|
||||
|
||||
const promoClick = () => {
|
||||
promoStore.openOnNextLoad();
|
||||
close();
|
||||
};
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
|
||||
const keyInstallStatusCopy = computed((): { text: string; } => {
|
||||
let txt1 = props.t('Installing');
|
||||
let txt2 = props.t('Installed');
|
||||
let txt3 = props.t('Install');
|
||||
switch (keyInstallStatus.value) {
|
||||
case 'ready':
|
||||
return {
|
||||
text: props.t('Ready to Install Key'),
|
||||
};
|
||||
case 'installing':
|
||||
if (keyActionType.value === 'trialExtend') { txt1 = props.t('Installing Extended Trial'); }
|
||||
if (keyActionType.value === 'recover') { txt1 = props.t('Installing Recovered'); }
|
||||
if (keyActionType.value === 'replace') { txt1 = props.t('Installing Replaced'); }
|
||||
return {
|
||||
text: props.t('{0} {1} Key…', [txt1, keyType.value]),
|
||||
};
|
||||
case 'success':
|
||||
if (keyActionType.value === 'trialExtend') { txt2 = props.t('Extension Installed'); }
|
||||
if (keyActionType.value === 'recover') { txt2 = props.t('Recovered'); }
|
||||
if (keyActionType.value === 'replace') { txt2 = props.t('Replaced'); }
|
||||
return {
|
||||
text: props.t('{1} Key {0} Successfully', [txt2, keyType.value]),
|
||||
};
|
||||
case 'failed':
|
||||
if (keyActionType.value === 'trialExtend') { txt3 = props.t('Install Extended'); }
|
||||
if (keyActionType.value === 'recover') { txt3 = props.t('Install Recovered'); }
|
||||
if (keyActionType.value === 'replace') { txt3 = props.t('Install Replaced'); }
|
||||
return {
|
||||
text: props.t('Failed to {0} {1} Key', [txt3, keyType.value]),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const accountActionStatusCopy = computed((): { text: string; } => {
|
||||
switch (accountActionStatus.value) {
|
||||
case 'ready':
|
||||
return {
|
||||
text: props.t('Ready to update Connect account configuration'),
|
||||
};
|
||||
case 'updating':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? props.t('Signing in {0}…', [accountAction.value.user?.preferred_username])
|
||||
: props.t('Signing out {0}…', [username.value]),
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? props.t('{0} Signed In Successfully', [accountAction.value.user?.preferred_username])
|
||||
: props.t('{0} Signed Out Successfully', [username.value]),
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? props.t('Sign In Failed')
|
||||
: props.t('Sign Out Failed'),
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:t="t"
|
||||
:title="heading"
|
||||
:description="subheading"
|
||||
:open="open"
|
||||
max-width="max-w-640px"
|
||||
:error="callbackStatus === 'error'"
|
||||
:success="callbackStatus === 'success'"
|
||||
:show-close-x="callbackStatus !== 'loading'"
|
||||
@close="close"
|
||||
>
|
||||
<template #main>
|
||||
<div
|
||||
v-if="keyInstallStatus !== 'ready' || accountActionStatus !== 'ready'"
|
||||
class="text-center relative w-full flex flex-col justify-center gap-y-16px py-24px sm:py-32px"
|
||||
>
|
||||
<BrandLoading v-if="callbackStatus === 'loading'" class="w-[110px] mx-auto" />
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="keyInstallStatus !== 'ready'"
|
||||
:success="keyInstallStatus === 'success'"
|
||||
:error="keyInstallStatus === 'failed'"
|
||||
:text="keyInstallStatusCopy.text"
|
||||
>
|
||||
<UpcUptimeExpire
|
||||
v-if="keyType === 'Trial'"
|
||||
:for-expire="true"
|
||||
class="opacity-75 italic mt-4px"
|
||||
:t="t"
|
||||
/>
|
||||
|
||||
<template v-if="keyInstallStatus === 'failed'">
|
||||
<div v-if="isSupported" class="flex justify-center">
|
||||
<BrandButton
|
||||
:icon="ClipboardIcon"
|
||||
:text="copied ? t('Copied') : t('Copy Key URL')"
|
||||
@click="copy(keyUrl)"
|
||||
/>
|
||||
</div>
|
||||
<p v-else>
|
||||
{{ t('Copy your Key URL: {0}', [keyUrl]) }}
|
||||
</p>
|
||||
<p>
|
||||
<a href="/Tools/Registration" class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition">
|
||||
{{ t('Then go to Tools > Registration to manually install it') }}
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
</UpcCallbackFeedbackStatus>
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="accountActionStatus !== 'ready' && !accountActionHide"
|
||||
:success="accountActionStatus === 'success'"
|
||||
:error="accountActionStatus === 'failed'"
|
||||
:text="accountActionStatusCopy.text"
|
||||
/>
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="showPromoCta"
|
||||
:icon="InformationCircleIcon"
|
||||
:text="t('Enhance your experience with Unraid Connect')"
|
||||
/>
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="showSignInCta"
|
||||
:icon="InformationCircleIcon"
|
||||
:text="t('Sign In to utilize Unraid Connect')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="callbackStatus === 'success'" class="flex flex-row-reverse justify-center gap-16px">
|
||||
<template v-if="connectPluginInstalled && accountActionType === 'signIn'">
|
||||
<BrandButton
|
||||
v-if="isSettingsPage"
|
||||
:icon="CogIcon"
|
||||
:text="t('Configure Connect Features')"
|
||||
class="grow-0"
|
||||
@click="close"
|
||||
/>
|
||||
<BrandButton
|
||||
v-else
|
||||
:href="PLUGIN_SETTINGS.toString()"
|
||||
:icon="CogIcon"
|
||||
:text="t('Configure Connect Features')"
|
||||
class="grow-0"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<BrandButton
|
||||
v-if="showPromoCta"
|
||||
:text="t('Learn More')"
|
||||
@click="promoClick"
|
||||
/>
|
||||
|
||||
<BrandButton
|
||||
v-if="showSignInCta"
|
||||
:disabled="authAction?.disabled"
|
||||
:external="authAction?.external"
|
||||
:icon="authAction?.icon"
|
||||
:text="t(authAction?.text)"
|
||||
@click="authAction?.click"
|
||||
/>
|
||||
|
||||
<BrandButton
|
||||
btn-style="underline"
|
||||
:text="closeText"
|
||||
@click="close"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
.unraid_mark_2,
|
||||
.unraid_mark_4 {
|
||||
animation: mark_2 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_3 {
|
||||
animation: mark_3 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_6,
|
||||
.unraid_mark_8 {
|
||||
animation: mark_6 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_7 {
|
||||
animation: mark_7 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes mark_2 {
|
||||
50% {
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_3 {
|
||||
50% {
|
||||
transform: translateY(-62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_6 {
|
||||
50% {
|
||||
transform: translateY(40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_7 {
|
||||
50% {
|
||||
transform: translateY(62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
31
web/components/UserProfile/CallbackFeedbackStatus.vue
Normal file
31
web/components/UserProfile/CallbackFeedbackStatus.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
export interface Props {
|
||||
error?: boolean;
|
||||
icon?: typeof CheckCircleIcon;
|
||||
success?: boolean;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
error: false,
|
||||
icon: undefined,
|
||||
success: false,
|
||||
text: undefined,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-[45ch]">
|
||||
<div class="flex items-start justify-center gap-x-8px">
|
||||
<CheckCircleIcon v-if="success" class="fill-green-600 w-28px shrink-0" />
|
||||
<XCircleIcon v-if="error" class="fill-unraid-red w-28px shrink-0" />
|
||||
<component :is="icon" v-if="icon" class="fill-current opacity-75 w-28px shrink-0" />
|
||||
<p class="text-18px">
|
||||
{{ text }}
|
||||
</p>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
34
web/components/UserProfile/Dropdown.vue
Normal file
34
web/components/UserProfile/Dropdown.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { TransitionRoot } from '@headlessui/vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
defineProps<{ t: any; }>();
|
||||
|
||||
const dropdownStore = useDropdownStore();
|
||||
const { dropdownVisible } = storeToRefs(dropdownStore);
|
||||
const { connectPluginInstalled, registered, state, stateDataError } = storeToRefs(useServerStore());
|
||||
|
||||
const showDefaultContent = computed(() => !showLaunchpad.value);
|
||||
const showLaunchpad = computed(() => state.value === 'ENOKEYFILE' || ((connectPluginInstalled.value && !registered.value) && !stateDataError.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TransitionRoot
|
||||
as="template"
|
||||
:show="dropdownVisible"
|
||||
enter="transition-all duration-200"
|
||||
enter-from="opacity-0 translate-y-[16px]"
|
||||
enter-to="opacity-100"
|
||||
leave="transition-all duration-150"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0 translate-y-[16px]"
|
||||
>
|
||||
<UpcDropdownWrapper class="DropdownWrapper_blip text-beta absolute z-30 top-full right-0 transition-all">
|
||||
<UpcDropdownContent v-if="showDefaultContent" :t="t" />
|
||||
<UpcDropdownLaunchpad v-else-if="showLaunchpad" :t="t" />
|
||||
</UpcDropdownWrapper>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
71
web/components/UserProfile/DropdownConnectStatus.vue
Normal file
71
web/components/UserProfile/DropdownConnectStatus.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { ExclamationTriangleIcon, CheckCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import BrandLoading from '~/components/Brand/Loading.vue';
|
||||
import { useUnraidApiStore } from '~/store/unraidApi';
|
||||
|
||||
const props = defineProps<{ t: any; }>();
|
||||
|
||||
const unraidApiStore = useUnraidApiStore();
|
||||
const { unraidApiStatus, unraidApiRestartAction } = storeToRefs(unraidApiStore);
|
||||
|
||||
interface StatusOutput {
|
||||
icon: typeof BrandLoading | typeof ExclamationTriangleIcon | typeof CheckCircleIcon;
|
||||
iconClasses?: string;
|
||||
text: string;
|
||||
textClasses?: string;
|
||||
}
|
||||
const status = computed((): StatusOutput | undefined => {
|
||||
if (unraidApiStatus.value === 'connecting') {
|
||||
return {
|
||||
icon: BrandLoading,
|
||||
iconClasses: 'w-16px',
|
||||
text: props.t('Loading…'),
|
||||
textClasses: 'italic',
|
||||
};
|
||||
}
|
||||
if (unraidApiStatus.value === 'restarting') {
|
||||
return {
|
||||
icon: BrandLoading,
|
||||
iconClasses: 'w-16px',
|
||||
text: props.t('Restarting unraid-api…'),
|
||||
textClasses: 'italic',
|
||||
};
|
||||
}
|
||||
if (unraidApiStatus.value === 'offline') {
|
||||
return {
|
||||
icon: ExclamationTriangleIcon,
|
||||
iconClasses: 'text-red-500 w-16px h-16px',
|
||||
text: props.t('unraid-api is offline'),
|
||||
};
|
||||
}
|
||||
if (unraidApiStatus.value === 'online') {
|
||||
return {
|
||||
icon: CheckCircleIcon,
|
||||
iconClasses: 'text-green-600 w-16px h-16px',
|
||||
text: props.t('Connected'),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
v-if="status"
|
||||
class="flex flex-row justify-start items-center gap-8px mt-8px px-8px"
|
||||
>
|
||||
<component
|
||||
:is="status.icon"
|
||||
:class="status.iconClasses"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span :class="status?.textClasses">
|
||||
{{ status.text }}
|
||||
</span>
|
||||
</li>
|
||||
<li v-if="unraidApiRestartAction" class="w-full">
|
||||
<UpcDropdownItem :item="unraidApiRestartAction" :t="t" />
|
||||
</li>
|
||||
</template>
|
||||
107
web/components/UserProfile/DropdownContent.vue
Normal file
107
web/components/UserProfile/DropdownContent.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ArrowTopRightOnSquareIcon, CogIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
import { ACCOUNT, CONNECT_DASHBOARD, PLUGIN_SETTINGS } from '~/helpers/urls';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
// import { usePromoStore } from '~/store/promo';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
const props = defineProps<{ t: any; }>();
|
||||
|
||||
const errorsStore = useErrorsStore();
|
||||
// const promoStore = usePromoStore();
|
||||
|
||||
const { keyActions, connectPluginInstalled, registered, stateData } = storeToRefs(useServerStore());
|
||||
const { errors } = storeToRefs(errorsStore);
|
||||
|
||||
const signInAction = computed(() => stateData.value.actions?.filter((act: { name: string; }) => act.name === 'signIn') ?? []);
|
||||
const signOutAction = computed(() => stateData.value.actions?.filter((act: { name: string; }) => act.name === 'signOut') ?? []);
|
||||
|
||||
const links = computed(():UserProfileLink[] => {
|
||||
return [
|
||||
...(registered.value && connectPluginInstalled.value
|
||||
? [
|
||||
{
|
||||
emphasize: true,
|
||||
external: true,
|
||||
href: CONNECT_DASHBOARD.toString(),
|
||||
icon: ArrowTopRightOnSquareIcon,
|
||||
text: props.t('Go to Connect'),
|
||||
title: props.t('Opens Connect in new tab'),
|
||||
},
|
||||
{
|
||||
external: true,
|
||||
href: ACCOUNT.toString(),
|
||||
icon: ArrowTopRightOnSquareIcon,
|
||||
text: props.t('Manage Unraid.net Account'),
|
||||
title: props.t('Manage Unraid.net Account in new tab'),
|
||||
},
|
||||
{
|
||||
href: PLUGIN_SETTINGS.toString(),
|
||||
icon: CogIcon,
|
||||
text: props.t('Settings'),
|
||||
title: props.t('Go to Connect plugin settings'),
|
||||
},
|
||||
...(signOutAction.value),
|
||||
]
|
||||
: []
|
||||
),
|
||||
...(!registered.value && connectPluginInstalled.value
|
||||
? [
|
||||
...(signInAction.value),
|
||||
]
|
||||
: []
|
||||
),
|
||||
// ...(!connectPluginInstalled.value
|
||||
// ? [
|
||||
// {
|
||||
// click: () => {
|
||||
// promoStore.promoShow();
|
||||
// },
|
||||
// icon: InformationCircleIcon,
|
||||
// text: props.t('Enhance your Unraid experience with Connect'),
|
||||
// title: props.t('Enhance your Unraid experience with Connect'),
|
||||
// },
|
||||
// ]
|
||||
// : []
|
||||
// ),
|
||||
];
|
||||
});
|
||||
|
||||
const showErrors = computed(() => errors.value.length);
|
||||
const showConnectStatus = computed(() => !showErrors.value && !stateData.value.error && registered.value && connectPluginInstalled.value);
|
||||
const showKeyline = computed(() => showConnectStatus.value && (keyActions.value?.length || links.value.length));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-8px min-w-300px max-w-350px">
|
||||
<header v-if="connectPluginInstalled" class="flex flex-row items-center justify-between mt-8px mx-8px">
|
||||
<h2 class="text-18px leading-none flex flex-row gap-x-8px items-center justify-between">
|
||||
<BrandLogoConnect gradient-start="currentcolor" gradient-stop="currentcolor" class="text-beta w-[120px]" />
|
||||
<UpcBeta />
|
||||
</h2>
|
||||
</header>
|
||||
<ul class="list-reset flex flex-col gap-y-4px p-0">
|
||||
<UpcDropdownConnectStatus v-if="showConnectStatus" :t="t" />
|
||||
<UpcDropdownError v-if="showErrors" :t="t" />
|
||||
|
||||
<li v-if="showKeyline" class="my-8px">
|
||||
<UpcKeyline />
|
||||
</li>
|
||||
|
||||
<template v-if="keyActions">
|
||||
<li v-for="action in keyActions" :key="action.name">
|
||||
<UpcDropdownItem :item="action" :t="t" />
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<template v-if="links.length">
|
||||
<li v-for="(link, index) in links" :key="`link_${index}`">
|
||||
<UpcDropdownItem :item="link" :t="t" />
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
27
web/components/UserProfile/DropdownError.vue
Normal file
27
web/components/UserProfile/DropdownError.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
// eslint-disable vue/no-v-html
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
|
||||
defineProps<{ t: any; }>();
|
||||
|
||||
const errorsStore = useErrorsStore();
|
||||
const { errors } = storeToRefs(errorsStore);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul v-if="errors.length" class="list-reset flex flex-col gap-y-8px mb-4px border-2 border-solid border-unraid-red/90 rounded-md">
|
||||
<li v-for="(error, index) in errors" :key="index" class="flex flex-col gap-8px">
|
||||
<h3 class="text-18px py-4px px-12px text-white bg-unraid-red/90 font-semibold">
|
||||
<span>{{ t(error.heading) }}</span>
|
||||
</h3>
|
||||
<div class="text-14px px-12px flex flex-col gap-y-8px" :class="{ 'pb-8px': !error.actions }" v-html="t(error.message)" />
|
||||
<nav v-if="error.actions">
|
||||
<li v-for="(link, idx) in error.actions" :key="`link_${idx}`">
|
||||
<UpcDropdownItem :item="link" :rounded="false" :t="t" />
|
||||
</li>
|
||||
</nav>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
46
web/components/UserProfile/DropdownItem.vue
Normal file
46
web/components/UserProfile/DropdownItem.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
import type { ServerStateDataAction } from '~/types/server';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
export interface Props {
|
||||
item: ServerStateDataAction | UserProfileLink;
|
||||
rounded?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rounded: true,
|
||||
});
|
||||
|
||||
const showExternalIconOnHover = computed(() => props.item?.external && props.item.icon !== ArrowTopRightOnSquareIcon);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="item?.click ? 'button' : 'a'"
|
||||
:disabled="item?.disabled"
|
||||
:href="item?.href ?? null"
|
||||
:title="item?.title ? t(item?.title) : null"
|
||||
:target="item?.external ? '_blank' : null"
|
||||
:rel="item?.external ? 'noopener noreferrer' : null"
|
||||
class="text-left text-14px w-full flex flex-row items-center justify-between gap-x-8px px-8px py-8px cursor-pointer"
|
||||
:class="{
|
||||
'text-beta bg-transparent hover:text-white hover:bg-gradient-to-r hover:from-unraid-red hover:to-orange focus:text-white focus:bg-gradient-to-r focus:from-unraid-red focus:to-orange focus:outline-none': !item?.emphasize,
|
||||
'text-white bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60': item?.emphasize,
|
||||
'group': showExternalIconOnHover,
|
||||
'rounded-md': rounded,
|
||||
'disabled:opacity-50 disabled:hover:opacity-50 disabled:focus:opacity-50 disabled:cursor-not-allowed': item?.disabled,
|
||||
}"
|
||||
@click.stop="item?.click ? item?.click() : null"
|
||||
>
|
||||
<span class="leading-snug inline-flex flex-row items-center gap-x-8px">
|
||||
<component :is="item?.icon" class="flex-shrink-0 text-current w-16px h-16px" aria-hidden="true" />
|
||||
{{ t(item?.text) }}
|
||||
</span>
|
||||
<ArrowTopRightOnSquareIcon
|
||||
v-if="showExternalIconOnHover"
|
||||
class="text-white fill-current flex-shrink-0 w-16px h-16px ml-8px opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-in-out"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
124
web/components/UserProfile/DropdownLaunchpad.vue
Normal file
124
web/components/UserProfile/DropdownLaunchpad.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
const props = defineProps<{ t: any; }>();
|
||||
|
||||
const { expireTime, connectPluginInstalled, registered, state, stateData } = storeToRefs(useServerStore());
|
||||
|
||||
const showConnectCopy = computed(() => (connectPluginInstalled.value && !registered.value && !stateData.value?.error));
|
||||
|
||||
const heading = computed(() => {
|
||||
if (showConnectCopy.value) { return props.t('Thank you for installing Connect!'); }
|
||||
return props.t(stateData.value.heading);
|
||||
});
|
||||
|
||||
const subheading = computed(() => {
|
||||
if (showConnectCopy.value) { return props.t('Sign In to your Unraid.net account to get started'); }
|
||||
return props.t(stateData.value.message);
|
||||
});
|
||||
|
||||
const showExpireTime = computed(() => {
|
||||
return (state.value === 'TRIAL' || state.value === 'EEXPIRED') && expireTime.value > 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-24px w-full min-w-300px md:min-w-[500px] max-w-xl p-16px">
|
||||
<header :class="{ 'text-center': showConnectCopy }">
|
||||
<h2 class="text-24px text-center font-semibold" v-html="heading" />
|
||||
<div class="flex flex-col gap-y-8px" v-html="subheading" />
|
||||
<UpcUptimeExpire
|
||||
v-if="showExpireTime"
|
||||
class="text-center opacity-75 mt-12px"
|
||||
:t="t"
|
||||
/>
|
||||
</header>
|
||||
<ul v-if="stateData.actions" class="list-reset flex flex-col gap-y-8px px-16px">
|
||||
<li v-for="action in stateData.actions" :key="action.name">
|
||||
<BrandButton
|
||||
class="w-full"
|
||||
:disabled="action?.disabled"
|
||||
:external="action?.external"
|
||||
:href="action?.href"
|
||||
:icon="action.icon"
|
||||
:text="t(action.text)"
|
||||
@click="action.click()"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.DropdownWrapper_blip {
|
||||
box-shadow: var(--ring-offset-shadow), var(--ring-shadow), var(--shadow-beta);
|
||||
|
||||
&::before {
|
||||
@apply absolute z-20 block;
|
||||
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: -10px;
|
||||
right: 42px;
|
||||
border-right: 11px solid transparent;
|
||||
border-bottom: 11px solid var(--color-alpha);
|
||||
border-left: 11px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.unraid_mark_2,
|
||||
.unraid_mark_4 {
|
||||
animation: mark_2 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_3 {
|
||||
animation: mark_3 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_6,
|
||||
.unraid_mark_8 {
|
||||
animation: mark_6 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_7 {
|
||||
animation: mark_7 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes mark_2 {
|
||||
50% {
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_3 {
|
||||
50% {
|
||||
transform: translateY(-62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_6 {
|
||||
50% {
|
||||
transform: translateY(40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_7 {
|
||||
50% {
|
||||
transform: translateY(62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
web/components/UserProfile/DropdownTrigger.vue
Normal file
56
web/components/UserProfile/DropdownTrigger.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { InformationCircleIcon, ExclamationTriangleIcon, ShieldExclamationIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
const props = defineProps<{ t: any; }>();
|
||||
|
||||
const dropdownStore = useDropdownStore();
|
||||
const { dropdownVisible } = storeToRefs(dropdownStore);
|
||||
const { errors } = storeToRefs(useErrorsStore());
|
||||
const {
|
||||
connectPluginInstalled,
|
||||
registered,
|
||||
state,
|
||||
stateData,
|
||||
username,
|
||||
} = storeToRefs(useServerStore());
|
||||
|
||||
const registeredAndconnectPluginInstalled = computed(() => connectPluginInstalled.value && registered.value);
|
||||
const showErrorIcon = computed(() => errors.value.length || stateData.value.error);
|
||||
|
||||
const text = computed((): string | undefined => {
|
||||
if ((stateData.value.error) && state.value !== 'EEXPIRED') { return props.t('Fix Error'); }
|
||||
if (registeredAndconnectPluginInstalled.value) { return username.value; }
|
||||
});
|
||||
|
||||
const title = computed((): string => {
|
||||
if (state.value === 'ENOKEYFILE') { return props.t('Get Started'); }
|
||||
if (state.value === 'EEXPIRED') { return props.t('Trial Expired, see options below'); }
|
||||
if (showErrorIcon.value) { return props.t('Learn more about the error'); }
|
||||
return dropdownVisible.value ? props.t('Close Dropdown') : props.t('Open Dropdown');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="group text-18px hover:text-alpha focus:text-alpha border-0 relative flex flex-row justify-end items-center h-full gap-x-8px outline-none focus:outline-none"
|
||||
:title="title"
|
||||
@click="dropdownStore.dropdownToggle()"
|
||||
>
|
||||
<template v-if="errors.length && errors[0].level">
|
||||
<InformationCircleIcon v-if="errors[0].level === 'info'" class="text-unraid-red fill-current relative w-24px h-24px" />
|
||||
<ExclamationTriangleIcon v-if="errors[0].level === 'warning'" class="text-unraid-red fill-current relative w-24px h-24px" />
|
||||
<ShieldExclamationIcon v-if="errors[0].level === 'error'" class="text-unraid-red fill-current relative w-24px h-24px" />
|
||||
</template>
|
||||
<span v-if="text" class="relative leading-none">
|
||||
<span>{{ text }}</span>
|
||||
<span class="absolute bottom-[-3px] inset-x-0 h-2px w-full bg-gradient-to-r from-unraid-red to-orange rounded opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity" />
|
||||
</span>
|
||||
<UpcDropdownTriggerMenuIcon :open="dropdownVisible" />
|
||||
<BrandAvatar />
|
||||
</button>
|
||||
</template>
|
||||
14
web/components/UserProfile/DropdownTriggerMenuIcon.vue
Normal file
14
web/components/UserProfile/DropdownTriggerMenuIcon.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { Bars3Icon, XMarkIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Bars3Icon v-if="!open" class="w-20px" />
|
||||
<XMarkIcon v-else class="w-20px" />
|
||||
</template>
|
||||
10
web/components/UserProfile/DropdownWrapper.vue
Normal file
10
web/components/UserProfile/DropdownWrapper.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
// shadow-[var(--ring-offset-shadow)_var(--ring-shadow)_var(--shadow-beta)]
|
||||
// border border-solid border-beta/5
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="flex flex-col gap-y-8px p-8px bg-alpha rounded-lg shadow-xl shadow-orange/10">
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
3
web/components/UserProfile/Keyline.vue
Normal file
3
web/components/UserProfile/Keyline.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<hr class="w-full h-2px bg-gradient-to-r from-unraid-red to-orange shadow-none border-none rounded">
|
||||
</template>
|
||||
141
web/components/UserProfile/Promo.vue
Normal file
141
web/components/UserProfile/Promo.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* @todo devEnv should be a .env variable so we can gate staging installs
|
||||
*
|
||||
* @todo future idea – turn this into a carousel. each feature could have a short video if we ever them
|
||||
*/
|
||||
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
import useInstallPlugin from '~/composables/installPlugin';
|
||||
import { CONNECT_DOCS } from '~/helpers/urls';
|
||||
import { usePromoStore } from '~/store/promo';
|
||||
import type { UserProfilePromoFeature } from '~/types/userProfile';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const promoStore = usePromoStore();
|
||||
|
||||
/**
|
||||
* These are translated in the component below. So if you add a new feature, make sure to add it to the translation file.
|
||||
*/
|
||||
const features = ref<UserProfilePromoFeature[]>([
|
||||
{
|
||||
title: 'Dynamic Remote Access',
|
||||
copy: 'Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.',
|
||||
},
|
||||
{
|
||||
title: 'Manage Your Server Within Connect',
|
||||
copy: 'Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.',
|
||||
},
|
||||
{
|
||||
title: 'Deep Linking',
|
||||
copy: 'The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.',
|
||||
},
|
||||
{
|
||||
title: 'Online Flash Backup',
|
||||
copy: 'Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.',
|
||||
},
|
||||
{
|
||||
title: 'Real-time Monitoring',
|
||||
copy: 'Get an overview of your server\'s state, storage space, apps and VMs status, and more.',
|
||||
},
|
||||
{
|
||||
title: 'Customizable Dashboard Tiles',
|
||||
copy: 'Set custom server tiles how you like and automatically display your server\'s banner image on your Connect Dashboard.',
|
||||
},
|
||||
{
|
||||
title: 'License Management',
|
||||
copy: 'Manage your license keys at any time via the My Keys section.',
|
||||
},
|
||||
{
|
||||
title: 'Plus more on the way',
|
||||
copy: 'All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.',
|
||||
},
|
||||
]);
|
||||
|
||||
const staging = ref(false);
|
||||
const { install } = useInstallPlugin();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:t="t"
|
||||
:title="t('Introducing Unraid Connect')"
|
||||
:description="t('Enhance your Unraid experience')"
|
||||
:open="open"
|
||||
:show-close-x="true"
|
||||
max-width="max-w-800px"
|
||||
@close="promoStore.promoHide()"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<span><UpcBeta class="relative -top-1" /></span>
|
||||
</template>
|
||||
|
||||
<template #main>
|
||||
<div class="text-center relative w-full">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 justify-center p-16px md:py-24px gap-16px">
|
||||
<UpcPromoFeature
|
||||
v-for="(feature, index) in features"
|
||||
:key="index"
|
||||
:title="t(feature.title)"
|
||||
:copy="t(feature.copy)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="w-full max-w-xs flex flex-col items-center gap-y-16px mx-auto">
|
||||
<SwitchGroup v-if="import.meta.env.DEV" as="div" class="flex items-center justify-center">
|
||||
<Switch v-model="staging" :class="[staging ? 'bg-indigo-600' : 'bg-gray-200', 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2']">
|
||||
<span aria-hidden="true" :class="[staging ? 't-x-5' : 't-x-0', 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out']" />
|
||||
</Switch>
|
||||
<SwitchLabel as="span" class="ml-3 text-12px">
|
||||
<span class="font-semibold">Install Staging</span>
|
||||
</SwitchLabel>
|
||||
</SwitchGroup>
|
||||
<button
|
||||
class="text-white text-14px text-center w-full flex flex-row items-center justify-center gap-x-8px px-8px py-8px cursor-pointer rounded-md bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60"
|
||||
@click="install({ staging, update: false })"
|
||||
>
|
||||
{{ staging ? 'Install Connect Staging' : t('Install Connect') }}
|
||||
</button>
|
||||
<div>
|
||||
<a
|
||||
:href="CONNECT_DOCS.toString()"
|
||||
class="text-12px tracking-wide inline-flex flex-row items-center justify-start gap-8px mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:title="t('Checkout the Connect Documentation')"
|
||||
>
|
||||
{{ t('Learn More') }}
|
||||
<ArrowTopRightOnSquareIcon class="w-16px" />
|
||||
</a>
|
||||
<button
|
||||
class="text-12px tracking-wide inline-block mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
|
||||
:title="t('Close')"
|
||||
@click="promoStore.promoHide()"
|
||||
>
|
||||
{{ t('No thanks') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
39
web/components/UserProfile/PromoFeature.vue
Normal file
39
web/components/UserProfile/PromoFeature.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
center?: boolean;
|
||||
copy?: string;
|
||||
icon?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-left relative flex overflow-hidden">
|
||||
<span v-if="!center" class="flex-shrink-0">
|
||||
<slot />
|
||||
</span>
|
||||
<div class="inline-flex flex-col" :class="{ 'text-center': center }">
|
||||
<h3
|
||||
class="text-16px font-semibold"
|
||||
:class="{
|
||||
'mt-0 mb-4px': copy,
|
||||
'my-0': !copy,
|
||||
'flex flex-row justify-center items-center': center
|
||||
}"
|
||||
>
|
||||
<span v-if="center" class="flex-shrink-0 mr-8px">
|
||||
<slot />
|
||||
</span>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="copy"
|
||||
class="text-14px opacity-75 py-0"
|
||||
:class="{'px-8px': center}"
|
||||
v-html="copy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
42
web/components/UserProfile/ServerState.vue
Normal file
42
web/components/UserProfile/ServerState.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { ServerStateDataAction } from '~/types/server';
|
||||
|
||||
defineProps<{ t: any; }>();
|
||||
|
||||
const { state, stateData } = storeToRefs(useServerStore());
|
||||
|
||||
const purchaseAction = computed((): ServerStateDataAction | undefined => {
|
||||
return stateData.value.actions && stateData.value.actions.find(action => action.name === 'purchase');
|
||||
});
|
||||
const upgradeAction = computed((): ServerStateDataAction | undefined => {
|
||||
return stateData.value.actions && stateData.value.actions.find(action => action.name === 'upgrade');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="flex flex-row items-center gap-x-8px">
|
||||
<template v-if="upgradeAction">
|
||||
<UpcServerStateBuy
|
||||
class="text-gamma"
|
||||
:title="t('Upgrade Key')"
|
||||
@click="upgradeAction.click()"
|
||||
>
|
||||
<h5>Unraid OS <em><strong>{{ t(stateData.humanReadable) }}</strong></em></h5>
|
||||
</UpcServerStateBuy>
|
||||
</template>
|
||||
<h5 v-else>
|
||||
Unraid OS <em :class="{ 'text-unraid-red': stateData.error || state === 'EEXPIRED' }"><strong>{{ t(stateData.humanReadable) }}</strong></em>
|
||||
</h5>
|
||||
|
||||
<template v-if="purchaseAction">
|
||||
<UpcServerStateBuy
|
||||
class="text-orange-dark relative top-[1px] hidden sm:block"
|
||||
:title="t('Purchase Key')"
|
||||
@click="purchaseAction.click()"
|
||||
>{{ t('Purchase') }}</UpcServerStateBuy>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
5
web/components/UserProfile/ServerStateBuy.vue
Normal file
5
web/components/UserProfile/ServerStateBuy.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<button class="text-12px font-semibold transition-colors duration-150 ease-in-out border-t-0 border-l-0 border-r-0 border-b-2 border-transparent hover:border-orange-dark focus:border-orange-dark focus:outline-none">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
82
web/components/UserProfile/Trial.vue
Normal file
82
web/components/UserProfile/Trial.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useTrialStore } from '~/store/trial';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const trialStore = useTrialStore();
|
||||
const { trialModalLoading, trialStatus } = storeToRefs(trialStore);
|
||||
|
||||
interface TrialStatusCopy {
|
||||
heading: string;
|
||||
subheading?: string;
|
||||
}
|
||||
const trialStatusCopy = computed((): TrialStatusCopy | null => {
|
||||
switch (trialStatus.value) {
|
||||
case 'failed':
|
||||
return {
|
||||
heading: props.t('Trial Key Creation Failed'),
|
||||
subheading: props.t('Error creatiing a trial key. Please try again later.'),
|
||||
};
|
||||
case 'trialExtend':
|
||||
return {
|
||||
heading: props.t('Extending your free trial by 15 days'),
|
||||
subheading: props.t('Please keep this window open'),
|
||||
};
|
||||
case 'trialStart':
|
||||
return {
|
||||
heading: props.t('Starting your free 30 day trial'),
|
||||
subheading: props.t('Please keep this window open'),
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
heading: props.t('Trial Key Created'),
|
||||
subheading: props.t('Please wait while the page reloads to install your trial key'),
|
||||
};
|
||||
case 'ready':
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
if (trialStatus.value === 'trialStart') { return console.debug('[close] not allowed'); }
|
||||
trialStore.setTrialStatus('ready');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:t="t"
|
||||
:open="open"
|
||||
:title="trialStatusCopy?.heading"
|
||||
:description="trialStatusCopy?.subheading"
|
||||
:show-close-x="!trialModalLoading"
|
||||
max-width="max-w-640px"
|
||||
@close="close"
|
||||
>
|
||||
<template #main>
|
||||
<BrandLoading v-if="trialModalLoading" class="w-[150px] mx-auto my-24px" />
|
||||
</template>
|
||||
|
||||
<template v-if="!trialModalLoading" #footer>
|
||||
<div class="w-full max-w-xs flex flex-col items-center gap-y-16px mx-auto">
|
||||
<div>
|
||||
<button
|
||||
class="text-12px tracking-wide inline-block mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
|
||||
:title="t('Close Modal')"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('Close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
78
web/components/UserProfile/UptimeExpire.vue
Normal file
78
web/components/UserProfile/UptimeExpire.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import useTimeHelper from '~/composables/time';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
export interface Props {
|
||||
forExpire?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
forExpire: false,
|
||||
});
|
||||
|
||||
const { buildStringFromValues, dateDiff, formatDate } = useTimeHelper(props.t);
|
||||
|
||||
const serverStore = useServerStore();
|
||||
const { uptime, expireTime, state } = storeToRefs(serverStore);
|
||||
|
||||
const time = computed(() => {
|
||||
if (props.forExpire && expireTime.value) {
|
||||
return expireTime.value;
|
||||
}
|
||||
return (state.value === 'TRIAL' || state.value === 'EEXPIRED') && expireTime.value && expireTime.value > 0
|
||||
? expireTime.value
|
||||
: uptime.value;
|
||||
});
|
||||
|
||||
const parsedTime = ref<string>('');
|
||||
const formattedTime = computed<string>(() => formatDate(time.value));
|
||||
|
||||
const countUp = computed<boolean>(() => {
|
||||
if (props.forExpire && expireTime.value) {
|
||||
return false;
|
||||
}
|
||||
return state.value !== 'TRIAL' && state.value !== 'ENOCONN';
|
||||
});
|
||||
|
||||
const output = computed(() => {
|
||||
if (!countUp.value || state.value === 'EEXPIRED') {
|
||||
return {
|
||||
title: state.value === 'EEXPIRED'
|
||||
? props.t('Trial Key Expired at {0}', [formattedTime.value])
|
||||
: props.t('Trial Key Expires at {0}', [formattedTime.value]),
|
||||
text: state.value === 'EEXPIRED'
|
||||
? props.t('Trial Key Expired {0}', [parsedTime.value])
|
||||
: props.t('Trial Key Expires in {0}', [parsedTime.value]),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: props.t('Server Up Since {0}', [formattedTime.value]),
|
||||
text: props.t('Uptime {0}', [parsedTime.value]),
|
||||
};
|
||||
});
|
||||
|
||||
const runDiff = () => {
|
||||
parsedTime.value = buildStringFromValues(dateDiff((time.value).toString(), countUp.value));
|
||||
};
|
||||
|
||||
let interval: string | number | NodeJS.Timeout | undefined;
|
||||
onBeforeMount(() => {
|
||||
runDiff();
|
||||
interval = setInterval(() => {
|
||||
runDiff();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p :title="output.title">
|
||||
{{ output.text }}
|
||||
</p>
|
||||
</template>
|
||||
76
web/components/WanIpCheck.ce.vue
Normal file
76
web/components/WanIpCheck.ce.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { request } from '~/composables/services/request';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
export interface Props {
|
||||
phpWanIp?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { isRemoteAccess } = storeToRefs(useServerStore());
|
||||
|
||||
const wanIp = ref<string | null>();
|
||||
const fetchError = ref<any>();
|
||||
const loading = ref(false);
|
||||
|
||||
const computedError = computed(() => {
|
||||
if (!props.phpWanIp) { return t('DNS issue, unable to resolve wanip4.unraid.net'); }
|
||||
if (fetchError.value) { return fetchError.value; }
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
wanIp.value = sessionStorage.getItem('unraidConnect_wanIp');
|
||||
});
|
||||
|
||||
watchEffect(async () => {
|
||||
// if we don't have a client WAN IP AND we have the server WAN IP then we fetch
|
||||
if (!wanIp.value && props.phpWanIp) {
|
||||
console.debug('[watch] wanIp');
|
||||
loading.value = true;
|
||||
|
||||
const response = await request.url('https://wanip4.unraid.net/')
|
||||
.get()
|
||||
.text();
|
||||
|
||||
if (response) {
|
||||
console.debug('[watch] wanIp response', response);
|
||||
loading.value = false;
|
||||
wanIp.value = response as string; // response returns text nothing to traverse
|
||||
// save in sessionStorage so we only make this request once per webGUI session
|
||||
sessionStorage.setItem('unraidConnect_wanIp', wanIp.value);
|
||||
} else {
|
||||
loading.value = false;
|
||||
fetchError.value = t('Unable to fetch client WAN IPv4');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="loading" class="italic">{{ t('Checking WAN IPs…') }}</span>
|
||||
<template v-else>
|
||||
<span v-if="computedError" class="text-unraid-red font-semibold">{{ computedError }}</span>
|
||||
<template v-else>
|
||||
<span v-if="isRemoteAccess || phpWanIp === wanIp && !isRemoteAccess">{{ t('Remark: your WAN IPv4 is {0}', [wanIp]) }}</span>
|
||||
<span v-else class="inline-block w-1/2 whitespace-normal">
|
||||
{{ t("Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.", [phpWanIp, wanIp]) }}
|
||||
{{ t('This may indicate a complex network that will not work with this Remote Access solution.') }}
|
||||
{{ t('Ignore this message if you are currently connected via Remote Access or VPN.') }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
66
web/composables/gql/fragment-masking.ts
Normal file
66
web/composables/gql/fragment-masking.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
|
||||
import type { FragmentDefinitionNode } from 'graphql';
|
||||
import type { Incremental } from './graphql';
|
||||
|
||||
|
||||
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
|
||||
infer TType,
|
||||
any
|
||||
>
|
||||
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
|
||||
? TKey extends string
|
||||
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
// return non-nullable if `fragmentType` is non-nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
|
||||
): TType;
|
||||
// return nullable if `fragmentType` is nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
|
||||
): TType | null | undefined;
|
||||
// return array of non-nullable if `fragmentType` is array of non-nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
): ReadonlyArray<TType>;
|
||||
// return array of nullable if `fragmentType` is array of nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
||||
): ReadonlyArray<TType> | null | undefined;
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
||||
): TType | ReadonlyArray<TType> | null | undefined {
|
||||
return fragmentType as any;
|
||||
}
|
||||
|
||||
|
||||
export function makeFragmentData<
|
||||
F extends DocumentTypeDecoration<any, any>,
|
||||
FT extends ResultOf<F>
|
||||
>(data: FT, _fragment: F): FragmentType<F> {
|
||||
return data as FragmentType<F>;
|
||||
}
|
||||
export function isFragmentReady<TQuery, TFrag>(
|
||||
queryNode: DocumentTypeDecoration<TQuery, any>,
|
||||
fragmentNode: TypedDocumentNode<TFrag>,
|
||||
data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
|
||||
): data is FragmentType<typeof fragmentNode> {
|
||||
const deferredFields = (queryNode as { __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> } }).__meta__
|
||||
?.deferredFields;
|
||||
|
||||
if (!deferredFields) return true;
|
||||
|
||||
const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
|
||||
const fragName = fragDef?.name?.value;
|
||||
|
||||
const fields = (fragName && deferredFields[fragName]) || [];
|
||||
return fields.length > 0 && fields.every(field => data && field in data);
|
||||
}
|
||||
42
web/composables/gql/gql.ts
Normal file
42
web/composables/gql/gql.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable */
|
||||
import * as types from './graphql';
|
||||
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
|
||||
/**
|
||||
* Map of all GraphQL operations in the project.
|
||||
*
|
||||
* This map has several performance disadvantages:
|
||||
* 1. It is not tree-shakeable, so it will include all operations in the project.
|
||||
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
|
||||
* 3. It does not support dead code elimination, so it will add unused operations.
|
||||
*
|
||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||
*/
|
||||
const documents = {
|
||||
"\n query serverState {\n cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n": types.serverStateDocument,
|
||||
};
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
|
||||
* ```
|
||||
*
|
||||
* The query argument is unknown!
|
||||
* Please regenerate the types.
|
||||
*/
|
||||
export function graphql(source: string): unknown;
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query serverState {\n cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n"): (typeof documents)["\n query serverState {\n cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n"];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
}
|
||||
|
||||
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;
|
||||
1595
web/composables/gql/graphql.ts
Normal file
1595
web/composables/gql/graphql.ts
Normal file
File diff suppressed because it is too large
Load Diff
2
web/composables/gql/index.ts
Normal file
2
web/composables/gql/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./fragment-masking";
|
||||
export * from "./gql";
|
||||
48
web/composables/installPlugin.ts
Normal file
48
web/composables/installPlugin.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
const useInstallPlugin = () => {
|
||||
const install = (payload = { staging: false, update: false }) => {
|
||||
console.debug('[useInstallPlugin.install]', { payload });
|
||||
try {
|
||||
const file = `https://sfo2.digitaloceanspaces.com/unraid-dl/unraid-api/dynamix.unraid.net${payload?.staging ? '.staging.plg' : '.plg'}`;
|
||||
console.debug('[useInstallPlugin.install]', file);
|
||||
|
||||
if (!payload.update) {
|
||||
// after initial install, the dropdown store looks for this to automatically open the launchpad dropdown
|
||||
sessionStorage.setItem('clickedInstallPlugin', '1');
|
||||
}
|
||||
const modalTitle = payload.update ? 'Updating Connect (beta)' : 'Installing Connect (beta)';
|
||||
|
||||
// @ts-ignore – `openPlugin` will be included in 6.10.4+ DefaultPageLayout
|
||||
if (typeof openPlugin === 'function') {
|
||||
console.debug('[useInstallPlugin.install] using openPlugin', file);
|
||||
|
||||
// @ts-ignore
|
||||
openPlugin(
|
||||
`plugin ${payload.update ? 'update' : 'install'} ${file}`,
|
||||
modalTitle,
|
||||
'',
|
||||
'refresh',
|
||||
);
|
||||
} else {
|
||||
console.debug('[useInstallPlugin.install] using openBox', file);
|
||||
// `openBox()` is defined in the webgui's DefaultPageLayout.php and used when openPlugin is not available
|
||||
|
||||
// @ts-ignore
|
||||
openBox(
|
||||
`/plugins/dynamix.plugin.manager/scripts/plugin&arg1=install&arg2=${file}`,
|
||||
modalTitle,
|
||||
600,
|
||||
900,
|
||||
true,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
install,
|
||||
};
|
||||
};
|
||||
|
||||
export default useInstallPlugin;
|
||||
13
web/composables/preventClose.ts
Normal file
13
web/composables/preventClose.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const preventClose = (e: { preventDefault: () => void; returnValue: string; }) => {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
alert('Closing this pop-up window while actions are being preformed may lead to unintended errors.');
|
||||
};
|
||||
|
||||
export const addPreventClose = () => {
|
||||
window.addEventListener('beforeunload', preventClose);
|
||||
};
|
||||
|
||||
export const removePreventClose = () => {
|
||||
window.removeEventListener('beforeunload', preventClose);
|
||||
};
|
||||
21
web/composables/services/keyServer.ts
Normal file
21
web/composables/services/keyServer.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { request } from '~/composables/services/request';
|
||||
|
||||
const KeyServer = request.url('https://keys.lime-technology.com');
|
||||
|
||||
export interface StartTrialPayload {
|
||||
guid: string;
|
||||
timestamp: number; // timestamp in seconds
|
||||
}
|
||||
export interface StartTrialResponse {
|
||||
license?: string;
|
||||
trial?: string
|
||||
}
|
||||
export const startTrial = (payload: StartTrialPayload) => KeyServer
|
||||
.url('/account/trial')
|
||||
.formUrl(payload)
|
||||
.post();
|
||||
|
||||
export const validateGuid = (payload: { guid: string }) => KeyServer
|
||||
.url('/validate/guid')
|
||||
.formUrl(payload)
|
||||
.post();
|
||||
25
web/composables/services/request.ts
Normal file
25
web/composables/services/request.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import wretch from 'wretch';
|
||||
import formUrl from 'wretch/addons/formUrl';
|
||||
import queryString from 'wretch/addons/queryString';
|
||||
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
|
||||
const errorsStore = useErrorsStore();
|
||||
|
||||
export const request = wretch()
|
||||
.addon(formUrl)
|
||||
.addon(queryString)
|
||||
.errorType('json')
|
||||
.resolve((response) => {
|
||||
return (
|
||||
response
|
||||
.error('Error', (error) => {
|
||||
console.log('global catch (Error class)', error);
|
||||
errorsStore.setError(error);
|
||||
})
|
||||
.error('TypeError', (error) => {
|
||||
console.log('global type error catch (TypeError class)', error);
|
||||
errorsStore.setError(error);
|
||||
})
|
||||
);
|
||||
});
|
||||
65
web/composables/services/webgui.ts
Normal file
65
web/composables/services/webgui.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { request } from '~/composables/services/request';
|
||||
/**
|
||||
* @name WebguiInstallKey
|
||||
* @description used to auto install key urls
|
||||
* @type GET - data should be passed using wretch's `.query({ url: String })`
|
||||
* @param {string} url - URL of license key
|
||||
*/
|
||||
export const WebguiInstallKey = request.url('/webGui/include/InstallKey.php');
|
||||
/**
|
||||
* @type POST
|
||||
* @dataType - `formUrl(Object)` https://github.com/elbywan/wretch#formurlinput-object--string
|
||||
* @param {string} csrf_token
|
||||
* @param {string} '#file' - ex: getters.myServersCfgPath
|
||||
* @param {string} '#section' - ex: 'remote'
|
||||
* @param {string} apikey - from key server's response
|
||||
* @param {string} avatar
|
||||
* @param {string} regWizTime - date_guid
|
||||
* @param {string} email
|
||||
* @param {string} username
|
||||
*/
|
||||
export const WebguiUpdate = request.url('/update.php');
|
||||
/**
|
||||
* @name WebguiUpdateDns
|
||||
* @dataForm formUrl
|
||||
* @description Used after Sign In to ensure URLs will work correctly
|
||||
* @note this request is delayed by 500ms to allow server to process key install fully
|
||||
* @todo potentially remove delay
|
||||
* @param csrf_token
|
||||
* @type POST
|
||||
*/
|
||||
export const WebguiUpdateDns = request.url('/webGui/include/UpdateDNS.php');
|
||||
/**
|
||||
* @name WebguiState
|
||||
* @description used to get current state of server via PHP rather than unraid-api
|
||||
* @type GET
|
||||
*/
|
||||
export const WebguiState = request.url('/plugins/dynamix.my.servers/data/server-state.php');
|
||||
/**
|
||||
* Run unraid-api command's via php requests
|
||||
*/
|
||||
export interface WebguiUnraidApiCommandPayload {
|
||||
csrf_token: string;
|
||||
command: 'report' | 'start';
|
||||
param1?: '-v'|'-vv';
|
||||
}
|
||||
export const WebguiUnraidApiCommand = async (payload: WebguiUnraidApiCommandPayload) => {
|
||||
console.debug('[WebguiUnraidApiCommand]', payload);
|
||||
if (!payload) { return console.error('[WebguiUnraidApiCommand] payload is required'); }
|
||||
|
||||
try {
|
||||
return await request
|
||||
.url('/plugins/dynamix.my.servers/include/unraid-api.php')
|
||||
.formUrl(payload)
|
||||
.post()
|
||||
.json((json) => {
|
||||
console.debug('[WebguiUnraidApiCommand]', json);
|
||||
return json;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`[WebguiUnraidApiCommand] catch failed to execute unraid-api ${command}`, error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[WebguiUnraidApiCommand] catch failed to execute unraid-api ${command}`, error);
|
||||
}
|
||||
};
|
||||
140
web/composables/time.ts
Normal file
140
web/composables/time.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import dayjs, { extend } from 'dayjs';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
/** @see https://day.js.org/docs/en/display/format#localized-formats */
|
||||
extend(localizedFormat);
|
||||
|
||||
export interface TimeStringsObject {
|
||||
years: number;
|
||||
months: number;
|
||||
days: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
firstDateWasLater: boolean;
|
||||
displaySeconds?: boolean;
|
||||
}
|
||||
|
||||
const useTimeHelper = (t: any) => {
|
||||
const buildStringFromValues = (payload: TimeStringsObject) => {
|
||||
const {
|
||||
years,
|
||||
months,
|
||||
days,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
firstDateWasLater,
|
||||
displaySeconds,
|
||||
} = payload;
|
||||
const result = [];
|
||||
|
||||
if (years) { result.push(t('year', years)); }
|
||||
if (months) { result.push(t('month', months)); }
|
||||
if (days) { result.push(t('day', days)); }
|
||||
if (hours) { result.push(t('hour', hours)); }
|
||||
if (minutes) { result.push(t('minute', minutes)); }
|
||||
if (seconds && ((!years && !months && !days && !hours && !minutes) || displaySeconds)) { result.push(t('second', seconds)); }
|
||||
if (firstDateWasLater) { result.push(t('ago')); }
|
||||
return result.join(' ');
|
||||
};
|
||||
|
||||
const formatDate = (date: number): string => dayjs(date).format('llll');
|
||||
|
||||
/**
|
||||
* Original meat and potatos from:
|
||||
* @version: 1.0.1
|
||||
* @author: huangjinlin
|
||||
* @repo: https://github.com/huangjinlin/dayjs-precise-range
|
||||
*/
|
||||
const buildValueObject = (
|
||||
yDiff: number,
|
||||
mDiff: number,
|
||||
dDiff: number,
|
||||
hourDiff: number,
|
||||
minDiff: number,
|
||||
secDiff: number,
|
||||
firstDateWasLater: boolean
|
||||
): TimeStringsObject => ({
|
||||
years: yDiff,
|
||||
months: mDiff,
|
||||
days: dDiff,
|
||||
hours: hourDiff,
|
||||
minutes: minDiff,
|
||||
seconds: secDiff,
|
||||
firstDateWasLater,
|
||||
});
|
||||
|
||||
const preciseDateDiff = (d1: dayjs.Dayjs, d2: dayjs.Dayjs): TimeStringsObject => {
|
||||
let m1 = dayjs(d1);
|
||||
let m2 = dayjs(d2);
|
||||
let firstDateWasLater;
|
||||
|
||||
if (m1.isSame(m2)) {
|
||||
return buildValueObject(0, 0, 0, 0, 0, 0, false);
|
||||
}
|
||||
if (m1.isAfter(m2)) {
|
||||
const tmp = m1;
|
||||
m1 = m2;
|
||||
m2 = tmp;
|
||||
firstDateWasLater = true;
|
||||
} else {
|
||||
firstDateWasLater = false;
|
||||
}
|
||||
|
||||
let yDiff = m2.year() - m1.year();
|
||||
let mDiff = m2.month() - m1.month();
|
||||
let dDiff = m2.date() - m1.date();
|
||||
let hourDiff = m2.hour() - m1.hour();
|
||||
let minDiff = m2.minute() - m1.minute();
|
||||
let secDiff = m2.second() - m1.second();
|
||||
|
||||
if (secDiff < 0) {
|
||||
secDiff = 60 + secDiff;
|
||||
minDiff -= 1;
|
||||
}
|
||||
if (minDiff < 0) {
|
||||
minDiff = 60 + minDiff;
|
||||
hourDiff -= 1;
|
||||
}
|
||||
if (hourDiff < 0) {
|
||||
hourDiff = 24 + hourDiff;
|
||||
dDiff -= 1;
|
||||
}
|
||||
if (dDiff < 0) {
|
||||
const daysInLastFullMonth = dayjs(`${m2.year()}-${m2.month() + 1}`).subtract(1, 'M').daysInMonth();
|
||||
if (daysInLastFullMonth < m1.date()) { // 31/01 -> 2/03
|
||||
dDiff = daysInLastFullMonth + dDiff + (m1.date() - daysInLastFullMonth);
|
||||
} else {
|
||||
dDiff = daysInLastFullMonth + dDiff;
|
||||
}
|
||||
mDiff -= 1;
|
||||
}
|
||||
if (mDiff < 0) {
|
||||
mDiff = 12 + mDiff;
|
||||
yDiff -= 1;
|
||||
}
|
||||
|
||||
return buildValueObject(yDiff, mDiff, dDiff, hourDiff, minDiff, secDiff, firstDateWasLater);
|
||||
};
|
||||
|
||||
const readableDifference = (a = '', b = '') => {
|
||||
try {
|
||||
const x = a ? dayjs(parseInt(a, 10)) : dayjs();
|
||||
const y = b ? dayjs(parseInt(b, 10)) : dayjs();
|
||||
return preciseDateDiff(x, y);
|
||||
} catch (error) {
|
||||
throw new Error('Couldn\'t calculate date difference');
|
||||
}
|
||||
};
|
||||
|
||||
const dateDiff = (time: string, countUp: boolean) => countUp ? readableDifference(time, '') : readableDifference('', time);
|
||||
|
||||
return {
|
||||
buildStringFromValues,
|
||||
dateDiff,
|
||||
formatDate,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTimeHelper;
|
||||
43
web/composables/useFocusTrap.js
Normal file
43
web/composables/useFocusTrap.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @see https://www.telerik.com/blogs/how-to-trap-focus-modal-vue-3
|
||||
*/
|
||||
import { customRef } from 'vue';
|
||||
// eslint-disable-next-line import/named
|
||||
import { createFocusTrap } from 'focus-trap';
|
||||
|
||||
const useFocusTrap = (focusTrapArgs) => {
|
||||
const trapRef = customRef((track, trigger) => {
|
||||
let $trapEl = null;
|
||||
return {
|
||||
get () {
|
||||
track();
|
||||
return $trapEl;
|
||||
},
|
||||
set (value) {
|
||||
$trapEl = value;
|
||||
value ? initFocusTrap(focusTrapArgs) : clearFocusTrap();
|
||||
trigger();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
let trap = null;
|
||||
const initFocusTrap = (focusTrapArgs) => {
|
||||
if (!trapRef.value) { return; }
|
||||
trap = createFocusTrap(trapRef.value, focusTrapArgs);
|
||||
trap.activate();
|
||||
};
|
||||
|
||||
const clearFocusTrap = () => {
|
||||
trap?.deactivate();
|
||||
trap = null;
|
||||
};
|
||||
|
||||
return {
|
||||
trapRef,
|
||||
initFocusTrap,
|
||||
clearFocusTrap,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFocusTrap;
|
||||
17
web/fix-array-type.ts
Normal file
17
web/fix-array-type.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* This function wraps constant case, that turns any string into CONSTANT_CASE
|
||||
* However, this function has a bug that, if you pass _ to it it will return an empty
|
||||
* string. This small module fixes that
|
||||
*
|
||||
* @param {string*} str
|
||||
* @return {string}
|
||||
*/
|
||||
function FixArrayType (str) {
|
||||
if (str === 'Array') {
|
||||
return 'ArrayType';
|
||||
}
|
||||
// If result is an empty string, just return the original string
|
||||
return str;
|
||||
}
|
||||
|
||||
module.exports = FixArrayType;
|
||||
7
web/helpers/functions.ts
Normal file
7
web/helpers/functions.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @name OBJ_TO_STR
|
||||
* @param {object} obj
|
||||
* @returns {String}
|
||||
* @description Output key + value for each item in the object. Adds new line after each item.
|
||||
*/
|
||||
export const OBJ_TO_STR = obj => Object.entries(obj).reduce((str, [p, val]) => `${str}${p}: ${val}\n`, '');
|
||||
31
web/helpers/urls.ts
Normal file
31
web/helpers/urls.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @todo setup .env
|
||||
*/
|
||||
const ACCOUNT = new URL(import.meta.env.VITE_ACCOUNT ?? 'https://account.unraid.net');
|
||||
const UNRAID_NET = new URL(import.meta.env.VITE_UNRAID_NET ?? 'https://unraid.net');
|
||||
|
||||
const ACCOUNT_CALLBACK = new URL('c', ACCOUNT);
|
||||
const CONNECT_DOCS = new URL('https://docs.unraid.net/category/unraid-connect');
|
||||
const CONNECT_DASHBOARD = new URL(import.meta.env.VITE_CONNECT ?? 'https://connect.myunraid.net');
|
||||
const CONNECT_FORUMS = new URL('https://forums.unraid.net/forum/94-connect-plugin-support/');
|
||||
const CONTACT = new URL('/contact', UNRAID_NET);
|
||||
const DEV_GRAPH_URL = '';
|
||||
const DISCORD = new URL('https://discord.gg/unraid');
|
||||
const PURCHASE_CALLBACK = new URL('/c', UNRAID_NET);
|
||||
|
||||
const SETTINGS_MANAGMENT_ACCESS = new URL('/Settings/ManagementAccess', window.location.origin);
|
||||
const PLUGIN_SETTINGS = new URL('#UnraidNetSettings', SETTINGS_MANAGMENT_ACCESS);
|
||||
|
||||
export {
|
||||
ACCOUNT,
|
||||
ACCOUNT_CALLBACK,
|
||||
CONNECT_DASHBOARD,
|
||||
CONNECT_DOCS,
|
||||
CONNECT_FORUMS,
|
||||
CONTACT,
|
||||
DEV_GRAPH_URL,
|
||||
DISCORD,
|
||||
PURCHASE_CALLBACK,
|
||||
PLUGIN_SETTINGS,
|
||||
SETTINGS_MANAGMENT_ACCESS,
|
||||
};
|
||||
13
web/layouts/default.vue
Normal file
13
web/layouts/default.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<client-only>
|
||||
<div class="flex flex-row items-center justify-center gap-6 p-6 text-gray-200 bg-zinc-800">
|
||||
<NuxtLink to="/" class="underline hover:no-underline focus:no-underline" active-class="text-orange">
|
||||
Test Vue Components
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/webComponents" class="underline hover:no-underline focus:no-underline" active-class="text-orange">
|
||||
Test Web Components
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<slot />
|
||||
</client-only>
|
||||
</template>
|
||||
200
web/locales/_template.json
Normal file
200
web/locales/_template.json
Normal file
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"LAN IP": "",
|
||||
"LAN IP {0}": "",
|
||||
"LAN IP Copied": "",
|
||||
"Click to Copy LAN IP {0}": "",
|
||||
"Trial Key Expired at {0}": "",
|
||||
"Trial Key Expires at {0}": "",
|
||||
"Trial Key Expired {0}": "",
|
||||
"Trial Key Expires in {0}": "",
|
||||
"Server Up Since {0}": "",
|
||||
"Uptime {0}": "",
|
||||
"year": "",
|
||||
"month": "",
|
||||
"day": "",
|
||||
"hour": "",
|
||||
"minute": "",
|
||||
"second": "",
|
||||
"ago": "",
|
||||
"Purchase": "",
|
||||
"Upgrade": "",
|
||||
"Fix Error": "",
|
||||
"Get Started": "",
|
||||
"Trial Expired, see options below": "",
|
||||
"Learn more about the error": "",
|
||||
"Close Dropdown": "",
|
||||
"Open Dropdown": "",
|
||||
"Thank you for installing Connect!": "",
|
||||
"Sign In to your Unraid.net account to get started": "",
|
||||
"Go to Connect": "",
|
||||
"Opens Connect in new tab": "",
|
||||
"Manage Unraid.net Account": "",
|
||||
"Manage Unraid.net Account in new tab": "",
|
||||
"Settings": "",
|
||||
"Go to Connect plugin settings": "",
|
||||
"Enhance your Unraid experience with Connect": "",
|
||||
"Beta": "",
|
||||
"Loading": "",
|
||||
"Restarting unraid-api…": "",
|
||||
"unraid-api is offline": "",
|
||||
"Introducing Unraid Connect": "",
|
||||
"Enhance your Unraid experience": "",
|
||||
"Connected": "",
|
||||
"Dynamic Remote Access": "",
|
||||
"Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.": "",
|
||||
"Manage Your Server Within Connect": "",
|
||||
"Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.": "",
|
||||
"Deep Linking": "",
|
||||
"The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.": "",
|
||||
"Online Flash Backup": "",
|
||||
"Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.": "",
|
||||
"Real-time Monitoring": "",
|
||||
"Get an overview of your server's state, storage space, apps and VMs status, and more.": "",
|
||||
"Customizable Dashboard Tiles": "",
|
||||
"Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard.": "",
|
||||
"License Management": "",
|
||||
"Manage your license keys at any time via the My Keys section.": "",
|
||||
"Plus more on the way": "",
|
||||
"All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.": "",
|
||||
"Checkout the Connect Documentation": "",
|
||||
"No thanks": "",
|
||||
"Learn more": "",
|
||||
"Install Connect": "",
|
||||
"Close Modal": "",
|
||||
"Close": "",
|
||||
"Reload": "",
|
||||
"Unraid logo animating with a wave like effect": "",
|
||||
"Click to close modal": "",
|
||||
"Error": "",
|
||||
"Performing actions": "",
|
||||
"Success!": "",
|
||||
"Something went wrong": "",
|
||||
"Please keep this window open while we perform some actions": "",
|
||||
"You're one step closer to enhancing your Unraid experience": "",
|
||||
"Thank you for purchasing an Unraid {0} Key!": "",
|
||||
"Your {0} Key has been replaced!": "",
|
||||
"Your Trial key has been extended!": "",
|
||||
"Copied": "",
|
||||
"Copy Key URL": "",
|
||||
"Copy your Key URL: {0}": "",
|
||||
"Then go to Tools > Registration to manually install it": "",
|
||||
"Enhance your experience with Unraid Connect": "",
|
||||
"Sign In to utilize Unraid Connect": "",
|
||||
"Configure Connect Features": "",
|
||||
"The primary method of support for Unraid Connect is through our forums and Discord.": "",
|
||||
"If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.": "",
|
||||
"The logs may contain sensitive information so do not post them publicly.": "",
|
||||
"Download unraid-api Logs": "",
|
||||
"Unraid Connect Forums": "",
|
||||
"Unraid Discord": "",
|
||||
"Unraid Contact Page": "",
|
||||
"DNS issue, unable to resolve wanip4.unraid.net": "",
|
||||
"Unable to fetch client WAN IPv4": "",
|
||||
"Checking WAN IPs…": "",
|
||||
"Remark: your WAN IPv4 is {0}": "",
|
||||
"Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.": "",
|
||||
"This may indicate a complex network that will not work with this Remote Access solution.": "",
|
||||
"Ignore this message if you are currently connected via Remote Access or VPN.": "",
|
||||
"Ready to update Connect account configuration": "",
|
||||
"Signing in {0}…": "",
|
||||
"Signing out {0}…": "",
|
||||
"{0} Signed In Successfully": "",
|
||||
"{0} Signed Out Successfully": "",
|
||||
"Sign In Failed": "",
|
||||
"Sign Out Failed": "",
|
||||
"Failed to update Connect account configuration": "",
|
||||
"Callback redirect type not present or incorrect": "",
|
||||
"Failed to install key": "",
|
||||
"Ready to Install Key": "",
|
||||
"Installing Extended Trial": "",
|
||||
"Installing Recovered": "",
|
||||
"Installing Replaced": "",
|
||||
"{0} {1} Key…": "",
|
||||
"{1} Key {0} Successfully": "",
|
||||
"Failed to {0} {1} Key": "",
|
||||
"Purchase Key": "",
|
||||
"Upgrade Key": "",
|
||||
"Recover Key": "",
|
||||
"Redeem Activation Code": "",
|
||||
"Replace Key": "",
|
||||
"Sign In with Unraid.net Account": "",
|
||||
"Sign Out of Unraid.net": "",
|
||||
"Extend Trial": "",
|
||||
"Start Free 30 Day Trial": "",
|
||||
"Go to Management Access Now": "",
|
||||
"Contact Support": "",
|
||||
"Learn More": "",
|
||||
"No Keyfile": "",
|
||||
"Let's Unleash your Hardware!": "",
|
||||
"<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>": "",
|
||||
"Trial": "",
|
||||
"Thank you for choosing Unraid OS!": "",
|
||||
"<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>": "",
|
||||
"Trial Expired": "",
|
||||
"Your Trial has expired": "",
|
||||
"<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>": "",
|
||||
"<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>": "",
|
||||
"Basic": "",
|
||||
"<p>Register for Connect by signing in to your Unraid.net account</p>": "",
|
||||
"<p>To support more storage devices as your server grows, click Upgrade Key.</p>": "",
|
||||
"Plus": "",
|
||||
"Pro": "",
|
||||
"Flash GUID Error": "",
|
||||
"Registration key / USB Flash GUID mismatch": "",
|
||||
"<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "",
|
||||
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>": "",
|
||||
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "",
|
||||
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>": "",
|
||||
"Multiple License Keys Present": "",
|
||||
"<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>": "",
|
||||
"Missing key file": "",
|
||||
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "",
|
||||
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "",
|
||||
"Invalid installation": "",
|
||||
"<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>": "",
|
||||
"No USB flash configuration data": "",
|
||||
"<p>There is a problem with your USB Flash device</p>": "",
|
||||
"No Flash": "",
|
||||
"Cannot access your USB Flash boot device": "",
|
||||
"<p>There is a physical problem accessing your USB Flash boot device</p>": "",
|
||||
"BLACKLISTED": "",
|
||||
"Blacklisted USB Flash GUID": "",
|
||||
"<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique – this is common with USB card readers.</p>": "",
|
||||
"USB Flash device error": "",
|
||||
"<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>": "",
|
||||
"USB Flash has no serial number": "",
|
||||
"Trial Requires Internet Connection": "",
|
||||
"Cannot validate Unraid Trial key": "",
|
||||
"<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>": "",
|
||||
"Stale": "",
|
||||
"Stale Server": "",
|
||||
"<p>Please refresh the page to ensure you load your latest configuration</p>": "",
|
||||
"Invalid API Key": "",
|
||||
"Please sign out then sign back in to refresh your API key.": "",
|
||||
"Invalid API Key Format": "",
|
||||
"Too Many Devices": "",
|
||||
"You have exceeded the number of devices allowed for your license. Please remove a device before adding another.": "",
|
||||
"Unraid Connect Install Failed": "",
|
||||
"Rebooting will likely solve this.": "",
|
||||
"SSL certificates for unraid.net deprecated": "",
|
||||
"On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.": "",
|
||||
"Unraid Connect Error": "",
|
||||
"Trial Key Creation Failed": "",
|
||||
"Error creatiing a trial key. Please try again later.": "",
|
||||
"Extending your free trial by 15 days": "",
|
||||
"Please keep this window open": "",
|
||||
"Starting your free 30 day trial": "",
|
||||
"Trial Key Created": "",
|
||||
"Please wait while the page reloads to install your trial key": "",
|
||||
"A Trial key provides all the functionality of a Pro Registration key": "",
|
||||
"Extension Installed": "",
|
||||
"Recovered": "",
|
||||
"Replaced": "",
|
||||
"Installing": "",
|
||||
"Installed": "",
|
||||
"Install": "",
|
||||
"Install Extended": "",
|
||||
"Install Recovered": "",
|
||||
"Install Replaced": "",
|
||||
"Your free Trial key provides all the functionality of a Pro Registration key": ""
|
||||
}
|
||||
200
web/locales/en_US.json
Normal file
200
web/locales/en_US.json
Normal file
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"LAN IP": "LAN IP",
|
||||
"LAN IP {0}": "LAN IP {0}",
|
||||
"LAN IP Copied": "LAN IP Copied",
|
||||
"Click to Copy LAN IP {0}": "Click to Copy LAN IP {0}",
|
||||
"Trial Key Expired at {0}": "Trial Key Expired at {0}",
|
||||
"Trial Key Expires at {0}": "Trial Key Expires at {0}",
|
||||
"Trial Key Expired {0}": "Trial Key Expired {0}",
|
||||
"Trial Key Expires in {0}": "Trial Key Expires in {0}",
|
||||
"Server Up Since {0}": "Server Up Since {0}",
|
||||
"Uptime {0}": "Uptime {0}",
|
||||
"year": "{n} year | {n} years",
|
||||
"month": "{n} month | {n} months",
|
||||
"day": "{n} day | {n} days",
|
||||
"hour": "{n} hour | {n} hours",
|
||||
"minute": "{n} minute | {n} minutes",
|
||||
"second": "{n} second | {n} seconds",
|
||||
"ago": "ago",
|
||||
"Purchase": "Purchase",
|
||||
"Upgrade": "Upgrade",
|
||||
"Fix Error": "Fix Error",
|
||||
"Get Started": "Get Started",
|
||||
"Trial Expired, see options below": "Trial Expired, see options below",
|
||||
"Learn more about the error": "Learn more about the error",
|
||||
"Close Dropdown": "Close Dropdown",
|
||||
"Open Dropdown": "Open Dropdown",
|
||||
"Thank you for installing Connect!": "Thank you for installing Connect!",
|
||||
"Sign In to your Unraid.net account to get started": "Sign In to your Unraid.net account to get started",
|
||||
"Go to Connect": "Go to Connect",
|
||||
"Opens Connect in new tab": "Opens Connect in new tab",
|
||||
"Manage Unraid.net Account": "Manage Unraid.net Account",
|
||||
"Manage Unraid.net Account in new tab": "Manage Unraid.net Account in new tab",
|
||||
"Settings": "Settings",
|
||||
"Go to Connect plugin settings": "Go to Connect plugin settings",
|
||||
"Enhance your Unraid experience with Connect": "Enhance your Unraid experience with Connect",
|
||||
"Beta": "Beta",
|
||||
"Loading": "Loading",
|
||||
"Restarting unraid-api…": "Restarting unraid-api…",
|
||||
"unraid-api is offline": "unraid-api is offline",
|
||||
"Introducing Unraid Connect": "Introducing Unraid Connect",
|
||||
"Enhance your Unraid experience": "Enhance your Unraid experience",
|
||||
"Connected": "Connected",
|
||||
"Dynamic Remote Access": "Dynamic Remote Access",
|
||||
"Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.": "Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.",
|
||||
"Manage Your Server Within Connect": "Manage Your Server Within Connect",
|
||||
"Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.": "Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.",
|
||||
"Deep Linking": "Deep Linking",
|
||||
"The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.": "The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.",
|
||||
"Online Flash Backup": "Online Flash Backup",
|
||||
"Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.": "Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.",
|
||||
"Real-time Monitoring": "Real-time Monitoring",
|
||||
"Get an overview of your server's state, storage space, apps and VMs status, and more.": "Get an overview of your server's state, storage space, apps and VMs status, and more.",
|
||||
"Customizable Dashboard Tiles": "Customizable Dashboard Tiles",
|
||||
"Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard.": "Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard.",
|
||||
"License Management": "License Management",
|
||||
"Manage your license keys at any time via the My Keys section.": "Manage your license keys at any time via the My Keys section.",
|
||||
"Plus more on the way": "Plus more on the way",
|
||||
"All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.": "All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.",
|
||||
"Checkout the Connect Documentation": "Checkout the Connect Documentation",
|
||||
"No thanks": "No thanks",
|
||||
"Learn more": "Learn more",
|
||||
"Install Connect": "Install Connect",
|
||||
"Close Modal": "Close Modal",
|
||||
"Close": "Close",
|
||||
"Reload": "Reload",
|
||||
"Unraid logo animating with a wave like effect": "Unraid logo animating with a wave like effect",
|
||||
"Click to close modal": "Click to close modal",
|
||||
"Error": "Error",
|
||||
"Performing actions": "Performing actions",
|
||||
"Success!": "Success!",
|
||||
"Something went wrong": "Something went wrong",
|
||||
"Please keep this window open while we perform some actions": "Please keep this window open while we perform some actions",
|
||||
"You're one step closer to enhancing your Unraid experience": "You're one step closer to enhancing your Unraid experience",
|
||||
"Thank you for purchasing an Unraid {0} Key!": "Thank you for purchasing an Unraid {0} Key!",
|
||||
"Your {0} Key has been replaced!": "Your {0} Key has been replaced!",
|
||||
"Your Trial key has been extended!": "Your Trial key has been extended!",
|
||||
"Copied": "Copied",
|
||||
"Copy Key URL": "Copy Key URL",
|
||||
"Copy your Key URL: {0}": "Copy your Key URL: {0}",
|
||||
"Then go to Tools > Registration to manually install it": "Then go to Tools > Registration to manually install it",
|
||||
"Enhance your experience with Unraid Connect": "Enhance your experience with Unraid Connect",
|
||||
"Sign In to utilize Unraid Connect": "Sign In to utilize Unraid Connect",
|
||||
"Configure Connect Features": "Configure Connect Features",
|
||||
"The primary method of support for Unraid Connect is through our forums and Discord.": "The primary method of support for Unraid Connect is through our forums and Discord.",
|
||||
"If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.": "If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.",
|
||||
"The logs may contain sensitive information so do not post them publicly.": "The logs may contain sensitive information so do not post them publicly.",
|
||||
"Download unraid-api Logs": "Download unraid-api Logs",
|
||||
"Unraid Connect Forums": "Unraid Connect Forums",
|
||||
"Unraid Discord": "Unraid Discord",
|
||||
"Unraid Contact Page": "Unraid Contact Page",
|
||||
"DNS issue, unable to resolve wanip4.unraid.net": "DNS issue, unable to resolve wanip4.unraid.net",
|
||||
"Unable to fetch client WAN IPv4": "Unable to fetch client WAN IPv4",
|
||||
"Checking WAN IPs…": "Checking WAN IPs…",
|
||||
"Remark: your WAN IPv4 is {0}": "Remark: your WAN IPv4 is {0}",
|
||||
"Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.": "Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.",
|
||||
"This may indicate a complex network that will not work with this Remote Access solution.": "This may indicate a complex network that will not work with this Remote Access solution.",
|
||||
"Ignore this message if you are currently connected via Remote Access or VPN.": "Ignore this message if you are currently connected via Remote Access or VPN.",
|
||||
"Ready to update Connect account configuration": "Ready to update Connect account configuration",
|
||||
"Signing in {0}…": "Signing in {0}…",
|
||||
"Signing out {0}…": "Signing out {0}…",
|
||||
"{0} Signed In Successfully": "{0} Signed In Successfully",
|
||||
"{0} Signed Out Successfully": "{0} Signed Out Successfully",
|
||||
"Sign In Failed": "Sign In Failed",
|
||||
"Sign Out Failed": "Sign Out Failed",
|
||||
"Failed to update Connect account configuration": "Failed to update Connect account configuration",
|
||||
"Callback redirect type not present or incorrect": "Callback redirect type not present or incorrect",
|
||||
"Failed to install key": "Failed to install key",
|
||||
"Ready to Install Key": "Ready to Install Key",
|
||||
"Installing Extended Trial": "Installing Extended Trial",
|
||||
"Installing Recovered": "Installing Recovered",
|
||||
"Installing Replaced": "Installing Replaced",
|
||||
"{0} {1} Key…": "{0} {1} Key…",
|
||||
"{1} Key {0} Successfully": "{1} Key {0} Successfully",
|
||||
"Failed to {0} {1} Key": "Failed to {0} {1} Key",
|
||||
"Purchase Key": "Purchase Key",
|
||||
"Upgrade Key": "Upgrade Key",
|
||||
"Recover Key": "Recover Key",
|
||||
"Redeem Activation Code": "Redeem Activation Code",
|
||||
"Replace Key": "Replace Key",
|
||||
"Sign In with Unraid.net Account": "Sign In with Unraid.net Account",
|
||||
"Sign Out of Unraid.net": "Sign Out of Unraid.net",
|
||||
"Extend Trial": "Extend Trial",
|
||||
"Start Free 30 Day Trial": "Start Free 30 Day Trial",
|
||||
"Go to Management Access Now": "Go to Management Access Now",
|
||||
"Contact Support": "Contact Support",
|
||||
"Learn More": "Learn More",
|
||||
"No Keyfile": "No Keyfile",
|
||||
"Let's Unleash your Hardware!": "Let's Unleash your Hardware!",
|
||||
"<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>": "<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>",
|
||||
"Trial": "Trial",
|
||||
"Thank you for choosing Unraid OS!": "Thank you for choosing Unraid OS!",
|
||||
"<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>": "<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>",
|
||||
"Trial Expired": "Trial Expired",
|
||||
"Your Trial has expired": "Your Trial has expired",
|
||||
"<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>": "<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>",
|
||||
"<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>": "<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>",
|
||||
"Basic": "Basic",
|
||||
"<p>Register for Connect by signing in to your Unraid.net account</p>": "<p>Register for Connect by signing in to your Unraid.net account</p>",
|
||||
"<p>To support more storage devices as your server grows, click Upgrade Key.</p>": "<p>To support more storage devices as your server grows, click Upgrade Key.</p>",
|
||||
"Plus": "Plus",
|
||||
"Pro": "Pro",
|
||||
"Flash GUID Error": "Flash GUID Error",
|
||||
"Registration key / USB Flash GUID mismatch": "Registration key / USB Flash GUID mismatch",
|
||||
"<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>",
|
||||
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>",
|
||||
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>",
|
||||
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>",
|
||||
"Multiple License Keys Present": "Multiple License Keys Present",
|
||||
"<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>": "<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>",
|
||||
"Missing key file": "Missing key file",
|
||||
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>",
|
||||
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>",
|
||||
"Invalid installation": "Invalid installation",
|
||||
"<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>": "<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>",
|
||||
"No USB flash configuration data": "No USB flash configuration data",
|
||||
"<p>There is a problem with your USB Flash device</p>": "<p>There is a problem with your USB Flash device</p>",
|
||||
"No Flash": "No Flash",
|
||||
"Cannot access your USB Flash boot device": "Cannot access your USB Flash boot device",
|
||||
"<p>There is a physical problem accessing your USB Flash boot device</p>": "<p>There is a physical problem accessing your USB Flash boot device</p>",
|
||||
"BLACKLISTED": "BLACKLISTED",
|
||||
"Blacklisted USB Flash GUID": "Blacklisted USB Flash GUID",
|
||||
"<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique – this is common with USB card readers.</p>": "<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique – this is common with USB card readers.</p>",
|
||||
"USB Flash device error": "USB Flash device error",
|
||||
"<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>": "<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>",
|
||||
"USB Flash has no serial number": "USB Flash has no serial number",
|
||||
"Trial Requires Internet Connection": "Trial Requires Internet Connection",
|
||||
"Cannot validate Unraid Trial key": "Cannot validate Unraid Trial key",
|
||||
"<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>": "<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>",
|
||||
"Stale": "Stale",
|
||||
"Stale Server": "Stale Server",
|
||||
"<p>Please refresh the page to ensure you load your latest configuration</p>": "<p>Please refresh the page to ensure you load your latest configuration</p>",
|
||||
"Invalid API Key": "Invalid API Key",
|
||||
"Please sign out then sign back in to refresh your API key.": "Please sign out then sign back in to refresh your API key.",
|
||||
"Invalid API Key Format": "Invalid API Key Format",
|
||||
"Too Many Devices": "Too Many Devices",
|
||||
"You have exceeded the number of devices allowed for your license. Please remove a device before adding another.": "You have exceeded the number of devices allowed for your license. Please remove a device before adding another.",
|
||||
"Unraid Connect Install Failed": "Unraid Connect Install Failed",
|
||||
"Rebooting will likely solve this.": "Rebooting will likely solve this.",
|
||||
"SSL certificates for unraid.net deprecated": "SSL certificates for unraid.net deprecated",
|
||||
"On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.": "On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.",
|
||||
"Unraid Connect Error": "Unraid Connect Error",
|
||||
"Trial Key Creation Failed": "Trial Key Creation Failed",
|
||||
"Error creatiing a trial key. Please try again later.": "Error creatiing a trial key. Please try again later.",
|
||||
"Extending your free trial by 15 days": "Extending your free trial by 15 days",
|
||||
"Please keep this window open": "Please keep this window open",
|
||||
"Starting your free 30 day trial": "Starting your free 30 day trial",
|
||||
"Trial Key Created": "Trial Key Created",
|
||||
"Please wait while the page reloads to install your trial key": "Please wait while the page reloads to install your trial key",
|
||||
"A Trial key provides all the functionality of a Pro Registration key": "A Trial key provides all the functionality of a Pro Registration key.",
|
||||
"Extension Installed": "Extension Installed",
|
||||
"Recovered": "Recovered",
|
||||
"Replaced": "Replaced",
|
||||
"Installing": "Installing",
|
||||
"Installed": "Installed",
|
||||
"Install": "Install",
|
||||
"Install Extended": "Install Extended",
|
||||
"Install Recovered": "Install Recovered",
|
||||
"Install Replaced": "Install Replaced",
|
||||
"Your free Trial key provides all the functionality of a Pro Registration key": "Your free Trial key provides all the functionality of a Pro Registration key"
|
||||
}
|
||||
200
web/locales/ja.json
Normal file
200
web/locales/ja.json
Normal file
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"LAN IP": "LAN IP",
|
||||
"LAN IP {0}": "LAN IP {0}",
|
||||
"LAN IP Copied": "LAN IPがコピーされました",
|
||||
"Click to Copy LAN IP {0}": "クリックして LAN IP {0} をコピーします",
|
||||
"Trial Key Expired at {0}": "トライアル キーの有効期限は {0} に切れます",
|
||||
"Trial Key Expires at {0}": "試用版キーの有効期限は {0} に切れます",
|
||||
"Trial Key Expired {0}": "トライアルキーの有効期限が切れました {0}",
|
||||
"Trial Key Expires in {0}": "トライアル キーの有効期限は {0} に切れます",
|
||||
"Server Up Since {0}": "{0}以降サーバー稼働中",
|
||||
"Uptime {0}": "稼働時間 {0}",
|
||||
"year": "{n} 年 | {n}年",
|
||||
"month": "{n} 月 | {n}か月",
|
||||
"day": "{n} 日 | {n}日",
|
||||
"hour": "{n} 時間 | {n}時間",
|
||||
"minute": "{n} 分 | {n}分",
|
||||
"second": "{n} 秒 | {n}秒",
|
||||
"ago": "前",
|
||||
"Purchase": "購入",
|
||||
"Upgrade": "アップグレード",
|
||||
"Fix Error": "エラーを修正",
|
||||
"Get Started": "始めましょう",
|
||||
"Trial Expired, see options below": "試用期限が切れました。以下のオプションを参照してください",
|
||||
"Learn more about the error": "エラーの詳細を確認する",
|
||||
"Close Dropdown": "ドロップダウンを閉じる",
|
||||
"Open Dropdown": "ドロップダウンを開く",
|
||||
"Thank you for installing Connect!": "Connect をインストールしていただきありがとうございます。",
|
||||
"Sign In to your Unraid.net account to get started": "Unraid.net アカウントにサインインして開始してください",
|
||||
"Go to Connect": "「接続」に移動",
|
||||
"Opens Connect in new tab": "新しいタブで接続を開きます",
|
||||
"Manage Unraid.net Account": "Unraid.netアカウントの管理",
|
||||
"Manage Unraid.net Account in new tab": "新しいタブでUnraid.netアカウントを管理",
|
||||
"Settings": "設定",
|
||||
"Go to Connect plugin settings": "接続プラグイン設定に移動します",
|
||||
"Enhance your Unraid experience with Connect": "Connect で Unraid エクスペリエンスを強化",
|
||||
"Beta": "ベータ",
|
||||
"Loading": "読み込み中",
|
||||
"Restarting unraid-api…": "unraid-API を再起動しています…",
|
||||
"unraid-api is offline": "unraid-API はオフラインです",
|
||||
"Introducing Unraid Connect": "Unraid Connect の紹介",
|
||||
"Enhance your Unraid experience": "Unraid エクスペリエンスを強化する",
|
||||
"Connected": "接続済み",
|
||||
"Dynamic Remote Access": "ダイナミックリモートアクセス",
|
||||
"Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.": "動的リモート アクセスを使用してサーバー アクセシビリティのオン/オフを切り替えます。 ボタンをクリックするだけで UPnP が自動的にオンになり、ルーター上でランダムな WAN ポートが開き、数秒でアクセスが遮断されます。",
|
||||
"Manage Your Server Within Connect": "Connect 内でサーバーを管理",
|
||||
"Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.": "myunraid.net 証明書を備えたサーバーは、Connect Web UI 内から直接管理できます。 同じブラウザ ウィンドウで、携帯電話、タブレット、ラップトップ、または PC から複数のサーバーを管理します。",
|
||||
"Deep Linking": "ディープリンク",
|
||||
"The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.": "Connect ダッシュボードは Webgui の関連セクションにリンクしているため、これらの設定やサーバー セクションにすばやくアクセスできます。",
|
||||
"Online Flash Backup": "オンラインフラッシュバックアップ",
|
||||
"Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.": "設定のバックアップが欠かせないことはありません。 フラッシュ ドライブを変更する必要がある場合は、Connect からバックアップを生成すると、数分で起動して実行できるようになります。",
|
||||
"Real-time Monitoring": "リアルタイム監視",
|
||||
"Get an overview of your server's state, storage space, apps and VMs status, and more.": "サーバーの状態、ストレージ容量、アプリと VM のステータスなどの概要を取得します。",
|
||||
"Customizable Dashboard Tiles": "カスタマイズ可能なダッシュボード タイル",
|
||||
"Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard.": "カスタム サーバー タイルを好みに設定し、サーバーのバナー画像を Connect ダッシュボードに自動的に表示します。",
|
||||
"License Management": "ライセンス管理",
|
||||
"Manage your license keys at any time via the My Keys section.": "[マイ キー] セクションでいつでもライセンス キーを管理できます。",
|
||||
"Plus more on the way": "さらに途中でさらに追加",
|
||||
"All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.": "必要なのは、アクティブなインターネット接続、Unraid.net アカウント、および Connect プラグインだけです。 プラグインをインストールして始めます。",
|
||||
"Checkout the Connect Documentation": "Connect のドキュメントを確認する",
|
||||
"No thanks": "結構です",
|
||||
"Learn more": "もっと詳しく知る",
|
||||
"Install Connect": "インストール接続",
|
||||
"Close Modal": "モーダルを閉じる",
|
||||
"Close": "近い",
|
||||
"Reload": "リロード",
|
||||
"Unraid logo animating with a wave like effect": "波のような効果でアニメーション化された Unraid ロゴ",
|
||||
"Click to close modal": "クリックしてモーダルを閉じます",
|
||||
"Error": "エラー",
|
||||
"Performing actions": "アクションの実行",
|
||||
"Success!": "成功!",
|
||||
"Something went wrong": "何か問題が発生しました",
|
||||
"Please keep this window open while we perform some actions": "いくつかのアクションを実行する間、このウィンドウを開いたままにしてください",
|
||||
"You're one step closer to enhancing your Unraid experience": "Unraid エクスペリエンスの向上にまた一歩近づいています",
|
||||
"Thank you for purchasing an Unraid {0} Key!": "Unraid {0} キーをご購入いただきありがとうございます。",
|
||||
"Your {0} Key has been replaced!": "{0} キーは交換されました。",
|
||||
"Your Trial key has been extended!": "トライアルキーは延長されました!",
|
||||
"Copied": "コピーされました",
|
||||
"Copy Key URL": "キーの URL をコピー",
|
||||
"Copy your Key URL: {0}": "キーの URL をコピーします: {0}",
|
||||
"Then go to Tools > Registration to manually install it": "次に、[ツール] > [登録] に移動して手動でインストールします。",
|
||||
"Enhance your experience with Unraid Connect": "Unraid Connect でエクスペリエンスを向上",
|
||||
"Sign In to utilize Unraid Connect": "Unraid Connect を利用するにはサインインしてください",
|
||||
"Configure Connect Features": "接続機能の構成",
|
||||
"The primary method of support for Unraid Connect is through our forums and Discord.": "Unraid Connect の主なサポート方法は、フォーラムと Discord を使用することです。",
|
||||
"If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.": "ログの提供を求められた場合は、お問い合わせページでサポート リクエストを開き、受信した電子メール メッセージにログを添付してご返信ください。",
|
||||
"The logs may contain sensitive information so do not post them publicly.": "ログには機密情報が含まれている可能性があるため、公開しないでください。",
|
||||
"Download unraid-api Logs": "unraid-api ログをダウンロードする",
|
||||
"Unraid Connect Forums": "Unraid Connect フォーラム",
|
||||
"Unraid Discord": "アンレイドディスコード",
|
||||
"Unraid Contact Page": "アンレイド連絡先ページ",
|
||||
"DNS issue, unable to resolve wanip4.unraid.net": "DNS の問題、wanip4.unraid.net を解決できない",
|
||||
"Unable to fetch client WAN IPv4": "クライアントの WAN IPv4 を取得できません",
|
||||
"Checking WAN IPs…": "WAN IPをチェック中…",
|
||||
"Remark: your WAN IPv4 is {0}": "注: WAN IPv4 は {0} です",
|
||||
"Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.": "注: Unraid の WAN IPv4 {0} はクライアントの WAN IPv4 {1} と一致しません。",
|
||||
"This may indicate a complex network that will not work with this Remote Access solution.": "これは、このリモート アクセス ソリューションでは機能しない複雑なネットワークを示している可能性があります。",
|
||||
"Ignore this message if you are currently connected via Remote Access or VPN.": "現在リモート アクセスまたは VPN 経由で接続している場合は、このメッセージを無視してください。",
|
||||
"Ready to update Connect account configuration": "Connect アカウント構成を更新する準備ができました",
|
||||
"Signing in {0}…": "{0} にサインインしています…",
|
||||
"Signing out {0}…": "{0} からログアウトしています…",
|
||||
"{0} Signed In Successfully": "{0} が正常にサインインしました",
|
||||
"{0} Signed Out Successfully": "{0} が正常にサインアウトしました",
|
||||
"Sign In Failed": "サインインに失敗しました",
|
||||
"Sign Out Failed": "サインアウトに失敗しました",
|
||||
"Failed to update Connect account configuration": "Connect アカウント構成の更新に失敗しました",
|
||||
"Callback redirect type not present or incorrect": "コールバック リダイレクト タイプが存在しないか、正しくありません",
|
||||
"Failed to install key": "キーのインストールに失敗しました",
|
||||
"Ready to Install Key": "キーをインストールする準備ができました",
|
||||
"Installing Extended Trial": "延長トライアル版のインストール",
|
||||
"Installing Recovered": "回復されたものをインストールしています",
|
||||
"Installing Replaced": "取り付け、交換",
|
||||
"{0} {1} Key…": "{0} {1} キー…",
|
||||
"{1} Key {0} Successfully": "{1}キー{0}が成功しました",
|
||||
"Failed to {0} {1} Key": "{0} {1} キーに失敗しました",
|
||||
"Purchase Key": "キーを購入する",
|
||||
"Upgrade Key": "アップグレードキー",
|
||||
"Recover Key": "キーを回復する",
|
||||
"Redeem Activation Code": "アクティベーション コードを引き換える",
|
||||
"Replace Key": "キーを交換する",
|
||||
"Sign In with Unraid.net Account": "Unraid.net アカウントでサインインする",
|
||||
"Sign Out of Unraid.net": "Unraid.net からサインアウトする",
|
||||
"Extend Trial": "トライアルの延長",
|
||||
"Start Free 30 Day Trial": "30 日間の無料トライアルを開始する",
|
||||
"Go to Management Access Now": "今すぐ管理アクセスに移動",
|
||||
"Contact Support": "サポート問い合わせ先",
|
||||
"Learn More": "もっと詳しく知る",
|
||||
"No Keyfile": "キーファイルがありません",
|
||||
"Let's Unleash your Hardware!": "ハードウェアを解き放ちましょう!",
|
||||
"<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>": "<p>登録キーを購入するか、30 日間の無料の<em>トライアル</em>キーをインストールするまで、サーバーは使用できません。 \n<em>トライアル</em> キーは、Pro 登録キーのすべての機能を提供します。</p><p>登録キーは、USB フラッシュ ブート デバイスのシリアル番号 (GUID) にバインドされています。\nサイズが少なくとも 1 GB の高品質の有名ブランドのデバイスを使用してください。</p><p>注: USB メモリ カード リーダーのほとんどは固有のシリアル番号を提示していないため、通常はサポートされていません。</p><p><strong>\n重要:</strong></p><ul class='list-disc pl-16px'><li>サーバー時間が 5 分以内であることを確認してください。</li><li>\n指定された DNS サーバー</li></ul>",
|
||||
"Trial": "トライアル",
|
||||
"Thank you for choosing Unraid OS!": "Unraid OS をお選びいただきありがとうございます。",
|
||||
"<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>": "<p><em>トライアル</em> キーには、<em>プロ</em> キーのすべての機能とデバイス サポートが含まれています。</p><p><em>トライアル</em> の終了後\n有効期限に達しても、次回アレイを停止するかサーバーを再起動するまで、 サーバーは<strong>通常どおり機能</strong>します。</p><p>その時点で、ライセンス キーを購入するか、<em>ライセンス キーをリクエストすることができます。 \n> トライアル</em>拡張機能。</p>",
|
||||
"Trial Expired": "トライアル期間が終了しました",
|
||||
"Your Trial has expired": "トライアル版の有効期限が切れました",
|
||||
"<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>": "<p>Unraid OS を引き続き使用するには、ライセンス キーを購入することができます。\nあるいは、トライアルの延長をリクエストすることもできます。</p>",
|
||||
"<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>": "<p>トライアル拡張機能をすべて使用しました。 \nUnraid OS を引き続き使用するには、ライセンス キーを購入することができます。</p>",
|
||||
"Basic": "基本",
|
||||
"<p>Register for Connect by signing in to your Unraid.net account</p>": "<p>Unraid.net アカウントにサインインして Connect に登録します</p>",
|
||||
"<p>To support more storage devices as your server grows, click Upgrade Key.</p>": "<p>サーバーの成長に合わせてより多くのストレージ デバイスをサポートするには、[アップグレード キー] をクリックします。</p>",
|
||||
"Plus": "プラス",
|
||||
"Pro": "プロ",
|
||||
"Flash GUID Error": "フラッシュGUIDエラー",
|
||||
"Registration key / USB Flash GUID mismatch": "登録キー / USB フラッシュ GUID の不一致",
|
||||
"<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>Unraid 登録キーは、過去 12 か月以内に交換されているため、交換の対象外です。</p>",
|
||||
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>": "<p>ライセンス キー ファイルが USB フラッシュ ブート デバイスに対応していません。\n正しいキー ファイルを USB フラッシュ ブート デバイスの /config ディレクトリにコピーするか、[キーの購入] を選択してください。</p><p>Unraid 登録キーはブラックリストに登録されているため、交換の対象外です。</p>",
|
||||
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>ライセンス キー ファイルが USB フラッシュ ブート デバイスに対応していません。\n正しいキー ファイルを USB フラッシュ ブート デバイスの /config ディレクトリにコピーするか、[キーの購入] を選択してください。</p><p>Unraid 登録キーは、過去 12 か月以内に交換されているため、交換の対象外です。</p><p> \np>",
|
||||
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>": "<p>ライセンス キー ファイルが USB フラッシュ ブート デバイスに対応していません。\n正しいキー ファイルを USB フラッシュ ブート デバイスの /config ディレクトリにコピーしてください。</p><p>キーを購入または交換してみることもできます。</p>",
|
||||
"Multiple License Keys Present": "複数のライセンス キーが存在します",
|
||||
"<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>": "<p>USB フラッシュ デバイス上に複数のライセンス キー ファイルが存在しますが、どれも USB フラッシュ ブート デバイスに対応していません。\n置き換えるファイルを除くすべてのキー ファイルを、USB フラッシュ ブート デバイスの /config ディレクトリから削除してください。</p><p>あるいは、この USB フラッシュ デバイスのライセンス キーを購入することもできます。</p> \n<p>ライセンス キーの 1 つを、この USB フラッシュ デバイスにバインドされた新しいキーに置き換える場合は、まず他のすべてのキー ファイルを削除してください。</p>",
|
||||
"Missing key file": "キーファイルがありません",
|
||||
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "<p>ライセンス キー ファイルが破損しているか、見つからないようです。\nキー ファイルは、USB フラッシュ ブート デバイスの /config ディレクトリにある必要があります。</p><p>Unraid Connect (ベータ版) がインストールされている場合は、Unraid.net アカウントを使用してキーの回復を試みることができます。</p> \n<p>期限切れの試用版インストールの場合は、ライセンス キーを購入できます。</p>",
|
||||
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "<p>ライセンス キー ファイルが破損しているか、見つからないようです。\nキー ファイルは、USB フラッシュ ブート デバイスの /config ディレクトリにある必要があります。</p><p>ライセンス キー ファイルのバックアップ コピーがない場合は、Unraid Connect (ベータ) プラグインをインストールして試すことができます。 \n</p><p>期限切れの試用版インストールの場合は、ライセンス キーを購入できます。</p>",
|
||||
"Invalid installation": "無効なインストール",
|
||||
"<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>": "<p>既存の Unraid OS インストールでトライアル キーを使用することはできません。</p><p>このインストールの使用を続けるには、この USB フラッシュ デバイスに対応するライセンス キーを購入できます。</p>",
|
||||
"No USB flash configuration data": "USB フラッシュ構成データがありません",
|
||||
"<p>There is a problem with your USB Flash device</p>": "<p>USB フラッシュ デバイスに問題があります</p>",
|
||||
"No Flash": "フラッシュ禁止",
|
||||
"Cannot access your USB Flash boot device": "USB フラッシュ ブート デバイスにアクセスできません",
|
||||
"<p>There is a physical problem accessing your USB Flash boot device</p>": "<p>USB フラッシュ ブート デバイスへのアクセスに物理的な問題があります</p>",
|
||||
"BLACKLISTED": "ブラックリストに登録されました",
|
||||
"Blacklisted USB Flash GUID": "ブラックリストに登録された USB フラッシュ GUID",
|
||||
"<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique – this is common with USB card readers.</p>": "<p>この USB フラッシュ ブート デバイスはブラックリストに登録されています。\nこの問題は、ライセンス キーを交換用の USB フラッシュ デバイスに転送し、現在古い USB フラッシュ デバイスから起動している場合に発生する可能性があります。</p><p>シリアル番号が検出された場合、USB フラッシュ デバイスもブラックリストに登録される可能性があります。\n番号は一意ではありません。これは USB カード リーダーでは一般的です。</p>",
|
||||
"USB Flash device error": "USBフラッシュデバイスエラー",
|
||||
"<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>": "<p>この USB フラッシュ デバイスには無効な GUID があります。\n別の USB フラッシュ デバイスを試してください</p>",
|
||||
"USB Flash has no serial number": "USB フラッシュにはシリアル番号がありません",
|
||||
"Trial Requires Internet Connection": "トライアルにはインターネット接続が必要です",
|
||||
"Cannot validate Unraid Trial key": "Unraid Trial キーを検証できません",
|
||||
"<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>": "<p>トライアル キーにはインターネット接続が必要です。</p><p><a href='/Settings/NetworkSettings class='underline'>[設定] > [ネットワーク] を確認してください</a></p>",
|
||||
"Stale": "古い",
|
||||
"Stale Server": "古いサーバー",
|
||||
"<p>Please refresh the page to ensure you load your latest configuration</p>": "<p>ページを更新して最新の構成をロードしてください。</p>",
|
||||
"Invalid API Key": "無効な API キー",
|
||||
"Please sign out then sign back in to refresh your API key.": "サインアウトしてから再度サインインして、API キーを更新してください。",
|
||||
"Invalid API Key Format": "無効な API キー形式",
|
||||
"Too Many Devices": "デバイスが多すぎます",
|
||||
"You have exceeded the number of devices allowed for your license. Please remove a device before adding another.": "ライセンスで許可されているデバイスの数を超えました。 別のデバイスを追加する前に、デバイスを削除してください。",
|
||||
"Unraid Connect Install Failed": "Unraid Connect のインストールに失敗しました",
|
||||
"Rebooting will likely solve this.": "再起動すると解決する可能性があります。",
|
||||
"SSL certificates for unraid.net deprecated": "unraid.net の SSL 証明書が非推奨になりました",
|
||||
"On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.": "2023 年 1 月 1 日、unraid.net の SSL 証明書は廃止されました。 新しい myunraid.net ドメインを使用するには、新しい SSL 証明書をプロビジョニングする必要があります。 これは、「設定 > 管理アクセス」ページで行うことができます。",
|
||||
"Unraid Connect Error": "非RAID接続エラー",
|
||||
"Trial Key Creation Failed": "トライアルキーの作成に失敗しました",
|
||||
"Error creatiing a trial key. Please try again later.": "キー サーバーが試用版キーを返しませんでした。 後でもう一度試してください。",
|
||||
"Extending your free trial by 15 days": "無料トライアルを 15 日間延長します",
|
||||
"Please keep this window open": "この窓は開いたままにしておいてください",
|
||||
"Starting your free 30 day trial": "30 日間の無料トライアルを開始する",
|
||||
"Trial Key Created": "トライアルキーが作成されました",
|
||||
"Please wait while the page reloads to install your trial key": "試用版キーをインストールするには、ページがリロードされるまでお待ちください。",
|
||||
"A Trial key provides all the functionality of a Pro Registration key": "トライアル キーは、Pro 登録キーのすべての機能を提供します。",
|
||||
"Extension Installed": "拡張機能がインストールされました",
|
||||
"Recovered": "回復しました",
|
||||
"Replaced": "交換されました",
|
||||
"Installing": "インストール中",
|
||||
"Installed": "インストール済み",
|
||||
"Install": "インストール",
|
||||
"Install Extended": "拡張インストール",
|
||||
"Install Recovered": "インストールが回復しました",
|
||||
"Install Replaced": "インストールと置き換え",
|
||||
"Your free Trial key provides all the functionality of a Pro Registration key": "無料のトライアル キーは、プロ登録キーのすべての機能を提供します"
|
||||
}
|
||||
70
web/nuxt.config.ts
Normal file
70
web/nuxt.config.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { parse } from 'dotenv';
|
||||
const envConfig = parse(readFileSync('.env'));
|
||||
for (const k in envConfig) {
|
||||
process.env[k] = envConfig[k];
|
||||
}
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
ssr: false,
|
||||
devServer: {
|
||||
port: 4321,
|
||||
},
|
||||
devtools: {
|
||||
enabled: true,
|
||||
},
|
||||
modules: [
|
||||
'@vueuse/nuxt',
|
||||
'@pinia/nuxt',
|
||||
'@nuxtjs/tailwindcss',
|
||||
'nuxt-custom-elements',
|
||||
],
|
||||
components: [
|
||||
{ path: '~/components/Brand', prefix: 'Brand' },
|
||||
{ path: '~/components/UserProfile', prefix: 'Upc' },
|
||||
'~/components',
|
||||
],
|
||||
runtimeConfig: {
|
||||
public: { // will be exposed to the client-side
|
||||
callbackKey: 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB', // set in .env – https://nuxt.com/docs/guide/going-further/runtime-config#environment-variables
|
||||
}
|
||||
},
|
||||
customElements: {
|
||||
entries: [
|
||||
{
|
||||
name: 'UnraidComponents',
|
||||
tags: [
|
||||
{
|
||||
name: 'UnraidI18nHost',
|
||||
path: '@/components/I18nHost.ce',
|
||||
},
|
||||
{
|
||||
name: 'UnraidAuth',
|
||||
path: '@/components/Auth.ce',
|
||||
},
|
||||
{
|
||||
name: 'UnraidDownloadApiLogs',
|
||||
path: '@/components/DownloadApiLogs.ce',
|
||||
},
|
||||
{
|
||||
name: 'UnraidKeyActions',
|
||||
path: '@/components/KeyActions.ce',
|
||||
},
|
||||
{
|
||||
name: 'UnraidModals',
|
||||
path: '@/components/Modals.ce',
|
||||
},
|
||||
{
|
||||
name: 'UnraidUserProfile',
|
||||
path: '@/components/UserProfile.ce',
|
||||
},
|
||||
{
|
||||
name: 'UnraidWanIpCheck',
|
||||
path: '@/components/WanIpCheck.ce',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
21493
web/package-lock.json
generated
Normal file
21493
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
web/package.json
Normal file
56
web/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "connect-components",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"build:dev": "nuxt build && npm run deploy:dev",
|
||||
"deploy:dev": "./scripts/deploy-dev.sh",
|
||||
"dev": "nuxt dev",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"serve": "serve dist/nuxt-custom-elements/unraid-components",
|
||||
"codegen": "graphql-codegen --config codegen.ts -r dotenv/config",
|
||||
"codegen:watch": "graphql-codegen --config codegen.ts --watch -r dotenv/config"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^4.0.1",
|
||||
"@graphql-codegen/client-preset": "^4.0.1",
|
||||
"@graphql-codegen/introspection": "^4.0.0",
|
||||
"@nuxt/devtools": "^0.6.1",
|
||||
"@nuxt/eslint-config": "^0.1.1",
|
||||
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
||||
"@nuxtjs/tailwindcss": "^6.7.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/node": "^18",
|
||||
"@vue/apollo-util": "^4.0.0-beta.6",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/nuxt": "^10.1.2",
|
||||
"amazon-cognito-identity-js": "^6.2.0",
|
||||
"eslint": "^8.45.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nuxt": "^3.5.1",
|
||||
"nuxt-custom-elements": "^2.0.0-beta.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@headlessui/vue": "^1.7.14",
|
||||
"@heroicons/vue": "^2.0.18",
|
||||
"@pinia/nuxt": "^0.4.11",
|
||||
"@vue/apollo-composable": "^4.0.0-beta.8",
|
||||
"@vueuse/components": "^10.1.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"focus-trap": "^7.4.3",
|
||||
"graphql": "^16.7.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.14.0",
|
||||
"hex-to-rgba": "^2.0.1",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"wretch": "^2.6.0"
|
||||
},
|
||||
"overrides": {
|
||||
"vue": "latest"
|
||||
}
|
||||
}
|
||||
51
web/pages/index.vue
Normal file
51
web/pages/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts" setup>
|
||||
import { serverState } from '~/_data/serverState';
|
||||
|
||||
const nuxtApp = useNuxtApp();
|
||||
onBeforeMount(() => {
|
||||
nuxtApp.$customElements.registerEntry('UnraidComponents');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-black bg-white dark:text-white dark:bg-black">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<client-only>
|
||||
<div class="flex flex-col gap-6 p-6">
|
||||
<h2 class="text-xl font-semibold font-mono">
|
||||
Vue Components
|
||||
</h2>
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
UserProfileCe
|
||||
</h3>
|
||||
<UserProfileCe :server="serverState" />
|
||||
<hr class="border-black">
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
DownloadApiLogsCe
|
||||
</h3>
|
||||
<DownloadApiLogsCe />
|
||||
<hr class="border-black">
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
AuthCe
|
||||
</h3>
|
||||
<AuthCe />
|
||||
<hr class="border-black">
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
KeyActionsCe
|
||||
</h3>
|
||||
<KeyActionsCe />
|
||||
<hr class="border-black">
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
WanIpCheckCe
|
||||
</h3>
|
||||
<WanIpCheckCe php-wan-ip="47.184.85.45" />
|
||||
<hr class="border-black">
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
ModalsCe
|
||||
</h3>
|
||||
<ModalsCe />
|
||||
</div>
|
||||
</client-only>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
47
web/pages/webComponents.vue
Normal file
47
web/pages/webComponents.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { serverState } from '~/_data/serverState';
|
||||
|
||||
const nuxtApp = useNuxtApp();
|
||||
onBeforeMount(() => {
|
||||
nuxtApp.$customElements.registerEntry('UnraidComponents');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<client-only>
|
||||
<unraid-i18n-host class="flex flex-col gap-6 p-6 max-w-5xl mx-auto text-black bg-white dark:text-white dark:bg-black">
|
||||
<h2 class="text-xl font-semibold font-mono">
|
||||
Web Components
|
||||
</h2>
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
UserProfileCe
|
||||
</h3>
|
||||
<unraid-user-profile :server="JSON.stringify(serverState)" />
|
||||
<hr class="border-black">
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
DownloadApiLogsCe
|
||||
</h3>
|
||||
<unraid-download-api-logs />
|
||||
<hr class="border-black">
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
AuthCe
|
||||
</h3>
|
||||
<unraid-auth />
|
||||
<hr class="border-black">
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
KeyActionsCe
|
||||
</h3>
|
||||
<unraid-key-actions />
|
||||
<hr class="border-black">
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
WanIpCheckCe
|
||||
</h3>
|
||||
<unraid-wan-ip-check php-wan-ip="47.184.85.45" />
|
||||
<hr class="border-black">
|
||||
<h3 class="text-lg font-semibold font-mono">
|
||||
ModalsCe
|
||||
</h3>
|
||||
<unraid-modals />
|
||||
</unraid-i18n-host>
|
||||
</client-only>
|
||||
</template>
|
||||
19
web/plugins/i18n.ts
Normal file
19
web/plugins/i18n.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
export default defineNuxtPlugin(({ vueApp }) => {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
locale: 'ja',
|
||||
messages: {
|
||||
en: {
|
||||
hello: 'Hello!'
|
||||
},
|
||||
ja: {
|
||||
hello: 'こんにちは!'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
vueApp.use(i18n);
|
||||
});
|
||||
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
48
web/scripts/deploy-dev.sh
Executable file
48
web/scripts/deploy-dev.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Path to store the last used server name
|
||||
state_file="$HOME/.deploy_state"
|
||||
|
||||
# Read the last used server name from the state file
|
||||
if [[ -f "$state_file" ]]; then
|
||||
last_server_name=$(cat "$state_file")
|
||||
else
|
||||
last_server_name=""
|
||||
fi
|
||||
|
||||
# Read the server name from the command-line argument or use the last used server name as the default
|
||||
server_name="${1:-$last_server_name}"
|
||||
|
||||
# Check if the server name is provided
|
||||
if [[ -z "$server_name" ]]; then
|
||||
echo "Please provide the SSH server name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save the current server name to the state file
|
||||
echo "$server_name" > "$state_file"
|
||||
|
||||
# Replace the value inside the rsync command with the user's input
|
||||
rsync_command="rsync -avz -e ssh .nuxt/nuxt-custom-elements/dist/unraid-components root@${server_name}.local:/usr/local/emhttp/plugins/dynamix.my.servers"
|
||||
|
||||
echo "Executing the following command:"
|
||||
echo "$rsync_command"
|
||||
|
||||
# Execute the rsync command and capture the exit code
|
||||
eval "$rsync_command"
|
||||
exit_code=$?
|
||||
|
||||
# Play built-in sound based on the operating system
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
afplay /System/Library/Sounds/Glass.aiff
|
||||
elif [[ "$OSTYPE" == "linux-gnu" ]]; then
|
||||
# Linux
|
||||
paplay /usr/share/sounds/freedesktop/stereo/complete.oga
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
||||
# Windows
|
||||
powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()"
|
||||
fi
|
||||
|
||||
# Exit with the rsync command's exit code
|
||||
exit $exit_code
|
||||
3
web/server/tsconfig.json
Normal file
3
web/server/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
||||
177
web/store/account.ts
Normal file
177
web/store/account.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { useCallbackStore } from '~/store/callbackActions';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { WebguiUpdate } from '~/composables/services/webgui';
|
||||
import { ACCOUNT_CALLBACK } from '~/helpers/urls';
|
||||
import type { ExternalSignIn, ExternalSignOut } from '~/store/callback';
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export const useAccountStore = defineStore('account', () => {
|
||||
const callbackStore = useCallbackStore();
|
||||
const errorsStore = useErrorsStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
// State
|
||||
const accountAction = ref<ExternalSignIn|ExternalSignOut>();
|
||||
const accountActionHide = ref<boolean>(false);
|
||||
const accountActionStatus = ref<'failed' | 'ready' | 'success' | 'updating'>('ready');
|
||||
|
||||
const username = ref<string>('');
|
||||
|
||||
// Getters
|
||||
const accountActionType = computed(() => accountAction.value?.type);
|
||||
|
||||
// Actions
|
||||
const recover = () => {
|
||||
console.debug('[accountStore.recover]');
|
||||
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'recover',
|
||||
}]);
|
||||
};
|
||||
const replace = () => {
|
||||
console.debug('[accountStore.replace]');
|
||||
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'replace',
|
||||
}]);
|
||||
};
|
||||
const signIn = () => {
|
||||
console.debug('[accountStore.signIn]');
|
||||
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'signIn',
|
||||
}]);
|
||||
};
|
||||
const signOut = () => {
|
||||
console.debug('[accountStore.accountStore.signOut]');
|
||||
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'signOut',
|
||||
}]);
|
||||
};
|
||||
const trialExtend = () => {
|
||||
console.debug('[accountStore.accountStore.trialExtend]');
|
||||
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'trialExtend',
|
||||
}]);
|
||||
};
|
||||
const trialStart = () => {
|
||||
console.debug('[accountStore.accountStore.trialStart]');
|
||||
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'trialStart',
|
||||
}]);
|
||||
};
|
||||
/**
|
||||
* @description Update myservers.cfg for both Sign In & Sign Out
|
||||
* @note unraid-api requires apikey & token realted keys to be lowercase
|
||||
*/
|
||||
const updatePluginConfig = async (action: ExternalSignIn | ExternalSignOut) => {
|
||||
console.debug('[accountStore.updatePluginConfig]', action);
|
||||
// save any existing username before updating
|
||||
if (serverStore.username) { username.value = serverStore.username; }
|
||||
|
||||
accountAction.value = action;
|
||||
accountActionStatus.value = 'updating';
|
||||
|
||||
if (!serverStore.registered && !accountAction.value.user) {
|
||||
console.debug('[accountStore.updatePluginConfig] Not registered skipping sign out');
|
||||
accountActionHide.value = true;
|
||||
accountActionStatus.value = 'success';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userPayload = {
|
||||
...(accountAction.value.user
|
||||
? {
|
||||
apikey: accountAction.value.apiKey,
|
||||
// avatar: '',
|
||||
email: accountAction.value.user?.email,
|
||||
regWizTime: `${Date.now()}_${serverStore.guid}`, // set when signing in the first time and never unset for the sake of displaying Sign In/Up in the UPC without needing to validate guid every time
|
||||
username: accountAction.value.user?.preferred_username,
|
||||
}
|
||||
: {
|
||||
accesstoken: '',
|
||||
apikey: '',
|
||||
avatar: '',
|
||||
email: '',
|
||||
idtoken: '',
|
||||
refreshtoken: '',
|
||||
username: '',
|
||||
}),
|
||||
};
|
||||
const response = await WebguiUpdate
|
||||
.formUrl({
|
||||
csrf_token: serverStore.csrf,
|
||||
'#file': 'dynamix.my.servers/myservers.cfg',
|
||||
'#section': 'remote',
|
||||
...userPayload,
|
||||
})
|
||||
.post()
|
||||
.res((res) => {
|
||||
console.debug('[accountStore.updatePluginConfig] WebguiUpdate res', res);
|
||||
accountActionStatus.value = 'success';
|
||||
})
|
||||
.catch((err) => {
|
||||
console.debug('[accountStore.updatePluginConfig] WebguiUpdate err', err);
|
||||
accountActionStatus.value = 'failed';
|
||||
errorsStore.setError({
|
||||
heading: 'Failed to update Connect account configuration',
|
||||
message: err.message,
|
||||
level: 'error',
|
||||
ref: 'updatePluginConfig',
|
||||
type: 'account',
|
||||
});
|
||||
});
|
||||
return response;
|
||||
} catch (err) {
|
||||
console.debug('[accountStore.updatePluginConfig] WebguiUpdate catch err', err);
|
||||
accountActionStatus.value = 'failed';
|
||||
errorsStore.setError({
|
||||
heading: 'Failed to update Connect account configuration',
|
||||
message: err.message,
|
||||
level: 'error',
|
||||
ref: 'updatePluginConfig',
|
||||
type: 'account',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
accountAction,
|
||||
accountActionHide,
|
||||
accountActionStatus,
|
||||
// Getters
|
||||
accountActionType,
|
||||
// Actions
|
||||
recover,
|
||||
replace,
|
||||
signIn,
|
||||
signOut,
|
||||
trialExtend,
|
||||
trialStart,
|
||||
updatePluginConfig,
|
||||
};
|
||||
});
|
||||
164
web/store/callback.ts
Normal file
164
web/store/callback.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* This file is used to handle callbacks from the server.
|
||||
* It is used in the following apps:
|
||||
* - auth
|
||||
* - craft-unraid
|
||||
* - connect @todo
|
||||
* - connect-components
|
||||
*/
|
||||
import AES from 'crypto-js/aes';
|
||||
import Utf8 from 'crypto-js/enc-utf8';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export type SignIn = 'signIn';
|
||||
export type SignOut = 'signOut';
|
||||
export type OemSignOut = 'oemSignOut';
|
||||
export type Troubleshoot = 'troubleshoot';
|
||||
export type Recover = 'recover';
|
||||
export type Replace = 'replace';
|
||||
export type TrialExtend = 'trialExtend';
|
||||
export type TrialStart = 'trialStart';
|
||||
export type Purchase = 'purchase';
|
||||
export type Redeem = 'redeem';
|
||||
export type Upgrade = 'upgrade';
|
||||
|
||||
export type AccountAction = SignIn | SignOut | OemSignOut | Troubleshoot;
|
||||
export type AccountKeyAction = Recover | Replace | TrialExtend | TrialStart;
|
||||
export type PurchaseAction = Purchase | Redeem | Upgrade;
|
||||
|
||||
export type ServerAction = AccountAction | AccountKeyAction | PurchaseAction;
|
||||
|
||||
export interface UserInfo {
|
||||
'custom:ips_id'?: string;
|
||||
email?: string;
|
||||
email_verifed?: 'true' | 'false';
|
||||
preferred_username?: string;
|
||||
sub?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface ServerData {
|
||||
description?: string;
|
||||
deviceCount?: number;
|
||||
expireTime?: number;
|
||||
flashProduct?: string;
|
||||
flashVendor?: string;
|
||||
guid?: string;
|
||||
keyfile?: string;
|
||||
locale?: string;
|
||||
name?: string;
|
||||
registered: boolean;
|
||||
regGen?: number;
|
||||
regGuid?: string;
|
||||
state: string;
|
||||
wanFQDN?: string;
|
||||
}
|
||||
|
||||
export interface ServerPayload {
|
||||
server: ServerData;
|
||||
type: ServerAction;
|
||||
}
|
||||
|
||||
export interface ExternalSignIn {
|
||||
type: SignIn;
|
||||
apiKey: string;
|
||||
user: UserInfo;
|
||||
}
|
||||
export interface ExternalSignOut {
|
||||
type: SignOut | OemSignOut;
|
||||
}
|
||||
export interface ExternalKeyActions {
|
||||
type: PurchaseAction | AccountKeyAction;
|
||||
keyUrl: string;
|
||||
}
|
||||
|
||||
export type ExternalActions =
|
||||
| ExternalSignIn
|
||||
| ExternalSignOut
|
||||
| ExternalKeyActions;
|
||||
|
||||
export type UpcActions = ServerPayload;
|
||||
|
||||
export interface ExternalPayload {
|
||||
actions: ExternalActions[];
|
||||
sender: string;
|
||||
type: 'forUpc';
|
||||
}
|
||||
export interface UpcPayload {
|
||||
actions: UpcActions[];
|
||||
sender: string;
|
||||
type: 'fromUpc';
|
||||
}
|
||||
|
||||
export type SendPayloads = ExternalActions[] | UpcActions[];
|
||||
|
||||
export type QueryPayloads = ExternalPayload | UpcPayload;
|
||||
|
||||
interface CallbackActionsStore {
|
||||
redirectToCallbackType: (decryptedData: QueryPayloads) => void;
|
||||
encryptionKey: string;
|
||||
sendType: 'fromUpc' | 'forUpc';
|
||||
}
|
||||
|
||||
export const useCallbackStoreGeneric = (
|
||||
useCallbackActions: () => CallbackActionsStore,
|
||||
) =>
|
||||
defineStore('callback', () => {
|
||||
const callbackActions = useCallbackActions();
|
||||
const encryptionKey = import.meta.env.VITE_CALLBACK_KEY;
|
||||
const defaultSendType = 'fromUpc';
|
||||
|
||||
const send = (url: string, payload: SendPayloads, sendType?: 'fromUpc' | 'forUpc') => {
|
||||
console.debug('[callback.send]');
|
||||
try {
|
||||
const stringifiedData = JSON.stringify({
|
||||
actions: [
|
||||
...payload,
|
||||
],
|
||||
sender: window.location.href,
|
||||
type: sendType ?? defaultSendType,
|
||||
});
|
||||
const encryptedMessage = AES.encrypt(stringifiedData, encryptionKey).toString();
|
||||
// build and go to url
|
||||
const destinationUrl = new URL(url);
|
||||
destinationUrl.searchParams.set('data', encodeURI(encryptedMessage));
|
||||
console.debug('[callback.send]', encryptedMessage, destinationUrl);
|
||||
window.location.href = destinationUrl.toString();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error('Unable to create callback event');
|
||||
}
|
||||
};
|
||||
|
||||
const watcher = () => {
|
||||
console.debug('[callback.watcher]');
|
||||
const currentUrl = new URL(window.location.toString());
|
||||
const callbackValue = decodeURI(currentUrl.searchParams.get('data') ?? '');
|
||||
console.debug('[callback.watcher]', { callbackValue });
|
||||
if (!callbackValue) {
|
||||
return console.debug('[callback.watcher] no callback to handle');
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedMessage = AES.decrypt(callbackValue, encryptionKey);
|
||||
const decryptedData: QueryPayloads = JSON.parse(decryptedMessage.toString(Utf8));
|
||||
console.debug('[callback.watcher]', decryptedMessage, decryptedData);
|
||||
// Parse the data and perform actions
|
||||
callbackActions.redirectToCallbackType(decryptedData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error('Couldn\'t decrypt callback data');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
send,
|
||||
watcher,
|
||||
};
|
||||
});
|
||||
81
web/store/callbackActions.ts
Normal file
81
web/store/callbackActions.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { addPreventClose, removePreventClose } from '~/composables/preventClose';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useInstallKeyStore } from '~/store/installKey';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useCallbackStoreGeneric, type ExternalPayload, type ExternalKeyActions, type QueryPayloads } from '~/store/callback';
|
||||
|
||||
export const useCallbackActionsStore = defineStore('callbackActions', () => {
|
||||
const accountStore = useAccountStore();
|
||||
const installKeyStore = useInstallKeyStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
type CallbackStatus = 'closing' | 'error' | 'loading' | 'ready' | 'success';
|
||||
const callbackStatus = ref<CallbackStatus>('ready');
|
||||
|
||||
const callbackData = ref<ExternalPayload>();
|
||||
const callbackError = ref();
|
||||
|
||||
const redirectToCallbackType = (decryptedData: QueryPayloads) => {
|
||||
console.debug('[redirectToCallbackType]', { decryptedData });
|
||||
|
||||
if (!decryptedData.type || decryptedData.type === 'fromUpc' || !decryptedData.actions?.length) {
|
||||
callbackError.value = 'Callback redirect type not present or incorrect';
|
||||
callbackStatus.value = 'ready'; // default status
|
||||
return console.error('[redirectToCallbackType]', callbackError.value);
|
||||
}
|
||||
|
||||
// Display the feedback modal
|
||||
callbackData.value = decryptedData;
|
||||
callbackStatus.value = 'loading';
|
||||
|
||||
// Parse the data and perform actions
|
||||
callbackData.value.actions.forEach(async (action, index, array) => {
|
||||
console.debug('[action]', action);
|
||||
if (action?.keyUrl) {
|
||||
await installKeyStore.install(action as ExternalKeyActions);
|
||||
}
|
||||
if (action?.user || action.type === 'signOut' || action.type === 'oemSignOut') {
|
||||
await accountStore.updatePluginConfig(action);
|
||||
}
|
||||
// all actions have run
|
||||
if (array.length === (index + 1)) {
|
||||
await serverStore.refreshServerState();
|
||||
// callbackStatus.value = 'done';
|
||||
if (array.length > 1) {
|
||||
// if we have more than 1 action it means there was a key install and an account action so both need to be successful
|
||||
const allSuccess = accountStore.accountActionStatus === 'success' && installKeyStore.keyInstallStatus === 'success';
|
||||
callbackStatus.value = allSuccess ? 'success' : 'error';
|
||||
} else {
|
||||
// only 1 action needs to be successful
|
||||
const oneSuccess = accountStore.accountActionStatus === 'success' || installKeyStore.keyInstallStatus === 'success';
|
||||
callbackStatus.value = oneSuccess ? 'success' : 'error';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setCallbackStatus = (status: CallbackStatus) => { callbackStatus.value = status; };
|
||||
|
||||
watch(callbackStatus, (newVal, oldVal) => {
|
||||
console.debug('[callbackStatus]', newVal);
|
||||
if (newVal === 'loading') {
|
||||
addPreventClose();
|
||||
}
|
||||
if (oldVal === 'loading') {
|
||||
removePreventClose();
|
||||
// removing query string once actions are done so users can't refresh the page and go through the same actions
|
||||
window.history.replaceState(null, '', window.location.pathname);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
redirectToCallbackType,
|
||||
callbackData,
|
||||
callbackStatus,
|
||||
setCallbackStatus,
|
||||
};
|
||||
});
|
||||
|
||||
export const useCallbackStore = useCallbackStoreGeneric(useCallbackActionsStore);
|
||||
39
web/store/dropdown.ts
Normal file
39
web/store/dropdown.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { useServerStore } from './server';
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export const useDropdownStore = defineStore('dropdown', () => {
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const dropdownVisible = ref<boolean>(false);
|
||||
|
||||
const dropdownHide = () => { dropdownVisible.value = false; };
|
||||
const dropdownShow = () => { dropdownVisible.value = true; };
|
||||
const dropdownToggle = useToggle(dropdownVisible);
|
||||
|
||||
onMounted(() => {
|
||||
// automatically open the launchpad dropdown on first page load when ENOKEYFILE aka a new server
|
||||
const baseStorageName = `unraidConnect_${serverStore.guid}_`;
|
||||
if (serverStore.state === 'ENOKEYFILE' && !sessionStorage.getItem(`${baseStorageName}ENOKEYFILE`)) {
|
||||
sessionStorage.setItem(`${baseStorageName}ENOKEYFILE`, 'true');
|
||||
dropdownShow();
|
||||
}
|
||||
// automatically open the launchpad dropdown after plugin install on first page load
|
||||
if (serverStore.connectPluginInstalled && !serverStore.registered && sessionStorage.getItem(`${baseStorageName}clickedInstallPlugin`)) {
|
||||
sessionStorage.removeItem(`${baseStorageName}clickedInstallPlugin`);
|
||||
dropdownShow();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
dropdownVisible,
|
||||
dropdownHide,
|
||||
dropdownShow,
|
||||
dropdownToggle,
|
||||
};
|
||||
});
|
||||
133
web/store/errors.ts
Normal file
133
web/store/errors.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import type { ButtonProps } from '~/components/Brand/Button.vue';
|
||||
|
||||
import { OBJ_TO_STR } from '~/helpers/functions';
|
||||
|
||||
import type { Server } from '~/types/server';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export type ErrorType = 'account' | 'callback' | 'installKey' | 'server' | 'serverState' | 'unraidApiState';
|
||||
export interface Error {
|
||||
actions?: ButtonProps[];
|
||||
debugServer?: Server;
|
||||
forumLink?: boolean;
|
||||
heading: string; // if adding new errors be sure to add translations key value pairs
|
||||
level: 'error' | 'info' | 'warning';
|
||||
message: string;
|
||||
ref?: string;
|
||||
supportLink?: boolean;
|
||||
type: ErrorType;
|
||||
}
|
||||
|
||||
export const useErrorsStore = defineStore('errors', () => {
|
||||
const errors = ref<Error[]>([]);
|
||||
|
||||
// const displayedErrors = computed(() => errors.value.filter(error => error.type === 'server' || error.type === 'serverState'));
|
||||
|
||||
const removeErrorByIndex = (index: number) => {
|
||||
errors.value = errors.value.filter((_error, i) => i !== index);
|
||||
};
|
||||
|
||||
const removeErrorByRef = (ref: string) => {
|
||||
errors.value = errors.value.filter(error => error?.ref !== ref);
|
||||
};
|
||||
|
||||
const resetErrors = () => {
|
||||
errors.value = [];
|
||||
};
|
||||
|
||||
const setError = (error: Error) => {
|
||||
console.debug('[setError]', error);
|
||||
errors.value.push(error);
|
||||
};
|
||||
|
||||
interface TroubleshootPayload {
|
||||
email: string;
|
||||
includeUnraidApiLogs: boolean;
|
||||
}
|
||||
|
||||
const openTroubleshoot = async (payload: TroubleshootPayload) => {
|
||||
console.debug('[openTroubleshoot]', payload);
|
||||
try {
|
||||
// @ts-ignore – `FeedbackButton` will be included in 6.10.4+ DefaultPageLayout
|
||||
await FeedbackButton();
|
||||
// once the modal is visible we need to select the radio to correctly show the bug report form
|
||||
let $modal = document.querySelector('.sweet-alert.visible');
|
||||
while (!$modal) {
|
||||
console.debug('[openTroubleshoot] getting $modal…');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
$modal = document.querySelector('.sweet-alert.visible');
|
||||
}
|
||||
console.debug('[openTroubleshoot] $modal', $modal);
|
||||
// autofill errors into the bug report form
|
||||
if (errors.value.length) {
|
||||
let $textarea: HTMLInputElement | null = $modal.querySelector('#troubleshootDetails');
|
||||
while (!$textarea) {
|
||||
console.debug('[openTroubleshoot] getting $textarea…');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
$textarea = $modal.querySelector('#troubleshootDetails');
|
||||
}
|
||||
console.debug('[openTroubleshoot] $textarea', $textarea);
|
||||
const errorMessages = errors.value.map((error, index) => {
|
||||
const index1 = index + 1;
|
||||
let message = `• Error ${index1}: ${error.heading}\n`;
|
||||
message += `• Error ${index1} Message: ${error.message}\n`;
|
||||
message += `• Error ${index1} Level: ${error.level}\n`;
|
||||
message += `• Error ${index1} Type: ${error.type}\n`;
|
||||
if (error.ref) { message += `• Error ${index1} Ref: ${error.ref}\n`; }
|
||||
if (error.debugServer) { message += `• Error ${index1} Debug Server:\n${OBJ_TO_STR(error.debugServer)}\n`; }
|
||||
return message;
|
||||
}).join('\n***************\n');
|
||||
$textarea.value += '\n##########################\n';
|
||||
$textarea.value += `# Debug Details – Component Errors ${errors.value.length} #\n`;
|
||||
$textarea.value += '##########################\n';
|
||||
$textarea.value += errorMessages;
|
||||
}
|
||||
// autofill emails
|
||||
let $emailInput: HTMLInputElement | null = $modal.querySelector('#troubleshootEmail');
|
||||
while (!$emailInput) {
|
||||
console.debug('[openTroubleshoot] getting $emailInput…');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
$emailInput = $modal.querySelector('#troubleshootEmail');
|
||||
}
|
||||
console.debug('[openTroubleshoot] $emailInput', $emailInput);
|
||||
if (payload.email) {
|
||||
$emailInput.value = payload.email;
|
||||
} else {
|
||||
$emailInput.focus();
|
||||
}
|
||||
// select the radio to correctly show the bug report form
|
||||
let $myRadio: HTMLInputElement | null = $modal.querySelector('#optTroubleshoot');
|
||||
while (!$myRadio) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
$myRadio = $modal.querySelector('#optTroubleshoot');
|
||||
}
|
||||
$myRadio.checked = true;
|
||||
// show the correct form in the modal
|
||||
let $panels = $modal.querySelectorAll('.allpanels');
|
||||
while (!$panels) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
$panels = $modal.querySelectorAll('.allpanels');
|
||||
}
|
||||
$panels.forEach(($panel) => {
|
||||
if ($panel.id === 'troubleshoot_panel') { $panel.style.display = 'block'; } else { $panel.style.display = 'none'; }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[openTroubleshoot]', error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
errors,
|
||||
removeErrorByIndex,
|
||||
removeErrorByRef,
|
||||
resetErrors,
|
||||
setError,
|
||||
openTroubleshoot,
|
||||
};
|
||||
});
|
||||
85
web/store/installKey.ts
Normal file
85
web/store/installKey.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { delay } from 'wretch/middlewares';
|
||||
import { WebguiInstallKey, WebguiUpdateDns } from '~/composables/services/webgui';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { ExternalKeyActions } from '~/store/callback';
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export const useInstallKeyStore = defineStore('installKey', () => {
|
||||
const errorsStore = useErrorsStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const keyInstallStatus = ref<'failed' | 'installing' | 'ready' | 'success'>('ready');
|
||||
|
||||
const keyAction = ref<ExternalKeyActions>();
|
||||
const keyActionType = computed(() => keyAction.value?.type);
|
||||
const keyUrl = computed(() => keyAction.value?.keyUrl);
|
||||
/**
|
||||
* Extracts key type from key url. Works for both .key and .unkey.
|
||||
*/
|
||||
const keyType = computed((): string | undefined => {
|
||||
if (!keyUrl.value) { return undefined; }
|
||||
const parts = keyUrl.value.split('/');
|
||||
return parts[parts.length - 1].replace(/\.key|\.unkey/g, '');
|
||||
});
|
||||
|
||||
const install = async (action: ExternalKeyActions) => {
|
||||
console.debug('[install]');
|
||||
keyInstallStatus.value = 'installing';
|
||||
keyAction.value = action;
|
||||
|
||||
if (!keyUrl.value) { return console.error('[install] no key to install'); }
|
||||
|
||||
try {
|
||||
const installResponse = await WebguiInstallKey
|
||||
.query({ url: keyUrl.value })
|
||||
.get();
|
||||
console.log('[install] WebguiInstallKey installResponse', installResponse);
|
||||
|
||||
keyInstallStatus.value = 'success';
|
||||
|
||||
try {
|
||||
const updateDnsResponse = await WebguiUpdateDns
|
||||
.middlewares([
|
||||
delay(1500)
|
||||
])
|
||||
.formUrl({ csrf_token: serverStore.csrf })
|
||||
.post();
|
||||
console.log('[install] WebguiUpdateDns updateDnsResponse', updateDnsResponse);
|
||||
} catch (error) {
|
||||
console.error('[install] WebguiUpdateDns error', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[install] WebguiInstallKey error', error);
|
||||
keyInstallStatus.value = 'failed';
|
||||
errorsStore.setError({
|
||||
heading: 'Failed to install key',
|
||||
message: error.message,
|
||||
level: 'error',
|
||||
|
||||
ref: 'installKey',
|
||||
type: 'installKey',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
watch(keyInstallStatus, (newV, oldV) => {
|
||||
console.debug('[keyInstallStatus]', newV, oldV);
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
keyInstallStatus,
|
||||
// getters
|
||||
keyActionType,
|
||||
keyType,
|
||||
keyUrl,
|
||||
// Actions
|
||||
install,
|
||||
};
|
||||
});
|
||||
23
web/store/modal.ts
Normal file
23
web/store/modal.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export const useModalStore = defineStore('modal', () => {
|
||||
const modalVisible = ref<boolean>(true);
|
||||
|
||||
const modalHide = () => { modalVisible.value = false; };
|
||||
const modalShow = () => { modalVisible.value = true; };
|
||||
const modalToggle = useToggle(modalVisible);
|
||||
|
||||
return {
|
||||
modalVisible,
|
||||
modalHide,
|
||||
modalShow,
|
||||
modalToggle,
|
||||
};
|
||||
});
|
||||
43
web/store/promo.ts
Normal file
43
web/store/promo.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export const usePromoStore = defineStore('promo', () => {
|
||||
const dropdownStore = useDropdownStore();
|
||||
|
||||
const promoVisible = ref<boolean>(false);
|
||||
|
||||
const openOnNextLoad = () => sessionStorage.setItem('unraidConnectPromo', 'show');
|
||||
const promoHide = () => { promoVisible.value = false; };
|
||||
const promoShow = () => { promoVisible.value = true; };
|
||||
const promoToggle = useToggle(promoVisible);
|
||||
|
||||
watch(promoVisible, (newVal, _oldVal) => {
|
||||
if (newVal) { // close the dropdown when the promo is opened
|
||||
dropdownStore.dropdownHide();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (sessionStorage.getItem('unraidConnectPromo') === 'show') {
|
||||
sessionStorage.removeItem('unraidConnectPromo');
|
||||
promoShow();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
promoVisible,
|
||||
|
||||
openOnNextLoad,
|
||||
promoHide,
|
||||
promoShow,
|
||||
promoToggle,
|
||||
};
|
||||
});
|
||||
50
web/store/purchase.ts
Normal file
50
web/store/purchase.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { PURCHASE_CALLBACK } from '~/helpers/urls';
|
||||
import { useCallbackStore } from '~/store/callbackActions';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export const usePurchaseStore = defineStore('purchase', () => {
|
||||
const callbackStore = useCallbackStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const redeem = () => {
|
||||
console.debug('[redeem]');
|
||||
callbackStore.send(PURCHASE_CALLBACK.toString(), [{
|
||||
server: {
|
||||
...serverStore.serverPurchasePayload,
|
||||
},
|
||||
type: 'redeem',
|
||||
}]);
|
||||
};
|
||||
const purchase = () => {
|
||||
console.debug('[purchase]');
|
||||
callbackStore.send(PURCHASE_CALLBACK.toString(), [{
|
||||
server: {
|
||||
...serverStore.serverPurchasePayload,
|
||||
},
|
||||
type: 'purchase',
|
||||
}]);
|
||||
};
|
||||
const upgrade = () => {
|
||||
console.debug('[upgrade]');
|
||||
callbackStore.send(PURCHASE_CALLBACK.toString(), [{
|
||||
server: {
|
||||
...serverStore.serverPurchasePayload,
|
||||
},
|
||||
type: 'upgrade',
|
||||
}]);
|
||||
};
|
||||
|
||||
return {
|
||||
redeem,
|
||||
purchase,
|
||||
upgrade,
|
||||
};
|
||||
});
|
||||
106
web/store/server.fragment.ts
Normal file
106
web/store/server.fragment.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { graphql } from '~/composables/gql/gql';
|
||||
|
||||
// export const SERVER_CLOUD_FRAGMENT = graphql(/* GraphQL */`
|
||||
// fragment FragmentCloud on Cloud {
|
||||
// error
|
||||
// apiKey {
|
||||
// valid
|
||||
// error
|
||||
// }
|
||||
// cloud {
|
||||
// status
|
||||
// error
|
||||
// }
|
||||
// minigraphql {
|
||||
// status
|
||||
// error
|
||||
// }
|
||||
// relay {
|
||||
// status
|
||||
// error
|
||||
// }
|
||||
// }
|
||||
// `);
|
||||
|
||||
// export const SERVER_CONFIG_FRAGMENT = graphql(/* GraphQL */`
|
||||
// fragment FragmentConfig on Config {
|
||||
// error
|
||||
// valid
|
||||
// }
|
||||
// `);
|
||||
|
||||
// export const SERVER_OWNER_FRAGMENT = graphql(/* GraphQL */`
|
||||
// fragment FragmentOwner on Owner {
|
||||
// avatar
|
||||
// username
|
||||
// }
|
||||
// `);
|
||||
|
||||
// export const SERVER_REGISTRATION_FRAGMENT = graphql(/* GraphQL */`
|
||||
// fragment FragmentRegistration on Registration {
|
||||
// state
|
||||
// expiration
|
||||
// keyFile {
|
||||
// contents
|
||||
// }
|
||||
// }
|
||||
// `);
|
||||
|
||||
// export const SERVER_VARS_FRAGMENT = graphql(/* GraphQL */`
|
||||
// fragment FragmentVars on Vars {
|
||||
// regGen
|
||||
// regState
|
||||
// configError
|
||||
// configValid
|
||||
// }
|
||||
// `);
|
||||
|
||||
export const SERVER_STATE_QUERY = graphql(/* GraphQL */`
|
||||
query serverState {
|
||||
cloud {
|
||||
error
|
||||
apiKey {
|
||||
valid
|
||||
error
|
||||
}
|
||||
cloud {
|
||||
status
|
||||
error
|
||||
}
|
||||
minigraphql {
|
||||
status
|
||||
error
|
||||
}
|
||||
relay {
|
||||
status
|
||||
error
|
||||
}
|
||||
}
|
||||
config {
|
||||
error
|
||||
valid
|
||||
}
|
||||
info {
|
||||
os {
|
||||
hostname
|
||||
}
|
||||
}
|
||||
owner {
|
||||
avatar
|
||||
username
|
||||
}
|
||||
registration {
|
||||
state
|
||||
expiration
|
||||
keyFile {
|
||||
contents
|
||||
}
|
||||
}
|
||||
vars {
|
||||
regGen
|
||||
regState
|
||||
configError
|
||||
configValid
|
||||
}
|
||||
}
|
||||
`);
|
||||
912
web/store/server.ts
Normal file
912
web/store/server.ts
Normal file
@@ -0,0 +1,912 @@
|
||||
/**
|
||||
* @todo Check OS and Connect Plugin versions against latest via API every session
|
||||
*/
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import {
|
||||
ArrowRightOnRectangleIcon,
|
||||
CogIcon,
|
||||
GlobeAltIcon,
|
||||
InformationCircleIcon,
|
||||
KeyIcon,
|
||||
QuestionMarkCircleIcon
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { useQuery } from '@vue/apollo-composable';
|
||||
|
||||
import { SERVER_STATE_QUERY } from './server.fragment';
|
||||
import type { serverStateQuery } from '~/composables/gql/graphql';
|
||||
import { WebguiState } from '~/composables/services/webgui';
|
||||
import { SETTINGS_MANAGMENT_ACCESS } from '~/helpers/urls';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useErrorsStore, type Error } from '~/store/errors';
|
||||
import { usePurchaseStore } from '~/store/purchase';
|
||||
import { useThemeStore, type Theme } from '~/store/theme';
|
||||
import { useUnraidApiStore } from '~/store/unraidApi';
|
||||
|
||||
import type {
|
||||
Server,
|
||||
ServerAccountCallbackSendPayload,
|
||||
ServerKeyTypeForPurchase,
|
||||
ServerPurchaseCallbackSendPayload,
|
||||
ServerState,
|
||||
ServerStateCloudStatus,
|
||||
ServerStateConfigStatus,
|
||||
ServerStateData,
|
||||
ServerStateDataAction,
|
||||
ServerconnectPluginInstalled,
|
||||
} from '~/types/server';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export const useServerStore = defineStore('server', () => {
|
||||
const accountStore = useAccountStore();
|
||||
const errorsStore = useErrorsStore();
|
||||
const purchaseStore = usePurchaseStore();
|
||||
const themeStore = useThemeStore();
|
||||
const unraidApiStore = useUnraidApiStore();
|
||||
/**
|
||||
* State
|
||||
*/
|
||||
const apiKey = ref<string>(''); // @todo potentially move to a user store
|
||||
const apiVersion = ref<string>('');
|
||||
const avatar = ref<string>(''); // @todo potentially move to a user store
|
||||
const cloud = ref<ServerStateCloudStatus>();
|
||||
const config = ref<ServerStateConfigStatus>();
|
||||
const connectPluginInstalled = ref<ServerconnectPluginInstalled>('');
|
||||
const connectPluginVersion = ref<string>('');
|
||||
const csrf = ref<string>(''); // required to make requests to Unraid webgui
|
||||
const description = ref<string>('');
|
||||
const deviceCount = ref<number>(0);
|
||||
const email = ref<string>('');
|
||||
const expireTime = ref<number>(0);
|
||||
const flashProduct = ref<string>('');
|
||||
const flashVendor = ref<string>('');
|
||||
const guid = ref<string>('');
|
||||
const guidBlacklisted = ref<boolean>();
|
||||
const guidRegistered = ref<boolean>();
|
||||
const guidReplaceable = ref<boolean | undefined>();
|
||||
const inIframe = ref<boolean>(window.self !== window.top);
|
||||
const keyfile = ref<string>('');
|
||||
const lanIp = ref<string>('');
|
||||
const license = ref<string>('');
|
||||
const locale = ref<string>('');
|
||||
const name = ref<string>('');
|
||||
const osVersion = ref<string>('');
|
||||
const registered = ref<boolean>();
|
||||
const regGen = ref<number>(0);
|
||||
const regGuid = ref<string>('');
|
||||
const site = ref<string>('');
|
||||
const state = ref<ServerState>();
|
||||
const theme = ref<Theme>();
|
||||
watch(theme, (newVal) => {
|
||||
if (newVal) { themeStore.setTheme(newVal); }
|
||||
});
|
||||
const uptime = ref<number>(0);
|
||||
const username = ref<string>(''); // @todo potentially move to a user store
|
||||
const wanFQDN = ref<string>('');
|
||||
|
||||
const apiServerStateRefresh = ref<any>(null);
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
const isRemoteAccess = computed(() => wanFQDN.value || (site.value && site.value.includes('www.') && site.value.includes('unraid.net')));
|
||||
/**
|
||||
* @todo configure
|
||||
*/
|
||||
const pluginOutdated = computed(():boolean => {
|
||||
return false;
|
||||
});
|
||||
|
||||
const server = computed(():Server => {
|
||||
return {
|
||||
apiKey: apiKey.value,
|
||||
apiVersion: apiVersion.value,
|
||||
avatar: avatar.value,
|
||||
connectPluginVersion: connectPluginVersion.value,
|
||||
connectPluginInstalled: connectPluginInstalled.value,
|
||||
description: description.value,
|
||||
deviceCount: deviceCount.value,
|
||||
email: email.value,
|
||||
expireTime: expireTime.value,
|
||||
flashProduct: flashProduct.value,
|
||||
flashVendor: flashVendor.value,
|
||||
guid: guid.value,
|
||||
inIframe: inIframe.value,
|
||||
keyfile: keyfile.value,
|
||||
lanIp: lanIp.value,
|
||||
license: license.value,
|
||||
locale: locale.value,
|
||||
name: name.value,
|
||||
osVersion: osVersion.value,
|
||||
registered: registered.value,
|
||||
regGen: regGen.value,
|
||||
regGuid: regGuid.value,
|
||||
site: site.value,
|
||||
state: state.value,
|
||||
theme: theme.value,
|
||||
uptime: uptime.value,
|
||||
username: username.value,
|
||||
wanFQDN: wanFQDN.value,
|
||||
};
|
||||
});
|
||||
|
||||
const serverPurchasePayload = computed((): ServerPurchaseCallbackSendPayload => {
|
||||
/** @todo refactor out. Just parse state on craft site to determine */
|
||||
let keyTypeForPurchase: ServerKeyTypeForPurchase = 'Trial';
|
||||
switch (state.value) {
|
||||
case 'BASIC':
|
||||
keyTypeForPurchase = 'Basic';
|
||||
break;
|
||||
case 'PLUS':
|
||||
keyTypeForPurchase = 'Plus';
|
||||
break;
|
||||
case 'PRO':
|
||||
keyTypeForPurchase = 'Pro';
|
||||
break;
|
||||
}
|
||||
const server = {
|
||||
apiVersion: apiVersion.value,
|
||||
connectPluginVersion: connectPluginVersion.value,
|
||||
deviceCount: deviceCount.value,
|
||||
email: email.value,
|
||||
guid: guid.value,
|
||||
inIframe: inIframe.value,
|
||||
keyTypeForPurchase,
|
||||
locale: locale.value,
|
||||
osVersion: osVersion.value,
|
||||
registered: registered.value ?? false,
|
||||
state: state.value,
|
||||
site: site.value,
|
||||
};
|
||||
return server;
|
||||
});
|
||||
|
||||
const serverAccountPayload = computed((): ServerAccountCallbackSendPayload => {
|
||||
return {
|
||||
apiVersion: apiVersion.value,
|
||||
connectPluginVersion: connectPluginVersion.value,
|
||||
description: description.value,
|
||||
expireTime: expireTime.value,
|
||||
flashProduct: flashProduct.value,
|
||||
flashVendor: flashVendor.value,
|
||||
guid: guid.value,
|
||||
inIframe: inIframe.value,
|
||||
keyfile: keyfile.value,
|
||||
lanIp: lanIp.value,
|
||||
name: name.value,
|
||||
osVersion: osVersion.value,
|
||||
registered: registered.value ?? false,
|
||||
regGuid: regGuid.value,
|
||||
site: site.value,
|
||||
state: state.value,
|
||||
wanFQDN: wanFQDN.value,
|
||||
};
|
||||
});
|
||||
|
||||
const serverDebugPayload = computed((): Server => {
|
||||
const payload = {
|
||||
apiKey: apiKey.value ? `${apiKey.value.substring(0, 6)}__[REDACTED]` : '', // so we don't send full api key in email
|
||||
apiVersion: apiVersion.value,
|
||||
avatar: avatar.value,
|
||||
connectPluginInstalled: connectPluginInstalled.value,
|
||||
connectPluginVersion: connectPluginVersion.value,
|
||||
description: description.value,
|
||||
deviceCount: deviceCount.value,
|
||||
email: email.value,
|
||||
expireTime: expireTime.value,
|
||||
flashProduct: flashProduct.value,
|
||||
flashVendor: flashVendor.value,
|
||||
guid: guid.value,
|
||||
inIframe: inIframe.value,
|
||||
lanIp: lanIp.value,
|
||||
locale: locale.value,
|
||||
name: name.value,
|
||||
osVersion: osVersion.value,
|
||||
registered: registered.value,
|
||||
regGen: regGen.value,
|
||||
regGuid: regGuid.value,
|
||||
site: site.value,
|
||||
state: state.value,
|
||||
uptime: uptime.value,
|
||||
username: username.value,
|
||||
wanFQDN: wanFQDN.value,
|
||||
};
|
||||
// remove any empty values from object
|
||||
return Object.fromEntries(Object.entries(payload).filter(([_, v]) => v !== null && v !== undefined && v !== ''));
|
||||
});
|
||||
|
||||
const purchaseAction: ServerStateDataAction = {
|
||||
click: () => { purchaseStore.purchase(); },
|
||||
external: true,
|
||||
icon: KeyIcon,
|
||||
name: 'purchase',
|
||||
text: 'Purchase Key',
|
||||
};
|
||||
const upgradeAction: ServerStateDataAction = {
|
||||
click: () => { purchaseStore.upgrade(); },
|
||||
external: true,
|
||||
icon: KeyIcon,
|
||||
name: 'upgrade',
|
||||
text: 'Upgrade Key',
|
||||
};
|
||||
const recoverAction: ServerStateDataAction = {
|
||||
click: () => { accountStore.recover(); },
|
||||
external: true,
|
||||
icon: KeyIcon,
|
||||
name: 'recover',
|
||||
text: 'Recover Key',
|
||||
};
|
||||
const redeemAction: ServerStateDataAction = {
|
||||
click: () => { purchaseStore.redeem(); },
|
||||
external: true,
|
||||
icon: KeyIcon,
|
||||
name: 'redeem',
|
||||
text: 'Redeem Activation Code',
|
||||
};
|
||||
const replaceAction: ServerStateDataAction = {
|
||||
click: () => { accountStore.replace(); },
|
||||
external: true,
|
||||
icon: KeyIcon,
|
||||
name: 'replace',
|
||||
text: 'Replace Key',
|
||||
};
|
||||
const signInAction: ServerStateDataAction = {
|
||||
click: () => { accountStore.signIn(); },
|
||||
external: true,
|
||||
icon: GlobeAltIcon,
|
||||
name: 'signIn',
|
||||
text: 'Sign In with Unraid.net Account',
|
||||
};
|
||||
/**
|
||||
* This action is a computed property because it depends on the state of the keyfile
|
||||
*/
|
||||
const signOutAction = computed((): ServerStateDataAction => {
|
||||
return {
|
||||
click: () => { accountStore.signOut(); },
|
||||
disabled: !keyfile.value,
|
||||
external: true,
|
||||
icon: ArrowRightOnRectangleIcon,
|
||||
name: 'signOut',
|
||||
text: 'Sign Out of Unraid.net',
|
||||
title: !keyfile.value ? 'Sign Out requires a keyfile' : '',
|
||||
};
|
||||
});
|
||||
const trialExtendAction: ServerStateDataAction = {
|
||||
click: () => { accountStore.trialExtend(); },
|
||||
external: true,
|
||||
icon: KeyIcon,
|
||||
name: 'trialExtend',
|
||||
text: 'Extend Trial',
|
||||
};
|
||||
const trialStartAction: ServerStateDataAction = {
|
||||
click: () => { accountStore.trialStart(); },
|
||||
external: true,
|
||||
icon: KeyIcon,
|
||||
name: 'trialStart',
|
||||
text: 'Start Free 30 Day Trial',
|
||||
};
|
||||
|
||||
let messageEGUID = '';
|
||||
const stateData = computed(():ServerStateData => {
|
||||
switch (state.value) {
|
||||
case 'ENOKEYFILE':
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...([purchaseAction, redeemAction, trialStartAction]),
|
||||
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
|
||||
],
|
||||
humanReadable: 'No Keyfile',
|
||||
heading: 'Let\'s Unleash your Hardware!',
|
||||
message: '<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class=\'list-disc pl-16px\'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>',
|
||||
};
|
||||
case 'TRIAL':
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...([purchaseAction, redeemAction]),
|
||||
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
|
||||
],
|
||||
humanReadable: 'Trial',
|
||||
heading: 'Thank you for choosing Unraid OS!',
|
||||
message: '<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>',
|
||||
};
|
||||
case 'EEXPIRED':
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...([purchaseAction, redeemAction]),
|
||||
...(trialExtensionEligible.value ? [trialExtendAction] : []),
|
||||
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
|
||||
],
|
||||
error: true,
|
||||
humanReadable: 'Trial Expired',
|
||||
heading: 'Your Trial has expired',
|
||||
message: trialExtensionEligible.value
|
||||
? '<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>'
|
||||
: '<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>',
|
||||
};
|
||||
case 'BASIC':
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...([upgradeAction]),
|
||||
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
|
||||
],
|
||||
humanReadable: 'Basic',
|
||||
heading: 'Thank you for choosing Unraid OS!',
|
||||
message: registered.value
|
||||
? '<p>Register for Connect by signing in to your Unraid.net account</p>'
|
||||
: guidRegistered.value
|
||||
? '<p>To support more storage devices as your server grows, click Upgrade Key.</p>'
|
||||
: '',
|
||||
};
|
||||
case 'PLUS':
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...([upgradeAction]),
|
||||
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
|
||||
],
|
||||
humanReadable: 'Plus',
|
||||
heading: 'Thank you for choosing Unraid OS!',
|
||||
message: registered.value
|
||||
? '<p>Register for Connect by signing in to your Unraid.net account</p>'
|
||||
: guidRegistered.value
|
||||
? '<p>To support more storage devices as your server grows, click Upgrade Key.</p>'
|
||||
: '',
|
||||
};
|
||||
case 'PRO':
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
|
||||
],
|
||||
humanReadable: 'Pro',
|
||||
heading: 'Thank you for choosing Unraid OS!',
|
||||
message: registered.value
|
||||
? '<p>Register for Connect by signing in to your Unraid.net account</p>'
|
||||
: '',
|
||||
};
|
||||
case 'EGUID':
|
||||
if (guidReplaceable.value) {
|
||||
messageEGUID = '<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>';
|
||||
} else if (guidReplaceable.value === false && guidBlacklisted.value) {
|
||||
messageEGUID = '<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>';
|
||||
} else if (guidReplaceable.value === false && !guidBlacklisted.value) {
|
||||
messageEGUID = '<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>';
|
||||
} else { // basically guidReplaceable.value === null
|
||||
messageEGUID = '<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>';
|
||||
}
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...([replaceAction, purchaseAction, redeemAction]),
|
||||
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
|
||||
],
|
||||
error: true,
|
||||
humanReadable: 'Flash GUID Error',
|
||||
heading: 'Registration key / USB Flash GUID mismatch',
|
||||
message: messageEGUID,
|
||||
};
|
||||
case 'EGUID1':
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...([purchaseAction, redeemAction]),
|
||||
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
|
||||
],
|
||||
error: true,
|
||||
humanReadable: 'Multiple License Keys Present',
|
||||
heading: 'Multiple License Keys Present',
|
||||
message: '<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>',
|
||||
// signInToFix: true, // @todo is this needed?
|
||||
};
|
||||
case 'ENOKEYFILE2':
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...([recoverAction, purchaseAction, redeemAction]),
|
||||
...(registered.value ? [signOutAction.value] : []),
|
||||
],
|
||||
error: true,
|
||||
humanReadable: 'Missing key file',
|
||||
heading: 'Missing key file',
|
||||
message: connectPluginInstalled.value
|
||||
? '<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>'
|
||||
: '<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>',
|
||||
};
|
||||
case 'ETRIAL':
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...([purchaseAction, redeemAction]),
|
||||
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
|
||||
],
|
||||
error: true,
|
||||
humanReadable: 'Invalid installation',
|
||||
heading: 'Invalid installation',
|
||||
message: '<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>',
|
||||
};
|
||||
case 'ENOKEYFILE1':
|
||||
return {
|
||||
actions: [
|
||||
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
|
||||
...([purchaseAction, redeemAction]),
|
||||
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
|
||||
],
|
||||
error: true,
|
||||
humanReadable: 'No Keyfile',
|
||||
heading: 'No USB flash configuration data',
|
||||
message: '<p>There is a problem with your USB Flash device</p>',
|
||||
};
|
||||
case 'ENOFLASH':
|
||||
case 'ENOFLASH1':
|
||||
case 'ENOFLASH2':
|
||||
case 'ENOFLASH3':
|
||||
case 'ENOFLASH4':
|
||||
case 'ENOFLASH5':
|
||||
case 'ENOFLASH6':
|
||||
case 'ENOFLASH7':
|
||||
return {
|
||||
error: true,
|
||||
humanReadable: 'No Flash',
|
||||
heading: 'Cannot access your USB Flash boot device',
|
||||
message: '<p>There is a physical problem accessing your USB Flash boot device</p>',
|
||||
};
|
||||
case 'EBLACKLISTED':
|
||||
return {
|
||||
error: true,
|
||||
humanReadable: 'BLACKLISTED',
|
||||
heading: 'Blacklisted USB Flash GUID',
|
||||
message: '<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique – this is common with USB card readers.</p>',
|
||||
};
|
||||
case 'EBLACKLISTED1':
|
||||
return {
|
||||
error: true,
|
||||
humanReadable: 'BLACKLISTED',
|
||||
heading: 'USB Flash device error',
|
||||
message: '<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>',
|
||||
};
|
||||
case 'EBLACKLISTED2':
|
||||
return {
|
||||
error: true,
|
||||
humanReadable: 'BLACKLISTED',
|
||||
heading: 'USB Flash has no serial number',
|
||||
message: '<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique – this is common with USB card readers.</p>',
|
||||
};
|
||||
case 'ENOCONN':
|
||||
return {
|
||||
error: true,
|
||||
humanReadable: 'Trial Requires Internet Connection',
|
||||
heading: 'Cannot validate Unraid Trial key',
|
||||
message: '<p>Your Trial key requires an internet connection.</p><p><a href="/Settings/NetworkSettings" class="underline">Please check Settings > Network</a></p>',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
error: true,
|
||||
humanReadable: 'Stale',
|
||||
heading: 'Stale Server',
|
||||
message: '<p>Please refresh the page to ensure you load your latest configuration</p>',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const stateDataError = computed((): Error | undefined => {
|
||||
if (!stateData.value?.error) { return undefined; }
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
click: () => {
|
||||
errorsStore.openTroubleshoot({
|
||||
email: email.value,
|
||||
includeUnraidApiLogs: !!connectPluginInstalled.value,
|
||||
});
|
||||
},
|
||||
icon: QuestionMarkCircleIcon,
|
||||
text: 'Contact Support',
|
||||
},
|
||||
],
|
||||
debugServer: serverDebugPayload.value,
|
||||
heading: stateData.value?.heading ?? '',
|
||||
level: 'error',
|
||||
message: stateData.value?.message ?? '',
|
||||
ref: `stateDataError__${state.value}`,
|
||||
type: 'serverState',
|
||||
};
|
||||
});
|
||||
watch(stateDataError, (newVal, oldVal) => {
|
||||
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
|
||||
if (newVal) { errorsStore.setError(newVal); }
|
||||
});
|
||||
|
||||
const authActionsNames = ['signIn', 'signOut'];
|
||||
// Extract sign in / out from actions so we can display seperately as needed
|
||||
const authAction = computed((): ServerStateDataAction | undefined => {
|
||||
if (!stateData.value.actions) { return; }
|
||||
return stateData.value.actions.find(action => authActionsNames.includes(action.name));
|
||||
});
|
||||
// Remove sign in / out from actions so we can display them separately
|
||||
const keyActions = computed((): ServerStateDataAction[] | undefined => {
|
||||
if (!stateData.value.actions) { return; }
|
||||
return stateData.value.actions.filter(action => !authActionsNames.includes(action.name));
|
||||
});
|
||||
const trialExtensionEligible = computed(() => !regGen.value || regGen.value < 2);
|
||||
|
||||
const invalidApiKey = computed((): Error | undefined => {
|
||||
// must be registered with plugin installed
|
||||
if (!connectPluginInstalled.value || !registered.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Keeping separate from validApiKeyLength because we may want to add more checks. Cloud also help with debugging user error submissions.
|
||||
if (apiKey.value.length !== 64) {
|
||||
console.debug('[invalidApiKey] invalid length');
|
||||
return {
|
||||
heading: 'Invalid API Key',
|
||||
level: 'error',
|
||||
message: 'Please sign out then sign back in to refresh your API key.',
|
||||
ref: 'invalidApiKeyLength',
|
||||
type: 'server',
|
||||
};
|
||||
}
|
||||
if (!apiKey.value.startsWith('unupc_')) {
|
||||
console.debug('[invalidApiKey] invalid for upc');
|
||||
return {
|
||||
heading: 'Invalid API Key Format',
|
||||
level: 'error',
|
||||
message: 'Please sign out then sign back in to refresh your API key.',
|
||||
ref: 'invalidApiKeyFormat',
|
||||
type: 'server',
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
watch(invalidApiKey, (newVal, oldVal) => {
|
||||
console.debug('[watch:invalidApiKey]', newVal, oldVal);
|
||||
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
|
||||
if (newVal) { errorsStore.setError(newVal); }
|
||||
});
|
||||
|
||||
const tooManyDevices = computed((): Error | undefined => {
|
||||
if (!config.value?.valid && config.value?.error === 'INVALID') {
|
||||
return {
|
||||
heading: 'Too Many Devices',
|
||||
level: 'error',
|
||||
message: 'You have exceeded the number of devices allowed for your license. Please remove a device before adding another.',
|
||||
ref: 'tooManyDevices',
|
||||
type: 'server',
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
watch(tooManyDevices, (newVal, oldVal) => {
|
||||
console.debug('[watch:tooManyDevices]', newVal, oldVal);
|
||||
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
|
||||
if (newVal) { errorsStore.setError(newVal); }
|
||||
});
|
||||
|
||||
const pluginInstallFailed = computed((): Error | undefined => {
|
||||
if (connectPluginInstalled.value && connectPluginInstalled.value.includes('_installFailed')) {
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
external: true,
|
||||
href: 'https://forums.unraid.net/topic/112073-my-servers-releases/#comment-1154449',
|
||||
icon: InformationCircleIcon,
|
||||
text: 'Learn More',
|
||||
},
|
||||
],
|
||||
heading: 'Unraid Connect Install Failed',
|
||||
level: 'error',
|
||||
message: 'Rebooting will likely solve this.',
|
||||
ref: 'pluginInstallFailed',
|
||||
type: 'server',
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
watch(pluginInstallFailed, (newVal, oldVal) => {
|
||||
console.debug('[watch:pluginInstallFailed]', newVal, oldVal);
|
||||
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
|
||||
if (newVal) { errorsStore.setError(newVal); }
|
||||
});
|
||||
|
||||
/**
|
||||
* Deprecation warning for [hash].unraid.net SSL certs. Deprecation started 2023-01-01
|
||||
*/
|
||||
const deprecatedUnraidSSL = ref<Error | undefined>(
|
||||
(window.location.hostname.includes('localhost')
|
||||
? {
|
||||
actions: [
|
||||
{
|
||||
href: SETTINGS_MANAGMENT_ACCESS.toString(),
|
||||
icon: CogIcon,
|
||||
text: 'Go to Management Access Now',
|
||||
},
|
||||
{
|
||||
external: true,
|
||||
href: 'https://unraid.net/blog/ssl-certificate-update',
|
||||
icon: InformationCircleIcon,
|
||||
text: 'Learn More',
|
||||
},
|
||||
],
|
||||
forumLink: true,
|
||||
heading: 'SSL certificates for unraid.net deprecated',
|
||||
level: 'error',
|
||||
message: 'On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.',
|
||||
ref: 'deprecatedUnraidSSL',
|
||||
type: 'server',
|
||||
}
|
||||
: undefined));
|
||||
watch(deprecatedUnraidSSL, (newVal, oldVal) => {
|
||||
console.debug('[watch:deprecatedUnraidSSL]', newVal, oldVal);
|
||||
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
|
||||
if (newVal) { errorsStore.setError(newVal); }
|
||||
});
|
||||
|
||||
const cloudError = computed((): Error | undefined => {
|
||||
if (!cloud.value?.error) { return undefined; }
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
click: () => {
|
||||
errorsStore.openTroubleshoot({
|
||||
email: email.value,
|
||||
includeUnraidApiLogs: !!connectPluginInstalled.value,
|
||||
});
|
||||
},
|
||||
icon: QuestionMarkCircleIcon,
|
||||
text: 'Contact Support',
|
||||
},
|
||||
],
|
||||
debugServer: serverDebugPayload.value,
|
||||
heading: 'Unraid Connect Error',
|
||||
level: 'error',
|
||||
message: cloud.value.error,
|
||||
ref: 'cloudError',
|
||||
type: 'unraidApiState',
|
||||
};
|
||||
});
|
||||
watch(cloudError, (newVal, oldVal) => {
|
||||
console.debug('[watch:cloudError]', newVal, oldVal);
|
||||
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
|
||||
if (newVal) { errorsStore.setError(newVal); }
|
||||
});
|
||||
|
||||
const serverErrors = computed(() => {
|
||||
return [
|
||||
stateDataError.value,
|
||||
tooManyDevices.value,
|
||||
pluginInstallFailed.value,
|
||||
deprecatedUnraidSSL.value,
|
||||
invalidApiKey.value,
|
||||
cloudError.value,
|
||||
].filter(Boolean);
|
||||
});
|
||||
/**
|
||||
* Determines whether or not we start or stop the apollo client for unraid-api
|
||||
*/
|
||||
const registeredWithValidApiKey = computed(() => registered.value && !invalidApiKey.value);
|
||||
watch(registeredWithValidApiKey, (newVal, oldVal) => {
|
||||
console.debug('[watch:registeredWithValidApiKey]', newVal, oldVal);
|
||||
if (oldVal) {
|
||||
console.debug('[watch:registeredWithValidApiKey] no apiKey, stop unraid-api client');
|
||||
return unraidApiStore.closeUnraidApiClient();
|
||||
}
|
||||
if (newVal) {
|
||||
// if this is just after sign in, let's delay the start by a few seconds to give unraid-api time to update
|
||||
if (accountStore.accountActionType === 'signIn') {
|
||||
console.debug('[watch:registeredWithValidApiKey] delay start unraid-api client');
|
||||
return setTimeout(() => {
|
||||
unraidApiStore.createApolloClient();
|
||||
}, 2000);
|
||||
} else {
|
||||
console.debug('[watch:registeredWithValidApiKey] new apiKey, start unraid-api client');
|
||||
return unraidApiStore.createApolloClient();
|
||||
}
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
const setServer = (data: Server) => {
|
||||
console.debug('[setServer] data', data);
|
||||
if (typeof data?.apiKey !== 'undefined') { apiKey.value = data.apiKey; }
|
||||
if (typeof data?.apiVersion !== 'undefined') { apiVersion.value = data.apiVersion; }
|
||||
if (typeof data?.avatar !== 'undefined') { avatar.value = data.avatar; }
|
||||
if (typeof data?.cloud !== 'undefined') { cloud.value = data.cloud; }
|
||||
if (typeof data?.config !== 'undefined') { config.value = data.config; }
|
||||
if (typeof data?.connectPluginInstalled !== 'undefined') { connectPluginInstalled.value = data.connectPluginInstalled; }
|
||||
if (typeof data?.connectPluginVersion !== 'undefined') { connectPluginVersion.value = data.connectPluginVersion; }
|
||||
if (typeof data?.csrf !== 'undefined') { csrf.value = data.csrf; }
|
||||
if (typeof data?.description !== 'undefined') { description.value = data.description; }
|
||||
if (typeof data?.deviceCount !== 'undefined') { deviceCount.value = data.deviceCount; }
|
||||
if (typeof data?.email !== 'undefined') { email.value = data.email; }
|
||||
if (typeof data?.expireTime !== 'undefined') { expireTime.value = data.expireTime; }
|
||||
if (typeof data?.flashProduct !== 'undefined') { flashProduct.value = data.flashProduct; }
|
||||
if (typeof data?.flashVendor !== 'undefined') { flashVendor.value = data.flashVendor; }
|
||||
if (typeof data?.guid !== 'undefined') { guid.value = data.guid; }
|
||||
if (typeof data?.keyfile !== 'undefined') { keyfile.value = data.keyfile; }
|
||||
if (typeof data?.lanIp !== 'undefined') { lanIp.value = data.lanIp; }
|
||||
if (typeof data?.license !== 'undefined') { license.value = data.license; }
|
||||
if (typeof data?.locale !== 'undefined') { locale.value = data.locale; }
|
||||
if (typeof data?.name !== 'undefined') { name.value = data.name; }
|
||||
if (typeof data?.osVersion !== 'undefined') { osVersion.value = data.osVersion; }
|
||||
if (typeof data?.registered !== 'undefined') { registered.value = data.registered; }
|
||||
if (typeof data?.regGen !== 'undefined') { regGen.value = data.regGen; }
|
||||
if (typeof data?.regGuid !== 'undefined') { regGuid.value = data.regGuid; }
|
||||
if (typeof data?.site !== 'undefined') { site.value = data.site; }
|
||||
if (typeof data?.state !== 'undefined') { state.value = data.state; }
|
||||
if (typeof data?.theme !== 'undefined') { theme.value = data.theme; }
|
||||
if (typeof data?.uptime !== 'undefined') { uptime.value = data.uptime; }
|
||||
if (typeof data?.username !== 'undefined') { username.value = data.username; }
|
||||
if (typeof data?.wanFQDN !== 'undefined') { wanFQDN.value = data.wanFQDN; }
|
||||
console.debug('[setServer] server', server.value);
|
||||
};
|
||||
|
||||
const mutateServerStateFromApi = (data: serverStateQuery): Server => {
|
||||
const mutatedData = {
|
||||
// if we get an owners obj back and the username is root we don't want to overwrite the values
|
||||
...(data.owner && data.owner.username !== 'root' && {
|
||||
// avatar: data.owner.avatar,
|
||||
username: data.owner.username,
|
||||
registered: true,
|
||||
}),
|
||||
name: (data.info && data.info.os) ? data.info.os.hostname : null,
|
||||
keyfile: (data.registration && data.registration.keyFile) ? data.registration.keyFile.contents : null,
|
||||
regGen: data.vars ? data.vars.regGen : null,
|
||||
state: data.vars ? data.vars.regState : null,
|
||||
config: data.config
|
||||
? data.config
|
||||
: {
|
||||
error: data.vars ? data.vars.configError : null,
|
||||
valid: data.vars ? data.vars.configValid : true,
|
||||
},
|
||||
expireTime: (data.registration && data.registration.expiration) ? data.registration.expiration : 0,
|
||||
...(data.cloud && { cloud: data.cloud }),
|
||||
};
|
||||
console.debug('[mutateServerStateFromApi] mutatedData', mutatedData);
|
||||
return mutatedData;
|
||||
};
|
||||
|
||||
const fetchServerFromApi = () => {
|
||||
const { result: resultServerState, refetch: refetchServerState } = useQuery(SERVER_STATE_QUERY, null, {
|
||||
// pollInterval: 2500,
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
const serverState = computed(() => resultServerState.value ?? null);
|
||||
apiServerStateRefresh.value = refetchServerState;
|
||||
watch(serverState, (value) => {
|
||||
console.debug('[watch:serverState]', value);
|
||||
if (value) {
|
||||
const mutatedServerStateResult = mutateServerStateFromApi(value);
|
||||
setServer(mutatedServerStateResult);
|
||||
}
|
||||
});
|
||||
return resultServerState;
|
||||
};
|
||||
|
||||
const phpServerStateRefresh = async () => {
|
||||
console.debug('[phpServerStateRefresh] start');
|
||||
try {
|
||||
const stateResponse: Server = await WebguiState
|
||||
.get()
|
||||
.json();
|
||||
console.debug('[phpServerStateRefresh] stateResponse', stateResponse);
|
||||
setServer(stateResponse);
|
||||
return stateResponse;
|
||||
} catch (error) {
|
||||
console.error('[phpServerStateRefresh] error', error);
|
||||
}
|
||||
};
|
||||
|
||||
let refreshCount = 0;
|
||||
const refreshLimit = 20;
|
||||
const refreshTimeout = 250;
|
||||
const refreshServerStateStatus = ref<'done' | 'ready' | 'refreshing' | 'timeout'>('ready');
|
||||
const refreshServerState = async () => {
|
||||
// If we've reached the refresh limit, stop refreshing
|
||||
if (refreshCount >= refreshLimit) {
|
||||
console.debug('[refreshServerState] refresh limit reached, stop refreshing');
|
||||
refreshServerStateStatus.value = 'timeout';
|
||||
return false;
|
||||
}
|
||||
|
||||
refreshCount++;
|
||||
refreshServerStateStatus.value = 'refreshing';
|
||||
|
||||
const oldRegistered = registered.value;
|
||||
const oldState = state.value;
|
||||
const fromApi = !!apiServerStateRefresh.value;
|
||||
console.debug('[refreshServerState] start', {
|
||||
fromApi,
|
||||
refreshCount,
|
||||
});
|
||||
// Fetch the server state from the API or PHP
|
||||
const response = fromApi
|
||||
? await apiServerStateRefresh.value()
|
||||
: await phpServerStateRefresh();
|
||||
if (!response) {
|
||||
console.debug('[refreshServerState] no response, fetch again in 250ms…');
|
||||
return setTimeout(() => {
|
||||
refreshServerState();
|
||||
}, refreshTimeout);
|
||||
}
|
||||
console.debug('[refreshServerState] response', response);
|
||||
// Extract the new values from the response
|
||||
const newRegistered = fromApi && response?.data ? !!response.data.owner.username : response.registered;
|
||||
const newState = fromApi && response?.data ? response.data.vars.regState : response.state;
|
||||
// Compare the new values to the old values
|
||||
const registrationStatusChanged = oldRegistered !== newRegistered;
|
||||
const stateChanged = oldState !== newState;
|
||||
console.debug('[refreshServerState] newState', {
|
||||
oldRegistered,
|
||||
newRegistered,
|
||||
oldState,
|
||||
newState,
|
||||
registrationStatusChanged,
|
||||
stateChanged,
|
||||
});
|
||||
// If the registration status or state changed, stop refreshing
|
||||
if (registrationStatusChanged || stateChanged) {
|
||||
console.debug('[refreshServerState] change detected, stop refreshing', { registrationStatusChanged, stateChanged });
|
||||
refreshServerStateStatus.value = 'done';
|
||||
return true;
|
||||
}
|
||||
// If we haven't reached the refresh limit, try again
|
||||
console.debug('[refreshServerState] no change, fetch again in 250ms…', { registrationStatusChanged, stateChanged });
|
||||
setTimeout(() => {
|
||||
return refreshServerState();
|
||||
}, refreshTimeout);
|
||||
};
|
||||
|
||||
return {
|
||||
// state
|
||||
apiKey,
|
||||
avatar,
|
||||
cloud,
|
||||
config,
|
||||
connectPluginInstalled,
|
||||
csrf,
|
||||
description,
|
||||
deviceCount,
|
||||
expireTime,
|
||||
guid,
|
||||
locale,
|
||||
lanIp,
|
||||
name,
|
||||
registered,
|
||||
regGen,
|
||||
regGuid,
|
||||
site,
|
||||
state,
|
||||
theme,
|
||||
uptime,
|
||||
username,
|
||||
refreshServerStateStatus,
|
||||
// getters
|
||||
authAction,
|
||||
deprecatedUnraidSSL,
|
||||
invalidApiKey,
|
||||
isRemoteAccess,
|
||||
keyActions,
|
||||
pluginInstallFailed,
|
||||
pluginOutdated,
|
||||
registeredWithValidApiKey,
|
||||
server,
|
||||
serverAccountPayload,
|
||||
serverPurchasePayload,
|
||||
stateData,
|
||||
stateDataError,
|
||||
serverErrors,
|
||||
tooManyDevices,
|
||||
// actions
|
||||
setServer,
|
||||
fetchServerFromApi,
|
||||
refreshServerState,
|
||||
};
|
||||
});
|
||||
80
web/store/theme.ts
Normal file
80
web/store/theme.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import hexToRgba from 'hex-to-rgba';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export interface Theme {
|
||||
banner: boolean;
|
||||
bannerGradient: boolean;
|
||||
bgColor: string;
|
||||
descriptionShow: boolean;
|
||||
metaColor: string;
|
||||
name: string;
|
||||
textColor: string;
|
||||
}
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
// State
|
||||
const theme = ref<Theme | undefined>();
|
||||
// Getters
|
||||
const darkMode = computed(() => (theme.value?.name === 'black' || theme.value?.name === 'azure') ?? false);
|
||||
const bannerGradient = computed(() => {
|
||||
if (!theme.value?.banner || !theme.value?.bannerGradient) { return undefined; }
|
||||
const start = theme.value?.bgColor ? 'var(--color-customgradient-start)' : 'rgba(0, 0, 0, 0)';
|
||||
const end = theme.value?.bgColor ? 'var(--color-customgradient-end)' : 'var(--color-beta)';
|
||||
return `background-image: linear-gradient(90deg, ${start} 0, ${end} 30%);`;
|
||||
});
|
||||
// Actions
|
||||
const setTheme = (data: Theme) => {
|
||||
console.debug('[setTheme]');
|
||||
theme.value = data;
|
||||
};
|
||||
const setCssVars = () => {
|
||||
const body = document.body;
|
||||
const defaultColors = {
|
||||
darkTheme: {
|
||||
alpha: '#1c1b1b',
|
||||
beta: '#f2f2f2',
|
||||
gamma: '#999999',
|
||||
},
|
||||
lightTheme: {
|
||||
alpha: '#f2f2f2',
|
||||
beta: '#1c1b1b',
|
||||
gamma: '#999999',
|
||||
},
|
||||
};
|
||||
let { alpha, beta, gamma } = darkMode.value ? defaultColors.darkTheme : defaultColors.lightTheme;
|
||||
// overwrite with hex colors set in webGUI @ /Settings/DisplaySettings
|
||||
if (theme.value?.textColor) { alpha = theme.value?.textColor; }
|
||||
if (theme.value?.bgColor) {
|
||||
beta = theme.value?.bgColor;
|
||||
body.style.setProperty('--color-customgradient-start', hexToRgba(beta, 0));
|
||||
body.style.setProperty('--color-customgradient-end', hexToRgba(beta, 0.7));
|
||||
}
|
||||
if (theme.value?.metaColor) { gamma = theme.value?.metaColor; }
|
||||
body.style.setProperty('--color-alpha', alpha);
|
||||
body.style.setProperty('--color-beta', beta);
|
||||
body.style.setProperty('--color-gamma', gamma);
|
||||
// box shadow
|
||||
body.style.setProperty('--shadow-beta', `0 25px 50px -12px ${hexToRgba(beta, 0.15)}`);
|
||||
body.style.setProperty('--ring-offset-shadow', `0 0 ${beta}`);
|
||||
body.style.setProperty('--ring-shadow', `0 0 ${beta}`);
|
||||
};
|
||||
|
||||
watch(theme, () => {
|
||||
setCssVars();
|
||||
});
|
||||
|
||||
return {
|
||||
// state
|
||||
bannerGradient,
|
||||
darkMode,
|
||||
theme,
|
||||
// actions
|
||||
setTheme,
|
||||
};
|
||||
});
|
||||
90
web/store/trial.ts
Normal file
90
web/store/trial.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { addPreventClose, removePreventClose } from '~/composables/preventClose';
|
||||
import { startTrial, type StartTrialResponse } from '~/composables/services/keyServer';
|
||||
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { ExternalPayload, TrialExtend, TrialStart } from '~/store/callback';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export const useTrialStore = defineStore('trial', () => {
|
||||
const callbackActionsStore = useCallbackActionsStore();
|
||||
const dropdownStore = useDropdownStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
type TrialStatus = 'failed' | 'ready' | TrialExtend | TrialStart | 'success';
|
||||
const trialStatus = ref<TrialStatus>('ready');
|
||||
|
||||
const trialModalLoading = computed(() => trialStatus.value === 'trialExtend' || trialStatus.value === 'trialStart');
|
||||
const trialModalVisible = computed(() => trialStatus.value === 'failed' || trialStatus.value === 'trialExtend' || trialStatus.value === 'trialStart');
|
||||
|
||||
const requestTrial = async (type?: TrialExtend | TrialStart) => {
|
||||
console.debug('[requestTrial]');
|
||||
try {
|
||||
const payload = {
|
||||
guid: serverStore.guid,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
const response: StartTrialResponse = await startTrial(payload).json();
|
||||
console.debug('[requestTrial]', response);
|
||||
if (!response.license) {
|
||||
trialStatus.value = 'failed';
|
||||
return console.error('[requestTrial]', 'No license returned', response);
|
||||
}
|
||||
// manually create a payload to mimic a callback for key installs
|
||||
const trialStartData: ExternalPayload = {
|
||||
actions: [
|
||||
{
|
||||
keyUrl: response.license,
|
||||
type: type ?? 'trialStart',
|
||||
},
|
||||
],
|
||||
sender: window.location.href,
|
||||
type: 'forUpc',
|
||||
};
|
||||
console.debug('[requestTrial]', trialStartData);
|
||||
trialStatus.value = 'success';
|
||||
return callbackActionsStore.redirectToCallbackType(trialStartData);
|
||||
} catch (error) {
|
||||
trialStatus.value = 'failed';
|
||||
console.error('[requestTrial]', error);
|
||||
}
|
||||
};
|
||||
|
||||
const setTrialStatus = (status: TrialStatus) => {
|
||||
trialStatus.value = status;
|
||||
};
|
||||
|
||||
watch(trialStatus, (newVal, oldVal) => {
|
||||
console.debug('[trialStatus]', newVal, oldVal);
|
||||
// opening
|
||||
if (newVal === 'trialExtend' || newVal === 'trialStart') {
|
||||
addPreventClose();
|
||||
dropdownStore.dropdownHide(); // close the dropdown when the trial modal is opened
|
||||
setTimeout(() => {
|
||||
requestTrial(newVal);
|
||||
}, 1500);
|
||||
}
|
||||
// allow closure
|
||||
if (newVal === 'failed' || newVal === 'success') {
|
||||
removePreventClose();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
trialModalLoading,
|
||||
trialModalVisible,
|
||||
trialStatus,
|
||||
// Actions
|
||||
requestTrial,
|
||||
setTrialStatus,
|
||||
};
|
||||
});
|
||||
216
web/store/unraidApi.ts
Normal file
216
web/store/unraidApi.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { from, ApolloClient, createHttpLink, InMemoryCache, split } from '@apollo/client/core/core.cjs';
|
||||
import { onError } from '@apollo/client/link/error';
|
||||
import { RetryLink } from '@apollo/client/link/retry';
|
||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
|
||||
import { getMainDefinition } from '@apollo/client/utilities';
|
||||
import { ArrowPathIcon } from '@heroicons/vue/24/solid';
|
||||
import { provideApolloClient } from '@vue/apollo-composable';
|
||||
// import { logErrorMessages } from '@vue/apollo-util';
|
||||
import { createClient } from 'graphql-ws';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { UserProfileLink } from 'types/userProfile';
|
||||
|
||||
import { WebguiUnraidApiCommand } from '~/composables/services/webgui';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
// import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
const ERROR_CORS_403 = 'The CORS policy for this site does not allow access from the specified Origin';
|
||||
let prioritizeCorsError = false; // Ensures we don't overwrite this specific error message with a non-descriptive network error message
|
||||
|
||||
let baseUrl = window.location.origin;
|
||||
/** @todo use ENV */
|
||||
const localDevUrl = baseUrl.includes(':4321');
|
||||
if (localDevUrl) {
|
||||
/** @temp local dev mode */
|
||||
baseUrl = baseUrl.replace(':4321', ':3001');
|
||||
}
|
||||
const httpEndpoint = new URL('/graphql', baseUrl);
|
||||
const wsEndpoint = new URL('/graphql', baseUrl.replace('http', 'ws'));
|
||||
|
||||
console.debug('[useUnraidApiStore] httpEndpoint', httpEndpoint.toString());
|
||||
console.debug('[useUnraidApiStore] wsEndpoint', wsEndpoint.toString());
|
||||
|
||||
export const useUnraidApiStore = defineStore('unraidApi', () => {
|
||||
console.debug('[useUnraidApiStore]');
|
||||
const accountStore = useAccountStore();
|
||||
// const errorsStore = useErrorsStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const unraidApiClient = ref<ApolloClient<any>>();
|
||||
watch(unraidApiClient, (newVal, oldVal) => {
|
||||
console.debug('[watch:unraidApiStore.unraidApiClient]', { newVal, oldVal });
|
||||
if (newVal) {
|
||||
const apiResponse = serverStore.fetchServerFromApi();
|
||||
if (apiResponse) {
|
||||
// we have a response, so we're online
|
||||
unraidApiStatus.value = 'online';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// const unraidApiErrors = ref<any[]>([]);
|
||||
const unraidApiStatus = ref<'connecting' | 'offline' | 'online' | 'restarting'>('offline');
|
||||
watch(unraidApiStatus, (newVal, oldVal) => {
|
||||
console.debug('[watch:unraidApiStore.unraidApiStatus]', { newVal, oldVal });
|
||||
});
|
||||
|
||||
const unraidApiRestartAction = computed((): UserProfileLink | undefined => {
|
||||
const { connectPluginInstalled, stateDataError } = serverStore;
|
||||
if (unraidApiStatus.value !== 'offline' || !connectPluginInstalled || stateDataError) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
click: () => restartUnraidApiClient(),
|
||||
emphasize: true,
|
||||
icon: ArrowPathIcon,
|
||||
text: 'Restart unraid-api',
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Automatically called when an apiKey is set in the serverStore
|
||||
*/
|
||||
const createApolloClient = () => {
|
||||
console.debug('[useUnraidApiStore.createApolloClient]', serverStore.apiKey);
|
||||
if (accountStore.accountActionType === 'signOut') {
|
||||
return console.debug('[useUnraidApiStore.createApolloClient] sign out imminent, skipping createApolloClient');
|
||||
}
|
||||
|
||||
unraidApiStatus.value = 'connecting';
|
||||
|
||||
const headers = { 'x-api-key': serverStore.apiKey };
|
||||
|
||||
const httpLink = createHttpLink({
|
||||
uri: httpEndpoint.toString(),
|
||||
headers,
|
||||
});
|
||||
|
||||
const wsLink = new GraphQLWsLink(
|
||||
createClient({
|
||||
url: wsEndpoint.toString(),
|
||||
connectionParams: () => ({
|
||||
headers,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @todo integrate errorsStore errorsStore.setError(error);
|
||||
*/
|
||||
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
||||
if (graphQLErrors) {
|
||||
console.debug('[GraphQL error]', graphQLErrors);
|
||||
graphQLErrors.map((error) => {
|
||||
console.error('[GraphQL error]', error, error.error.message);
|
||||
if (error.error.message.includes('offline')) {
|
||||
unraidApiStatus.value = 'offline';
|
||||
}
|
||||
if (error.error.message && error.error.message.includes(ERROR_CORS_403)) {
|
||||
prioritizeCorsError = true;
|
||||
}
|
||||
return error.message;
|
||||
});
|
||||
console.debug('[GraphQL error]', graphQLErrors);
|
||||
}
|
||||
|
||||
if (networkError && !prioritizeCorsError) {
|
||||
console.error(`[Network error]: ${networkError}`);
|
||||
const msg = networkError.message ? networkError.message : networkError;
|
||||
if (typeof msg === 'string' && msg.includes('Unexpected token < in JSON at position 0')) {
|
||||
return 'Unraid API • CORS Error';
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
});
|
||||
|
||||
const retryLink = new RetryLink({
|
||||
attempts: {
|
||||
max: Infinity,
|
||||
retryIf: (error, _operation) => {
|
||||
console.debug('[retryLink.retryIf]', { error, _operation, prioritizeCorsError });
|
||||
return !!error && !prioritizeCorsError; // don't retry when ERROR_CORS_403
|
||||
},
|
||||
},
|
||||
delay: {
|
||||
initial: prioritizeCorsError ? 3000 : 300,
|
||||
max: 10,
|
||||
jitter: true,
|
||||
},
|
||||
});
|
||||
|
||||
const splitLinks = split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return (
|
||||
definition.kind === 'OperationDefinition' &&
|
||||
definition.operation === 'subscription'
|
||||
);
|
||||
},
|
||||
wsLink,
|
||||
httpLink,
|
||||
);
|
||||
/**
|
||||
* @todo as we add retries, determine which we'll need
|
||||
* https://www.apollographql.com/docs/react/api/link/introduction/#additive-composition
|
||||
* https://www.apollographql.com/docs/react/api/link/introduction/#directional-composition
|
||||
*/
|
||||
const additiveLink = from([
|
||||
errorLink,
|
||||
retryLink,
|
||||
splitLinks,
|
||||
]);
|
||||
|
||||
unraidApiClient.value = new ApolloClient({
|
||||
link: additiveLink,
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
|
||||
provideApolloClient(unraidApiClient.value);
|
||||
console.debug('[useUnraidApiStore.createApolloClient] 🏁 CREATED');
|
||||
};
|
||||
/**
|
||||
* Automatically called when an apiKey is unset in the serverStore
|
||||
*/
|
||||
const closeUnraidApiClient = async () => {
|
||||
console.debug('[useUnraidApiStore.closeUnraidApiClient] STARTED');
|
||||
if (!unraidApiClient.value) { return console.debug('[useUnraidApiStore.closeUnraidApiClient] unraidApiClient not set'); }
|
||||
if (unraidApiClient.value) {
|
||||
await unraidApiClient.value.clearStore();
|
||||
unraidApiClient.value.stop();
|
||||
// (wsLink.value as any).subscriptionClient.close(); // needed if we start using subscriptions
|
||||
}
|
||||
unraidApiClient.value = undefined;
|
||||
unraidApiStatus.value = 'offline';
|
||||
console.debug('[useUnraidApiStore.closeUnraidApiClient] DONE');
|
||||
};
|
||||
|
||||
const restartUnraidApiClient = async () => {
|
||||
unraidApiStatus.value = 'restarting';
|
||||
const response = await WebguiUnraidApiCommand({
|
||||
csrf_token: serverStore.csrf,
|
||||
command: 'start',
|
||||
});
|
||||
console.debug('[restartUnraidApiClient]', response);
|
||||
return setTimeout(() => {
|
||||
if (unraidApiClient.value) {
|
||||
createApolloClient();
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
return {
|
||||
unraidApiClient,
|
||||
unraidApiStatus,
|
||||
unraidApiRestartAction,
|
||||
createApolloClient,
|
||||
closeUnraidApiClient,
|
||||
restartUnraidApiClient,
|
||||
};
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user