Compare commits

...

445 Commits

Author SHA1 Message Date
Thomas Camlong
7dc205fa66 🚀 v0.7.1 ! Bug fixes and QOL improvements
### Features
*  Changing deluge/qbittorent to use href instead of origin by @VinnyVynce in https://github.com/ajnart/homarr/pull/178
*  Transmission Integration by @ajnart in https://github.com/ajnart/homarr/pull/181
*  Add different URL for API calls by @ajnart in https://github.com/ajnart/homarr/pull/180
* Password / Login Page by @ajnart in https://github.com/ajnart/homarr/pull/179
*  Ability to toggle categories by @ajnart in https://github.com/ajnart/homarr/pull/177
*  Add settings to change title and icons by @Aimsucks in https://github.com/ajnart/homarr/pull/184
*  Color, shade, app opacity, and background customizations by @Aimsucks in https://github.com/ajnart/homarr/pull/188
*  Calendar indication about date and w-e with secondary color by @Darkham42 in https://github.com/ajnart/homarr/pull/193
*  More Information in Torrents Module by @LarveyOfficial in https://github.com/ajnart/homarr/pull/195
*  Could position widgets at left by @Darkham42 in https://github.com/ajnart/homarr/pull/197
*  Configure calendar widget to show Sunday first by @ajnart in https://github.com/ajnart/homarr/pull/224
* Service Addition Overhaul by @LarveyOfficial in https://github.com/ajnart/homarr/pull/229
*  Slidable service span in customizations  by @Aimsucks in https://github.com/ajnart/homarr/pull/222
### Tweaks
🔧 Make credentials non-required for torrents by @ajnart in https://github.com/ajnart/homarr/pull/223
### Bug fixes
* 🐛Fix completed torrents progress color by @LarveyOfficial in https://github.com/ajnart/homarr/pull/227
* 🐛Tiles could be moved accidentally on mobiles by @ajnart in https://github.com/ajnart/homarr/pull/226
* 🐛 Cannot open "New Tab URL" on mobile by @ajnart in https://github.com/ajnart/homarr/pull/225
* 🐛 Fix Calendar Item Duplication by @LarveyOfficial in https://github.com/ajnart/homarr/pull/249
* 🐛Fix URL for Radarr and other services by @LarveyOfficial in https://github.com/ajnart/homarr/pull/250
* 🐛 Fix Calendar not loading content when a service fails by @LarveyOfficial in https://github.com/ajnart/homarr/pull/230
* 🐛 Calendar current day for light theme by @Darkham42 in https://github.com/ajnart/homarr/pull/194
* 🐛 Fix for timezone issues by @LarveyOfficial in https://github.com/ajnart/homarr/pull/186
* 🐛 Fix Sonarr Incorrect Dates by @LarveyOfficial in https://github.com/ajnart/homarr/pull/189
* 🐛 Fix for origin URL not containing path in API request URL by @Aimsucks in https://github.com/ajnart/homarr/pull/221
2022-06-20 22:09:25 +02:00
ajnart
91a249d953 🔖 tag v0.7.1 2022-06-20 22:03:43 +02:00
Thomas Camlong
356afda9c7 Merge pull request #250 from LarveyOfficial/patch-2
🐛Fix URL for Radarr and other services
2022-06-20 20:14:17 +02:00
Larvey
35f02a2296 Fix URL for Radarr and other services 2022-06-20 14:02:33 -04:00
Thomas Camlong
16bcec0deb Merge pull request #249 from LarveyOfficial/patch-1
🐛 Fix Calendar Item Duplication
2022-06-20 19:45:47 +02:00
Larvey
16ec57081b Fix Calendar Item Duplication
- 6fd23cf6a0 changed how items for the calendar are acquired, making it so every series gets updated when a new one gets added, or one gets moved. This causes the entries in the calendar to duplicate due to old code being left in.
2022-06-20 13:32:31 -04:00
ajnart
690f09fcf3 🚑 Hotfix ServiceItems 2022-06-20 10:40:30 +02:00
Thomas Camlong
2f960169bb 🔀 Merge pull request #222 from Aimsucks/app-card-with-slider
 Slidable service span in customizations
2022-06-20 10:29:39 +02:00
ajnart
14a40d9f66 🔧 Tweak values and UI changes 2022-06-20 10:24:22 +02:00
Thomas Camlong
e5abd67f83 🔀 Merge pull request #221 from Aimsucks/issue-215-fix
🐛 Fix for origin URL not containing path in API request URL
2022-06-20 10:22:13 +02:00
Thomas Camlong
399ba7e2fc 🔀 Merge pull request #229 from LarveyOfficial/rework-AddAppShelfItem
Service Addition Overhaul
2022-06-20 10:20:01 +02:00
ajnart
7780ae3d7a ♻️ Re-implement changes 2022-06-20 10:17:49 +02:00
ajnart
80d3f16473 🔧 Tweak UI and change the name of the settings 2022-06-20 10:06:30 +02:00
Larvey
a8c0dfcd0c Fix Capitalization 2022-06-20 10:06:15 +02:00
Larvey
6ee7d6ec8d declutter config file 2022-06-20 10:06:15 +02:00
Larvey
544fae3808 Added Scrollbar 2022-06-20 10:05:55 +02:00
Larvey
4516dde1f4 Reworked AddAppShelfItem
Adds:
- Advanced Options tab
- Changed which ping status codes identify as "Online"
- Change if service opens in new tab or not

Fixes:
- Deluge and Transmission Password requirement
2022-06-20 10:01:36 +02:00
Thomas Camlong
a20c5f8d12 🔀 Merge pull request #223 from ajnart/transmission-login
🔧 Make credentials non-required for torrents
2022-06-20 09:08:10 +02:00
Thomas Camlong
60e5c0d165 🔀 Merge pull request #230 from LarveyOfficial/calendar-multi-content-fix
🐛 Fix Calendar not loading content when a service fails
2022-06-20 09:03:14 +02:00
Larvey
b7bf18250d Fix CalendarModule.tsx
Fix no content showing when 1 of the same service is down.
2022-06-16 21:02:30 -04:00
Thomas Camlong
93256b7a6a 🔀 Merge pull request #224 from ajnart/ajnart/issue217
 Configure calendar widget to show Sunday first #217
2022-06-15 06:52:50 +02:00
Thomas Camlong
47a4437a01 🔀 Merge pull request #225 from ajnart/ajnart/issue202
🐛 Fix cannot open "New Tab URL" on mobile
2022-06-15 06:52:16 +02:00
Thomas Camlong
92470c619e 🔀 Merge pull request #226 from ajnart/ajnart/issue214
🐛Tiles could be moved accidentally on mobiles
2022-06-15 06:51:18 +02:00
Thomas Camlong
7cb3dfbd16 🔀 Merge pull request #227 from LarveyOfficial/fix-progress-color
🐛Fix completed torrents progress color
2022-06-15 06:51:06 +02:00
ajnart
d69e4f41a1 🐛 Fix required username 2022-06-15 06:47:53 +02:00
Larvey
4980254e89 Update DownloadsModule.tsx 2022-06-14 21:14:26 -04:00
ajnart
5133286e04 🐛Tiles could be moved accidentally on mobiles
Fixes #214
2022-06-14 22:45:31 +02:00
ajnart
ca2713a12c 🐛 Cannot open "New Tab URL" on mobile
Fixes #202
2022-06-14 22:11:55 +02:00
ajnart
4981823c37 🔧 Tweak values on the slider 2022-06-14 22:09:30 +02:00
ajnart
5d31e414f0 Configure calendar widget to show Sunday first
Fixes #217
2022-06-14 20:50:11 +02:00
ajnart
8ec2b9d0cd 🔧 Make credentials non-required for torrents
Fixes #201
2022-06-14 20:33:26 +02:00
Aimsucks
bd920dfc86 App card width slider
Added a slider to determine the XL app card width
2022-06-14 17:45:22 +00:00
Aimsucks
b5540a9958 🐛 Origin URL in calendar to includes path
Changed the origin variable in the calendar module to use the entire URL instead of just the origin domain
2022-06-14 17:01:30 +00:00
Thomas Camlong
778988de58 🚀 v0.7.0 : Theming, Password protection, Autocompletion, Transmission, Mobile responsiveness! This is a big upgrade 👀 2022-06-13 10:15:39 +02:00
ajnart
5b1437552d 🔖 Bump version to v0.7.0 2022-06-12 16:58:48 +02:00
WalkxCode
e8a8fbe6ac 💄 Makes the ModuleEnabler grid look better 2022-06-12 16:35:56 +02:00
WalkxCode
5c0a074219 💬 Updates names and placeholders in AddAppShelfItem 2022-06-12 16:35:31 +02:00
WalkxCode
58ec74bb68 🚸 Improves case matching for auto-fill 2022-06-12 16:34:24 +02:00
Thomas Camlong
6ac82bda40 🔀 Merge pull request #197 from Darkham42/dev
feat: could position widgets at left
2022-06-12 15:46:13 +02:00
Thomas Camlong
2c6e86840a 🔀 Merge pull request #195 from LarveyOfficial/patch-3
More Information in Torrents Module
2022-06-12 15:39:32 +02:00
ajnart
df85fc6b7d 🐛 Fix multiple bugs and reformat code 2022-06-12 15:38:14 +02:00
Darkham42
89804dafd1 feat: could position widgets at left 2022-06-12 08:04:20 +02:00
Larvey
98eaee1234 Use humanFileSize 2022-06-12 01:15:38 -04:00
Larvey
433edafddd Remove humanFileSize 2022-06-12 01:14:38 -04:00
Larvey
e39d5741b6 Create humanFileSize.ts 2022-06-12 01:13:51 -04:00
Larvey
21c08cbe63 Change Elements hidden on Mobile 2022-06-12 00:59:12 -04:00
Larvey
ec92a1d89c Removed new Features on Mobile (Temporary) 2022-06-12 00:52:12 -04:00
ajnart
0f2c5dbce2 🔥 Remove keyboard usage to sort items 2022-06-12 06:36:37 +02:00
Larvey
8eae5a908c Merge branch 'dev' into patch-3 2022-06-12 00:25:36 -04:00
Thomas Camlong
a37f0fdee6 🔀 Merge pull request #194 from Darkham42/dev
fix: calendar current day for light theme
2022-06-12 06:16:59 +02:00
Larvey
08799aac18 Fix build issue from pull request #193 2022-06-11 19:16:03 -04:00
Larvey
06531e0fb8 Removed Logging used during development 2022-06-11 19:02:34 -04:00
Larvey
0f56ead24f Fixed Customization Spelling 2022-06-11 19:00:50 -04:00
Larvey
922caa76da More Info in Torrents Module
Added
- ETA
- Torrent Size
- Paused state color in progress bar
2022-06-11 18:59:45 -04:00
Darkham42
0acb1f6b6d refactor: use theme 2022-06-11 23:44:14 +02:00
Darkham42
8d645ca404 fix: calendar current day for light theme 2022-06-11 23:22:58 +02:00
Thomas Camlong
a5c4f30f57 Merge pull request #193 from Darkham42/dev
feat: calendar indication about date and w-e with secondary color
2022-06-11 20:20:37 +02:00
Darkham42
562a05adf5 feat: calendar indication about date and we with secondary color 2022-06-11 20:11:20 +02:00
Thomas Camlong
de77e06b18 🔀 Merge pull request #188 from Aimsucks/more-customizations
Color, shade, app opacity, and background customizations thank you @Aimsucks !
2022-06-11 19:49:46 +02:00
ajnart
03c499d822 🚚 Make the weather module release (from beta) 2022-06-11 19:45:09 +02:00
ajnart
169d08f3b6 🚚 Move selectors to customisation tab 2022-06-11 19:44:11 +02:00
ajnart
437807a9e0 💄 Change module enabler layout 2022-06-11 19:43:01 +02:00
ajnart
4866fd74b5 🚚 Rename tabs in settings 2022-06-11 19:42:36 +02:00
ajnart
426ba69afd Add more transparency areas and fix bugs 2022-06-11 18:37:13 +02:00
Aimsucks
74f87b570d Update src/components/layout/Background.tsx
Co-authored-by: Bjorn Lammers <walkxnl@gmail.com>
2022-06-08 18:09:59 -04:00
Aimsucks
fed5f6df52 📝 Added a placeholder for background 2022-06-08 20:20:24 +00:00
Aimsucks
5cc160473c Revert "📝 Background image placeholder and instructions"
This reverts commit 4833157061.
2022-06-08 20:18:42 +00:00
Aimsucks
4833157061 📝 Background image placeholder and instructions
Updated readme with instructions to mount the /public folder instead of the /public/icons folder, as well as added a placeholder for background image settings
2022-06-08 18:13:41 +00:00
Aimsucks
a0c8518d22 🎨 Made opacity change app background
Made the opacity slider change the individual app background and border rgba values instead of the entire app.
2022-06-08 18:01:16 +00:00
Aimsucks
c0c816d3db Update src/components/Settings/ShadeSelector.tsx
Co-authored-by: Bjorn Lammers <walkxnl@gmail.com>
2022-06-08 13:13:04 -04:00
Aimsucks
ac47de72ee Merge branch 'dev' into more-customizations 2022-06-08 12:48:02 -04:00
ajnart
d631865f71 💄 Small UI qol update
Module download now has a different look and can be toggled on and off
2022-06-08 18:41:22 +02:00
Aimsucks
4ee6562e35 🐛 Fix for favicon not changing
Removed favicon and title from _app.tsx
2022-06-08 16:26:09 +00:00
ajnart
19f80b9b4c 🚑 Hotfix calendar and mobile responsiveness 2022-06-08 16:16:00 +00:00
ajnart
949deacd6d 🔀 Rebase with dev 2022-06-08 16:16:00 +00:00
Larvey
b0f4a91878 Fix Sonarr Incorrect Dates
Due to how Sonarr gives dates, they recommend using UTC time when displaying it as it matches their calendar. Took some digging but it fixed it.
2022-06-08 16:16:00 +00:00
ajnart
68f2e79056 🧱 Try to fix cookies issues 2022-06-08 16:16:00 +00:00
ajnart
43e68e1bbf 🐛 Trying to fix dates 2022-06-08 16:16:00 +00:00
Larvey
5033323b7c Fix for timezone issues 2022-06-08 16:16:00 +00:00
Aimsucks
7519b4a6b2 Added an app opacity slider
Added a slider to change individual app opacity on the AppShelf
2022-06-08 16:03:06 +00:00
Aimsucks
e6eedefec4 Added a shade selector
Added a popover shade selector similar to the color selector, but shows primary and secondary colors to pick the desired Mantine primaryShade
2022-06-08 15:36:54 +00:00
Aimsucks
845d6a3d87 🎨 Made color switcher change Mantine styles
Moved the color switcher's functions to a context provider and made Mantine's styles derived off of that context.
2022-06-08 14:58:32 +00:00
ajnart
f75da289c2 🚑 Hotfix calendar and mobile responsiveness 2022-06-08 09:56:04 +02:00
Thomas Camlong
063a6447c0 🔀 Merge pull request #189 from LarveyOfficial/patch-2
Fix Sonarr Incorrect Dates
2022-06-08 08:11:13 +02:00
ajnart
4dac730412 🔀 Rebase with dev 2022-06-08 08:09:59 +02:00
Larvey
de6e0f645f Fix Sonarr Incorrect Dates
Due to how Sonarr gives dates, they recommend using UTC time when displaying it as it matches their calendar. Took some digging but it fixed it.
2022-06-07 22:05:28 -04:00
Aimsucks
b26ab50c8d 🎨 Changed primary/secondary color to camelCase 2022-06-07 17:48:04 +00:00
Aimsucks
423f8110b9 Added a background image input
Added an input in the advanced options for a background image. Also removed an unused import from my previous commit and changed the margin on the header bar to padding instead.
2022-06-07 17:36:05 +00:00
ajnart
84ae49ed2a 🧱 Try to fix cookies issues 2022-06-07 19:34:58 +02:00
ajnart
fb291c5411 🐛 Trying to fix dates 2022-06-07 19:34:24 +02:00
Aimsucks
901798055b Added primary/secondary color selection
Added two new inputs to the options menu: primary and secondary color selectors.
2022-06-07 16:53:51 +00:00
Thomas Camlong
d32d599098 🔀 Merge pull request #186 from LarveyOfficial/patch-1
Fix for timezone issues
2022-06-07 17:22:57 +02:00
Larvey
76e02cf148 Fix for timezone issues 2022-06-07 11:19:53 -04:00
Thomas Camlong
f19b4675ad 🔀 Merge pull request #184 from Aimsucks/change-title-icons
Add settings to change title and icons
2022-06-07 16:17:50 +02:00
ajnart
4f1640b70a 🐛 Fix a small bug inside the torrent module 2022-06-07 12:41:49 +02:00
Thomas Camlong
c1d17ec8b2 Update src/components/Settings/AdvancedSettings.tsx
Co-authored-by: Bjorn Lammers <walkxnl@gmail.com>
2022-06-07 12:12:23 +02:00
Thomas Camlong
d2f1268520 Merge branch 'dev' into change-title-icons 2022-06-07 10:37:36 +02:00
ajnart
b72afc2270 📦 💄 Upgrade packages and style 2022-06-07 10:36:47 +02:00
ajnart
de0c625f88 🐛 Fixing Deluge integration
Thanks to @scttcper for fixing https://github.com/scttcper/deluge/issues/106 so quickly !
2022-06-07 09:50:04 +02:00
Thomas Camlong
29c9f3ecac 🔥 Remove the Code quality tickboxes
They were annoying (to me at least)
2022-06-07 08:32:39 +02:00
ajnart
a321095daf 💄 Styling the settings 2022-06-07 08:21:03 +02:00
ajnart
ced18da65a 🔥 Remove default values for the Advanced settings 2022-06-07 08:20:19 +02:00
ajnart
1a642ad7b4 🔧 Make the changed values optional 2022-06-07 07:20:44 +02:00
Aimsucks
838f196937 Ability to change title and icons V2!
Results of criticism in pull request #182
2022-06-07 01:35:50 +00:00
Aimsucks
6af5166aa5 Ability to change title and icons 2022-06-07 00:07:56 +00:00
ajnart
7935fb6616 🚑 Hotfix icon matching 2022-06-07 01:04:34 +02:00
ajnart
ed567065b4 ⚰️ Remove dead code 2022-06-07 00:30:42 +02:00
ajnart
06035fb6f0 💄 Very minor fix the the AppShelf UI 2022-06-07 00:15:19 +02:00
ajnart
c1af0a087d 💄 Styling the AppShelf 2022-06-07 00:07:36 +02:00
ajnart
6067c5dfcf 💄 Styling changes for medias and AppShelf 2022-06-06 23:56:33 +02:00
ajnart
bf7b9637f7 🔥 Remove CPU module 2022-06-06 23:56:08 +02:00
ajnart
c552104413 🔥 Remove CPU module 2022-06-06 23:55:49 +02:00
ajnart
6fd23cf6a0 Add support for multiple Arr services
In the calendar, you can now have 2 separate Sonarr or Radarr instances
2022-06-06 23:40:45 +02:00
ajnart
e2f59383d6 🚑 Small UI hotfixes 2022-06-06 23:27:55 +02:00
ajnart
8b92135a80 💄 Make responsiveness better for mobile
Posters aren't huge on mobile anymore, yay
2022-06-06 22:32:57 +02:00
ajnart
aef4a30512 🚑 Hotfix position of the downloads module 2022-06-06 21:45:45 +02:00
Thomas Camlong
ace8bd75e7 🔀 Merge pull request #177 from ajnart/ajnart/issue150
 Ability to toggle categories
2022-06-06 21:40:02 +02:00
Thomas Camlong
2e461b4e7a 🔀 Merge pull request #179 from ajnart/ajnart/issue174
Password / Login Page
2022-06-06 21:39:47 +02:00
Thomas Camlong
3f87e939c9 🔀Merge pull request #180 from ajnart/ajnart/issue163
 Add different URL for API calls
2022-06-06 21:39:38 +02:00
Thomas Camlong
1d9dfc5102 🔀 Merge pull request #181 from ajnart/ajnart/issue147
Transmission Integration
2022-06-06 21:39:26 +02:00
ajnart
80a94d3778 FR: Transmission Integration
Fixes #147
2022-06-06 21:38:50 +02:00
ajnart
39d66faf4e Add autocomplete to Search Module
Suggestions when searching with the search bar Fixes #12
2022-06-06 20:02:42 +02:00
ajnart
c50e11c75b 🐛 Fix celcius to farenheit 2022-06-06 19:12:59 +02:00
ajnart
9a3ebb56cb 🎨 Quality of life : Use debouncedValue 2022-06-06 18:44:02 +02:00
ajnart
1d1495453a 🩹 Add default values for the categories to be opened by default 2022-06-06 18:31:42 +02:00
ajnart
26cfc485c2 Ability to toggle categories
Fixes #150
2022-06-06 18:31:40 +02:00
ajnart
83b4da282a Password / Login Page
Fixes #174
2022-06-06 18:30:14 +02:00
ajnart
ea972effb4 Add different URL for API calls
Fixes #163
2022-06-06 18:29:02 +02:00
ajnart
9686761c3d Merge branch 'master' into dev 2022-06-06 18:28:34 +02:00
ajnart
13a5a4a263 Revert CI changes 2022-06-06 18:28:26 +02:00
ajnart
339919cfff Add keyboard navigation (kind of)
Fixes #165
2022-06-06 17:39:18 +02:00
ajnart
2594a7caa5 💄 Disable item selection on mobile
Fixed Disable text selection in iOS (touch devices) #166
2022-06-06 17:37:42 +02:00
ajnart
2966be4fc4 Add support for multiple same service in Calendar
Fixes Calendar Support for Multiple Sonarr / Radarr #176
2022-06-06 15:34:33 +02:00
ajnart
5e21a7df9c 🐛 Fix bug in ping module
Module would not ping on the first activation / deactivation
2022-06-06 15:33:25 +02:00
Thomas Camlong
64eb00f2ee 🔀 Changing deluge/qbittorent to use href instead of origin
Thank you @VinnyVynce
2022-06-06 15:21:16 +02:00
ajnart
00928ae709 📱 Make the design way more responsive for mobile 2022-06-06 15:20:46 +02:00
ajnart
bbb912479b 🪝 AdduseSetSafeInterval hook 2022-06-06 15:02:41 +02:00
VinnyVynce
5b16589360 Change urls for href instead of origin 2022-06-06 07:07:35 -04:00
Thomas Camlong
39674fc769 Update docker_dev.yml 2022-06-06 12:22:32 +02:00
Thomas Camlong
e718fd6b80 v0.6.0 Categories and current download graphs ! 🥳 2022-06-03 14:14:47 +02:00
ajnart
bdaf70f26b 🚑 Hotfix errors 2022-06-03 14:11:24 +02:00
Thomas Camlong
44a7df5ae0 Merge pull request #170 from ajnart/system-info
System info
2022-06-03 13:56:45 +02:00
ajnart
25fa376c2d Update display for current CPU 2022-06-03 13:56:15 +02:00
Thomas "ajnart" Camlong
de3792fb6b 🐛 Fixing a bug related to imports 2022-06-03 13:56:14 +02:00
ajnart
64b1679b03 🐛 Fixing bugs in system info 2022-06-03 13:56:14 +02:00
ajnart
8da0b38662 Working on system info 2022-06-03 13:56:13 +02:00
ajnart
13fd1a9fc0 System-info WIP 2022-06-03 13:55:43 +02:00
Thomas Camlong
04c1b41015 Merge pull request #169 from ajnart/qol
🚸 Improve UX and QoL
2022-06-03 13:55:29 +02:00
Thomas Camlong
6a32b80098 📝 Update demo link 2022-06-02 17:51:21 +02:00
WalkxCode
759e02f74a 🔥 Remove modified files from pull request" 2022-06-01 20:00:21 +02:00
WalkxCode
5758019923 🚸 Improve UX and QoL 2022-06-01 19:53:57 +02:00
Thomas "ajnart" Camlong
cad160010d Make proxied requests for calendar 2022-06-01 16:19:32 +02:00
Thomas "ajnart" Camlong
56b6347824 🚧 Trying to improve calendar module 2022-06-01 15:32:29 +02:00
ajnart
c258003ec5 🐛 Fixed a bug in the Lidarr image display 2022-05-30 21:43:33 +02:00
ajnart
5ac5098a2a 💄 Small UI changes
Changed the color to use one of mantine's
2022-05-30 09:20:16 +02:00
ajnart
3c96053b7f Add a ScrollArea to the Downloads module 2022-05-30 09:19:49 +02:00
Thomas Camlong
67a89ba61a Merge pull request #167 from ajnart/build-optimization
Build optimization
2022-05-29 19:04:19 +02:00
ajnart
4c0a3ce48c 💚 Update CI 2022-05-29 18:57:03 +02:00
ajnart
2d2f9d8d19 🏗️ Update Yarn install 2022-05-29 18:55:22 +02:00
ajnart
0a7f98dd80 🏗️ Change packageManager 2022-05-29 18:47:12 +02:00
ajnart
5b4d302c17 ⬆️ Upgrade dependencies 2022-05-29 18:43:31 +02:00
ajnart
31d23852f7 Migrate from tabler-icons-react to @tabler/icons 2022-05-29 18:42:58 +02:00
ajnart
a9e8db5018 🔖 Bumb version to v0.6.0 2022-05-29 15:34:31 +02:00
ajnart
f3d1767daf 🚨 Linting 2022-05-29 15:33:44 +02:00
ajnart
b229aacba5 Add download module to the AppShelf 2022-05-29 15:32:39 +02:00
ajnart
174ed140ae Add total downloads module 2022-05-29 15:31:25 +02:00
ajnart
62635bffe9 💄 Style DownloadsModule (torrents) 2022-05-29 15:31:04 +02:00
ajnart
63e6efab1f Add TotalDownloadsModule to module exports 2022-05-29 15:30:50 +02:00
ajnart
ad1af0e07d 🧱 Move components in infrastructure 2022-05-29 15:30:23 +02:00
ajnart
cfd9eb94b5 💄 Improve boxShadows of menus
Makes them look better
2022-05-29 15:30:03 +02:00
ajnart
c6762281ef 📦 Add nivo for charts 💹 2022-05-29 15:29:11 +02:00
ajnart
7d09a0064a 🔥 Delete Download module from index 2022-05-29 15:28:45 +02:00
ajnart
2d6b9522c5 Make Calendar module fetch way more data 2022-05-29 11:25:53 +02:00
ajnart
1a2e752281 Add categories! 2022-05-29 10:45:49 +02:00
ajnart
c7c76ee22b 💄 Styling backgrounds of widgets 2022-05-29 09:11:46 +02:00
ajnart
0457c91ede 🚸 Improve add service autofill capabilities 2022-05-27 13:14:09 +02:00
ajnart
1a420c3b8b 🚑 Hotfix deluge torrent progress 2022-05-27 09:45:51 +02:00
Thomas Camlong
c993d32dd3 v0.5.2 : Torrents module : Deluge and qBittorrent integrations ! 🥳
v0.5.2 adds a bunch of QOL changes and the long awaited Deluge and qBittorrent integrations. More feature to come with the torrent module soon.
2022-05-26 21:29:48 +02:00
ajnart
1f66d64f24 Add deluge integration
Fixes #122
2022-05-26 21:08:16 +02:00
ajnart
54ce138475 Add deluge password saving 2022-05-26 21:07:01 +02:00
ajnart
6173c20616 🏷️ Update types for AppShelfMenu 2022-05-26 21:06:44 +02:00
ajnart
e3d22c6d3a 🏷️ Add deluge types 2022-05-26 21:06:17 +02:00
ajnart
fd44fbb208 📦 Add Deluge package for future deluge integration 2022-05-26 21:06:04 +02:00
ajnart
3dc0208a73 💄 Styling changes 2022-05-26 20:07:54 +02:00
ajnart
b6fcabc270 🐛 Fix footer display issues 2022-05-26 20:07:22 +02:00
ajnart
ee2e36bdfa Rename the download module 2022-05-26 19:45:18 +02:00
ajnart
6bc16a51f1 🔖 Bump version to v0.5.2 2022-05-26 19:16:33 +02:00
ajnart
b0c92c9951 💄 Style DownloadModule 2022-05-26 19:14:36 +02:00
ajnart
72fddda411 Make the Footer update a notification 2022-05-26 19:14:19 +02:00
ajnart
949379e6e6 🎨 Change default config 2022-05-26 18:35:28 +02:00
Thomas Camlong
17736fc432 Update README.md 2022-05-26 18:29:24 +02:00
ajnart
da31832a1e 💫 Add download module on the main page 2022-05-26 18:19:32 +02:00
ajnart
3a358a229d Add Download Module!
Shows current downloads from qbittorrent at the moment, transmission coming soon 😉
2022-05-26 18:19:12 +02:00
ajnart
a6875abfe3 💬 Update API to support getting downloads 2022-05-26 18:18:30 +02:00
ajnart
2aad3d3eb0 Add support for qBittorrent in AddAppShelfItem 2022-05-26 18:17:59 +02:00
ajnart
8e2d347ab5 🏗️ Rework moduleWrapper architecture 2022-05-26 18:16:57 +02:00
ajnart
8b055bc3b6 💄 Style the notifications to the bottom right 2022-05-26 18:16:24 +02:00
ajnart
54a68f1d74 🏷️ Update types to support qBittorrent login 2022-05-26 18:16:00 +02:00
ajnart
2fabd1908d 📦 Add @ctrl/qbittorrent as a dependency 2022-05-26 18:15:42 +02:00
ajnart
789e0510ea 🐛 Fix a bug with strings as module settings 2022-05-26 18:15:00 +02:00
ajnart
2c16075413 💄 WeatherModule styling 2022-05-26 18:13:51 +02:00
ajnart
96f58288ac 💄 DateModule styling 2022-05-26 18:13:35 +02:00
ajnart
d4168dcdf4 💄 Styling and UI changes 2022-05-26 18:13:23 +02:00
Thomas Camlong
c044da2b55 Update Crowdin configuration file 2022-05-26 00:44:24 +02:00
ajnart
1ec8f1db19 🚑 Critical hotfix for various bugs 2022-05-26 00:10:48 +02:00
ajnart
c725559e9b 🔥 Remove Crowdin 2022-05-25 18:32:55 +02:00
ajnart
044c3fdf4c Merge branch 'dev' of github.com:/ajnart/homarr into dev 2022-05-25 18:26:17 +02:00
ajnart
4026d0b6be 💄 Update poster styling 2022-05-25 18:26:13 +02:00
Thomas Camlong
151e37c282 Update Crowdin configuration file 2022-05-25 18:13:29 +02:00
Thomas Camlong
0a476f648a v0.5.1 : Readarr and Lidarr integrations !
### New Features
-  Lidarr and Readarr integrations
-  Add a way to delete a config via the API
-  Add a way to save a config and delete it
-  Add a key bind to open settings (CTRL + L)

### Bug Fixes
- 🐛 Fixing date issues with weather module
- 🐛 Fix Readarr date match

### UI Changes
- 💄 Totally rework how the media previews work!
- 💄 Make the settings menu a drawer instead
- 💄 Change the way the footer is displayed

### GitHub Changes
- 📝 (README): Updates documentation & Move to Wiki

### Other Changes
- 🧑‍💻 Added strings as an option type for modules
- 🏗️ Make the max notifications to 4
2022-05-25 13:17:54 +02:00
ajnart
3f2aa50f85 Totally rework how the media previews work! 2022-05-25 13:13:36 +02:00
ajnart
fbaaa389c2 🐛 Fix Readarr date match 2022-05-25 13:13:17 +02:00
ajnart
af83695d81 🏗️ Make the max notifications to 4 2022-05-25 13:12:12 +02:00
ajnart
2cb6781a94 Lidarr and Readarr integrations 2022-05-25 10:50:57 +02:00
ajnart
4f68f7e395 Add a keybind to open settings, CTRL + L 2022-05-24 23:02:27 +02:00
ajnart
6a14937112 Merge branch 'dev' of github.com:/ajnart/homarr into dev 2022-05-24 22:55:50 +02:00
ajnart
9eef4988e7 Add a way to save a config and delete it 2022-05-24 22:55:47 +02:00
ajnart
3855673787 Add a way to delete a config via the API 2022-05-24 22:55:28 +02:00
ajnart
a89b0746ba 💄 Make the settings menu a drawer instead 2022-05-24 22:55:10 +02:00
Bjorn Lammers
09dd5d7907 🔖 Update version to v0.5.1 2022-05-24 21:51:08 +02:00
Bjorn Lammers
f029483f1e 🔖 Update version to v0.5.1 2022-05-24 21:50:57 +02:00
Bjorn Lammers
364055b9b6 📝 Oopsie doopsie hehe 2022-05-24 21:48:06 +02:00
Thomas Camlong
8775ad249c Merge pull request #152 from ajnart/docs
📝 (README): Updates documentation & Move to Wiki
2022-05-24 21:39:50 +02:00
Bjorn Lammers
3249d766b3 📝 Add "Read the Wiki" 2022-05-24 21:39:34 +02:00
Bjorn Lammers
fd65dc8943 📝 Fix some bugs 2022-05-24 21:26:10 +02:00
WalkxCode
fd73c7f70d 📝 (README): Updates documentation & Move to Wiki 2022-05-24 21:21:20 +02:00
ajnart
4984866fb3 🚨 Linting and add icons
Adds future support for self hosted icons
2022-05-24 20:15:07 +02:00
ajnart
4ae4b224c7 🐛 Fixing issues with weahter module 2022-05-24 20:14:26 +02:00
ajnart
802f7fd6c7 🧑‍💻 Added strings as an option type for modules 2022-05-24 20:14:07 +02:00
ajnart
bbb35b236f 💄 Change the way the footer is displayed 2022-05-24 20:13:01 +02:00
Bjorn Lammers
2eb3b18499 Merge pull request #144 from ajnart/dev
v0.5.0 : Quality of life and dev experience
2022-05-23 22:24:24 +02:00
Bjorn Lammers
553be7da33 Merge pull request #144 from ajnart/dev
v0.5.0 : Quality of life and dev experience
2022-05-23 22:22:08 +02:00
Bjorn Lammers
260b850e1a 📝 Add star mention 2022-05-23 21:49:03 +02:00
Bjorn Lammers
726a4fddd3 🔥Remove fixed issues 2022-05-23 21:09:48 +02:00
Bjorn Lammers
318c094f27 📝 Update Docs to match new release 2022-05-23 18:30:11 +02:00
Thomas Camlong
6e0d3807e4 Merge pull request #142 from ajnart/dnd
Drag and drop ! (v0.5.0)
2022-05-23 17:02:18 +02:00
ajnart
10e9dc06dd ⚰️ Remove dead code 2022-05-23 16:52:43 +02:00
ajnart
e84687e5fc 🔖 Version v0.5.0 2022-05-23 14:44:01 +02:00
Thomas Camlong
361d41065c Merge branch 'dev' into dnd 2022-05-23 14:39:17 +02:00
ajnart
4c0fbc0b42 ⚰️ Remove dead code 2022-05-23 14:38:39 +02:00
ajnart
ef8e380956 🔥 Remove some other default configuration files 2022-05-23 14:34:17 +02:00
ajnart
5db28b1607 🚨 Fix storybook compilation 2022-05-23 14:23:05 +02:00
ajnart
dbfd4cf050 🐛 Fix search module default queryUrl 2022-05-23 12:38:10 +02:00
ajnart
ffd298a2b6 🐛 Fix line clamping in media display 2022-05-23 12:37:36 +02:00
ajnart
9b1b5906e7 ⬆️ Upgrade and remove dependencies 2022-05-23 11:48:25 +02:00
Thomas Camlong
19bd14c63d Merge branch 'dev' into dnd 2022-05-23 11:24:31 +02:00
ajnart
b69343af56 Introduce DND in main app shelf! 2022-05-23 11:20:08 +02:00
ajnart
94ee90eebb ⚰️ Remove dead code 2022-05-23 11:19:40 +02:00
ajnart
72b3097ad1 ⚰️ Remove dead code 2022-05-23 11:19:26 +02:00
Thomas Camlong
225f910fe8 Merge pull request #139 from ajnart/New-Config-Format
 Add new config format
2022-05-23 10:48:46 +02:00
ajnart
10d9ffc740 🚨 Fix compilation for types 2022-05-23 10:44:31 +02:00
ajnart
4202d25d62 📦 Add type definitions for UUID 2022-05-23 10:26:17 +02:00
ajnart
6a905e1b49 🚨 Lint code and prettier 2022-05-23 10:24:54 +02:00
ajnart
72e08f484f 🚑 Use different type of UUID 2022-05-23 10:23:10 +02:00
ajnart
64dbb9c025 Add drag and drop, fixes #88 2022-05-23 00:04:14 +02:00
ajnart
af2e0235bf Add new config format
Should be WAAAAY easier to work with modules now
2022-05-22 20:42:10 +02:00
ajnart
bf85818f8b 🐛 Fix #133 2022-05-22 20:40:10 +02:00
ajnart
1840713179 Basic drag and drop 2022-05-21 10:32:54 +02:00
ajnart
b11bffb7cf 🐛 Exclude stories from tsconfig 2022-05-21 10:32:35 +02:00
ajnart
bfb26a9402 🚑 Fix API url for services 2022-05-21 01:26:55 +02:00
ajnart
c3b11be2d0 🚑 Fix UUID by using crypto 2022-05-21 01:26:24 +02:00
ajnart
ecfb89de40 🏷️ Fix types
Fixed the apiKey field for a service
2022-05-21 01:02:45 +02:00
ajnart
e1eab70f93 Match config with URL typed
Homarr will now match a config with the URL used or return a 404 if not found
2022-05-21 01:01:20 +02:00
ajnart
adb341c0fa Add default icon, fix URL parsing
Fixes #121 and Fixes #132
2022-05-21 00:54:36 +02:00
ajnart
25ccdffeb9 Make logo clickable 2022-05-21 00:52:55 +02:00
ajnart
b98d399a9c Change 404 message 2022-05-21 00:52:39 +02:00
ajnart
f36e7b8abb Made service name clickable
Co-authored-by: Bjorn L. <walkxnl@gmail.com>
2022-05-20 23:03:42 +02:00
Thomas "ajnart" Camlong
667322d14e Use ID instead of only names 2022-05-20 22:34:36 +02:00
Thomas "ajnart" Camlong
9b440c0da3 🚧 Add basic BASE_URL and PORT env utilisation #76 2022-05-19 02:05:23 +02:00
Thomas Camlong
2586733a98 v0.4.0
Add Weather and Ping module
2022-05-18 23:22:14 +02:00
ajnart
7bc779b296 ⚰️ Remove dead code
Used to test the weather module
2022-05-18 23:13:32 +02:00
ajnart
6064dcb6a6 💄 Footer styling 2022-05-18 23:12:52 +02:00
ajnart
7c7b0cc970 💫 Add animations to the AppShelf 2022-05-18 23:12:34 +02:00
ajnart
c182397dd9 💫 Add animations to the PingModule 2022-05-18 23:11:58 +02:00
ajnart
dc5ee3bdf3 Add animations to the AppShelf 2022-05-18 22:51:12 +02:00
ajnart
c8e1295a4b Improve date module am/pm 2022-05-18 22:50:53 +02:00
ajnart
331c55240b Added Freedom units setting 2022-05-18 22:50:33 +02:00
Thomas Camlong
65037f9b56 Add Weather module (beta)
Shows the current weather !
2022-05-18 22:17:58 +02:00
Bjorn L
39853d79ce 🔧 Change versions to v0.4.0 2022-05-18 22:15:03 +02:00
Bjorn L
8530550347 🔧 Change versions to v0.4.0 2022-05-18 22:14:27 +02:00
Thomas Camlong
ba8e9ef63c Merge branch 'dev' into weather-module 2022-05-18 22:14:01 +02:00
ajnart
119f2d7e51 Add a proceudally generated options manager
This allows for options in settings generated based on their name in module config. Very important change 🧙
2022-05-18 22:11:37 +02:00
ajnart
b0be26300e 💄 Update AppShell menu and item styling
Co-authored-by: Bjorn L. <walkxnl@gmail.com>
2022-05-18 22:10:31 +02:00
ajnart
0400188ea7 🚚 Move the update indicator to the Footer
Co-authored-by: Bjorn L. <walkxnl@gmail.com>
2022-05-18 22:09:13 +02:00
ajnart
879581224a 🔥 Remove update indicator from settings
Co-authored-by: Bjorn L. <walkxnl@gmail.com>
2022-05-18 22:08:09 +02:00
ajnart
7e5602c881 🚨 Update eslint config
Co-authored-by: Bjorn L. <walkxnl@gmail.com>
2022-05-18 22:07:28 +02:00
Bjorn L
4870ea3e40 📝 Adds Docs for the Weather Module 2022-05-18 16:58:06 +02:00
Bjorn L
61c55acd50 📝 Adds Request Icons section 2022-05-18 16:55:48 +02:00
Thomas Camlong
c45421d27e Merge branch 'dev' into weather-module 2022-05-18 10:24:16 +02:00
Thomas "ajnart" Camlong
b396d2604f 🚑 Critical hotfix : Compilation failed 2022-05-18 10:23:18 +02:00
Thomas "ajnart" Camlong
28b6dcd1db 📦 Update deps 2022-05-18 10:10:42 +02:00
Thomas "ajnart" Camlong
1dd74ea7da 🐛 Try to fix module compilation 2022-05-17 22:59:02 +02:00
Thomas "ajnart" Camlong
64923b03d9 🎨 Fix architecture for CI 2022-05-17 22:59:02 +02:00
Thomas "ajnart" Camlong
2ba9d517a8 Improve weather module 2022-05-17 22:59:02 +02:00
Aj - Thomas
471a9f7407 Update page title 2022-05-17 22:59:02 +02:00
Aj - Thomas
bdf871b476 💄 � Update weather module styling 2022-05-17 22:59:02 +02:00
Aj - Thomas
ab860eeea1 � Weather module improvements 2022-05-17 22:59:02 +02:00
Thomas "ajnart" Camlong
50d760f3b8 Prepare for v0.3.2 2022-05-17 21:24:10 +02:00
Thomas "ajnart" Camlong
73d06e15fb Update tests for storybook 2022-05-17 21:04:19 +02:00
Thomas "ajnart" Camlong
49d57024b9 Advancement on the weather widget 2022-05-17 21:04:19 +02:00
Thomas "ajnart" Camlong
31deb5010f 💄 Improve styling of modules 2022-05-17 21:04:18 +02:00
Thomas "ajnart" Camlong
e86eb7798f 🚧 Set up the structure for the weather module 2022-05-17 21:04:16 +02:00
Thomas Camlong
2896423766 Add ping service module
 Add ping service module resolves #78
2022-05-17 20:59:44 +02:00
Thomas "ajnart" Camlong
696d0c582d 🐛 Clear the search input on search
Resolves #125
2022-05-17 20:58:55 +02:00
ajnart
e94cae620a Rever b7e8c51b29
Does not work. Apparently
2022-05-17 04:19:59 +02:00
ajnart
c9c6f2b0c9 Add ping service module
Resolves #78
2022-05-17 04:02:14 +02:00
ajnart
b8fe799ac6 ⚰️ Remove dead code for the settings
I turned the settings into a module in 4cb8539143
2022-05-17 02:07:38 +02:00
ajnart
4cb8539143 Make the search bar a module
Resolves #118
2022-05-17 02:04:44 +02:00
ajnart
16b86870c4 🏗️ Fix small bug in code arch, forgot the key 2022-05-17 02:03:52 +02:00
ajnart
d4ce2a3ed6 🏷️ Update types for the SearchBar 2022-05-17 01:52:43 +02:00
ajnart
a474f3e4ee 🥅 Add 404 to catch errors
Reduce the ammount of visible errors by adding a 404 page.
2022-05-17 01:44:26 +02:00
ajnart
9a49fbb747 💄 Update AppShelf UI 2022-05-17 01:43:40 +02:00
ajnart
e3d47d78e0 🐛 Add a delay before opening search results
Resolves #115
2022-05-17 01:23:19 +02:00
ajnart
d62189f086 💄 Remove version from logo and add it in footer
resolves #116
2022-05-17 01:01:26 +02:00
ajnart
bb1b3d7d9a Merge branch 'dev' of github.com:/ajnart/homarr into dev 2022-05-17 00:55:44 +02:00
ajnart
13aeeefb22 🐛 Fix AddAppShelfItem image fit not properly set
Resolves #117
2022-05-17 00:55:24 +02:00
ajnart
8cdc9c3e29 🎨 Use user prefered theme 2022-05-17 00:42:27 +02:00
ajnart
3e31a4d38e 💄 Better style events in the calendar 2022-05-17 00:42:27 +02:00
ajnart
0cb3db6b89 📦 Upgrade package 2022-05-17 00:42:27 +02:00
ajnart
b7e8c51b29 🎨 Use user prefered theme 2022-05-17 00:19:41 +02:00
ajnart
e60db9f57a 💄 Better style events in the calendar 2022-05-17 00:19:24 +02:00
ajnart
2c707e86aa 📦 Upgrade package 2022-05-17 00:18:22 +02:00
Thomas Camlong
5c6541e1a7 🚀 Patch v0.3.1
Patch v0.3.1
2022-05-16 23:50:54 +02:00
Bjorn L
da81783c8e 🧑‍💻 Adds release note template 2022-05-16 23:11:48 +02:00
Chris
6a90a124b3 Update docker.yml 2022-05-16 23:03:16 +02:00
Chris
bd6edbbec6 Reverting changes from c593334
Changing to back to how it was before c593334
2022-05-16 23:03:16 +02:00
Chris
53ab06f97e Update docker_dev.yml 2022-05-16 23:03:16 +02:00
Chris
6904018585 temp edit 2 2022-05-16 23:03:16 +02:00
Chris
8c14b3ccf9 temp edit to test workflow 2022-05-16 23:03:16 +02:00
Chris
8557820e6e Update docker_dev.yml 2022-05-16 23:03:16 +02:00
Walkx
3782499da5 💚 Update .github/workflows/docker_dev.yml
Co-authored-by: Thomas Camlong <49837342+ajnart@users.noreply.github.com>
2022-05-16 23:03:16 +02:00
Walkx
97ca45964a 💚 Update .github/workflows/docker_dev.yml
Co-authored-by: Thomas Camlong <49837342+ajnart@users.noreply.github.com>
2022-05-16 23:03:16 +02:00
Walkx
7fa464b38f 💚 Update .github/workflows/docker.yml
Co-authored-by: Thomas Camlong <49837342+ajnart@users.noreply.github.com>
2022-05-16 23:03:16 +02:00
Chris
fe5fa99b4a 💚 Fixed CI fail on PR from fork
Fixed CI failing when a PR is made from a fork due to user from fork not having the permission packages: write. CI will now check if it has write perms before push built docker image.
2022-05-16 23:03:16 +02:00
Chris
9bf8b337f6 test CI fix 4 2022-05-16 23:03:16 +02:00
Chris
06caa2ca5e test CI fix 3 2022-05-16 23:03:16 +02:00
Chris
1145ee39b6 test CI fix 2 2022-05-16 23:03:16 +02:00
Chris
68111616fe test CI fix 2022-05-16 23:03:16 +02:00
Chris
7662c11bb5 💚 CI won't push to docker on PRs from forks
If not from fork it still will. Since that's how it was set by ajnart.
2022-05-16 23:03:16 +02:00
Chris
1aaa575480 💚 Stop running CI when ignored files are updated
CI will now not run when some files are updated that are not related to building.
2022-05-16 23:03:16 +02:00
Chris
3529e46b11 🔥 Remove unneeded lines
Removed adduser and addgroup since they aren't needed.
2022-05-16 23:03:16 +02:00
ajnart
006b1a61bf 💄 Update styling of AppShelf 2022-05-16 23:03:16 +02:00
ajnart
f5eb36ff00 💄 Update styling of AppShelf 2022-05-16 23:03:16 +02:00
Aj - Thomas
a97c9b0c0f 💄 Update styling 2022-05-16 23:03:16 +02:00
Aj - Thomas
08daeb87bc � Header styling 2022-05-16 23:03:16 +02:00
Aj - Thomas
ebc7ba9684 � Update search bar styling 2022-05-16 23:03:16 +02:00
Aj - Thomas
8392dcef20 � Module styling 2022-05-16 23:03:16 +02:00
Aj - Thomas
20a37b678f Update header styling 2022-05-16 23:03:16 +02:00
Aj - Thomas
d3bd894c2a 🙈 Add configs to gitingore 2022-05-16 23:03:16 +02:00
Aj - Thomas
e75ff14975 🔥 Remove search bar from index 2022-05-16 23:03:16 +02:00
Aj - Thomas
ab1e2a32a0 🔥 Remove Navbar 2022-05-16 23:03:16 +02:00
Aj - Thomas
22cd5c8b93 Add search bar in top bar 2022-05-16 23:03:16 +02:00
Aj - Thomas
5c8b1c4fc4 Add all components in Aside bar 2022-05-16 23:03:16 +02:00
Aj - Thomas
a71b50e33f 📄 Documenting keybinds for theme switch 2022-05-16 23:03:16 +02:00
Aj - Thomas
d4c1148025 💄 Style the modal for adding a service 2022-05-16 23:03:16 +02:00
Aj - Thomas
0d11244506 ♻ Rework the search bar 2022-05-16 23:03:16 +02:00
Aj - Thomas
e786b1e44f ✏ Fix typos 2022-05-16 23:03:16 +02:00
Aj - Thomas
509873db55 💄 App shell styling
The modal now looks a little bit better
2022-05-16 23:03:16 +02:00
Aj - Thomas
c5178ee288 💄 Styling and responsiveness
Co-authored-by: Walkx <walkxnl@gmail.com>
2022-05-16 23:03:16 +02:00
Walkx
4045628166 🔥 Remove this random href 2022-05-16 23:03:16 +02:00
Thomas Camlong
f8b2d64a26 📝 Updates documentation
Thanks to @walkxcode
2022-05-16 22:53:36 +02:00
Walkx
62ba99f6cd 📝 Updates ToC and adds Back to Top link 2022-05-16 16:59:08 +02:00
Walkx
2fad4d06bd 📝 Adds Known Issues 2022-05-16 16:58:20 +02:00
Walkx
c9e58e17da 📝 Update Docs 2022-05-16 16:17:41 +02:00
Walkx
8e03719a51 📝 Update README.md
Co-authored-by: Thomas Camlong <49837342+ajnart@users.noreply.github.com>
2022-05-16 16:15:04 +02:00
Walkx
c4df55060b 📝 Update Docs 2022-05-16 15:43:58 +02:00
Walkx
47c636e810 📝 Updates documentation 2022-05-16 01:30:35 +02:00
Walkx
38d18fc433 🚀Change demo page link 2022-05-15 22:45:55 +02:00
Aj - Thomas
7b6fd5ed6a 🚀 🔖 Release v0.3.0 2022-05-15 19:50:17 +02:00
Walkx
8603395329 ✏️ Remove space because not everyone is french 2022-05-15 19:43:16 +02:00
Thomas "ajnart" Camlong
468b1912b8 🔖 Upgrade Homarr to v0.3.0 2022-05-15 19:32:26 +02:00
Thomas "ajnart" Camlong
c243256180 💄 Improve layout styling 2022-05-15 19:32:02 +02:00
Thomas "ajnart" Camlong
a9370881f4 💄 Improve styling of modules 2022-05-15 19:17:16 +02:00
Thomas "ajnart" Camlong
81a63cd1b7 👷 Add back raw dev tag 2022-05-15 19:13:42 +02:00
Thomas "ajnart" Camlong
f03219fd42 👷 Fix branches for CI pushes 2022-05-15 18:59:27 +02:00
Walkx
2e8f3d7d1f Merge pull request #102 from ajnart/better-tooltip
✏️ Improves the searchbar tooltip
2022-05-15 18:52:23 +02:00
Walkx
9ddb08cc9c ✏️ Improves the searchbar tooltip 2022-05-15 15:57:58 +02:00
Walkx
d44eed6581 🔥 Removes fixed known issues 2022-05-15 14:46:46 +02:00
Thomas "ajnart" Camlong
59b9baa579 Add custom search querry url in settings menu 2022-05-15 13:09:25 +02:00
Thomas "ajnart" Camlong
027ac94e80 🍱 Update logo and favicon 2022-05-15 12:42:53 +02:00
Aj - Thomas
b1cec402c3 👷 Remove test name from GitHub CI 2022-05-14 22:27:15 +02:00
Aj - Thomas
f2a7f83e12 ️ Work on responsiveness for the AppShelf
Fixes #1, Fixes #42, Fixes #82, Fixes #85
2022-05-14 21:45:54 +02:00
Thomas "ajnart" Camlong
477ff8241e 📱 More work on responsiveness 2022-05-14 21:42:30 +02:00
Thomas "ajnart" Camlong
95bae5929c 💄 Improve minor styles 2022-05-14 21:42:11 +02:00
Thomas "ajnart" Camlong
4545a6bbf5 Rework header, footer, logo
Fixes Align Homarr logo to the left #96
2022-05-14 21:41:30 +02:00
Aj - Thomas
b3a97431b3 Merge pull request #94 from ajnart/new-logo
🍱️ Update logo and add better accessibility
2022-05-14 20:02:13 +02:00
Aj - Thomas
b757046de8 Merge pull request #91 from c00ldude1oo/master
📝 Add build status badges
2022-05-14 20:00:40 +02:00
Walkx
0d1a1b899a 🧑‍💻 Adds template for ideas 2022-05-14 16:50:54 +02:00
Chris
71be4101a5 📝 Add Badges. Moved under name
Removed docker dev build status badge
Added docker pulls(downloads) badge
Added Release version badge
moved them under name
2022-05-14 10:27:40 -04:00
Walkx
db453d0f74 ✏️ Fixes yaml issue
Removes the : in the description
2022-05-14 13:56:10 +02:00
Walkx
1d450428c9 🧑‍💻 Improves feature request issue template 2022-05-14 13:55:20 +02:00
WalkxCode
e16601d113 ️ (favicon): Changes the favicon to .png
This improves the accessibility because some browsers don't support .svg. And some services fetch their icons from favicons!
2022-05-14 13:45:22 +02:00
WalkxCode
56b52d0808 🍱 (logo): Adds new logo & changes favicon
favicon will need to be changed from .svg to .png in code
2022-05-14 13:42:32 +02:00
Walkx
3fc0a2c64f 🧑‍💻 Adds back label & idiot check 2022-05-14 13:35:29 +02:00
Thomas "ajnart" Camlong
f3f2006f14 💄 Improving style 2022-05-14 11:40:05 +02:00
Thomas "ajnart" Camlong
2139f48df3 Work on responsiveness for the AppShelf 2022-05-14 11:19:04 +02:00
Thomas "ajnart" Camlong
09483ada01 👷 Update CI image name for easier deployment 2022-05-14 11:19:04 +02:00
Thomas "ajnart" Camlong
c593334be8 👷 Update CI image name for easier deployment 2022-05-14 11:18:50 +02:00
Thomas "ajnart" Camlong
af7d078293 👷 Fix CI 2022-05-14 10:28:24 +02:00
Thomas "ajnart" Camlong
96ab5dd9a7 🐛 Fix storybook and remove Jest 2022-05-14 10:10:18 +02:00
Thomas "ajnart" Camlong
9a3ef24619 💄 SearchBar styling 2022-05-14 10:09:22 +02:00
Thomas "ajnart" Camlong
3287752e45 ⚰️ Remove dead code 2022-05-14 10:08:26 +02:00
Chris
1c560f9a5b 📝 Add build status badges
Added build status badges 
made image bit bigger
2022-05-14 02:56:39 -04:00
Aj - Thomas
534cf3571a 🐳 Fix docker build 2022-05-14 01:29:50 +02:00
Aj - Thomas
cf17aa61cc Update basic layout 2022-05-14 01:19:12 +02:00
Aj - Thomas
32f81cefe7 🏗️ 💥 Change the whole folder structure.
Now using src as a subfolder to the source files
2022-05-14 01:14:56 +02:00
Aj - Thomas
15bb08e5f3 Merge branch 'master' into dev 2022-05-14 00:56:38 +02:00
Aj - Thomas
0a6346b383 📝 Update README.md 2022-05-14 00:55:04 +02:00
Walkx
43ee6899ae 📝 Adds known issues 2022-05-14 00:55:04 +02:00
Walkx
5159edbea1 📝 Adds documentation for volume mounting 2022-05-14 00:55:04 +02:00
Aj - Thomas
79dd1f3296 🔥 Remove labels from issue templates 2022-05-14 00:55:04 +02:00
Aj - Thomas
7a3ac58e4d 🔥 Remove labels from issue templates 2022-05-14 00:55:03 +02:00
Chris
527eb373a9 👷 Add dev builder
New workflow to build dev branch on push/pulls and upload to ghcr.io with dev tag
2022-05-14 00:52:11 +02:00
Aj - Thomas
16ecf59196 📝 Adds known issues
Thanks to @walkxcode
2022-05-13 23:29:49 +02:00
Aj - Thomas
66dd59f076 📝 Update README.md 2022-05-13 23:29:30 +02:00
Aj - Thomas
e95168288c 📝 Adds documentation for volume mounting
Thanks to @walkxcode
2022-05-13 23:26:53 +02:00
Aj - Thomas
4f5121b337 🔥 Remove labels from issue templates 2022-05-13 22:14:12 +02:00
Aj - Thomas
f0152d84d8 🔥 Remove labels from issue templates 2022-05-13 22:13:57 +02:00
Walkx
cc324dd8ec 📝 Adds known issues 2022-05-13 20:56:34 +02:00
Walkx
d332245cfc 📝 Adds documentation for volume mounting 2022-05-13 18:58:06 +02:00
Aj - Thomas
ac9ebe4160 Fix docker image volume mounting
Fixes #62 [🐛 Bug] Permission denied when writing configs
2022-05-13 18:14:08 +02:00
Aj - Thomas
b51e8861b7 Update README.md 2022-05-13 17:31:51 +02:00
Aj - Thomas
51fcba7109 Update README.md 2022-05-13 17:29:03 +02:00
Aj - Thomas
ce3afc6916 Delete preview.png 2022-05-13 17:19:43 +02:00
Aj - Thomas
fb1d2c3e80 Merge pull request #71 from ajnart/dev-experience
🧑‍💻 Aims to get 100% of the Community Standards
2022-05-13 16:58:58 +02:00
Aj - Thomas
d372b66e44 Update feature-request.yml 2022-05-13 16:24:02 +02:00
Walkx
4417168ef4 🔥 Remove label from title 2022-05-13 16:23:29 +02:00
Walkx
9a4fdc6f01 🔥 Remove label from title 2022-05-13 16:23:17 +02:00
Aj - Thomas
405537202d Update module.yml 2022-05-13 16:20:44 +02:00
Aj - Thomas
873d590606 Update bug.yml 2022-05-13 16:20:32 +02:00
Aj - Thomas
463670ebdf Update feature-request.yml 2022-05-13 16:20:13 +02:00
Walkx
eeb3d4eb23 🔥 Remove auto-assign for bugs 2022-05-13 15:32:49 +02:00
Walkx
651003f1d7 📝 Adds preview of Homarr
(Temporary until we have good video preview (.mov or .gif))
2022-05-13 14:17:58 +02:00
Walkx
2ee923d773 📝 Adds preview for docs 2022-05-13 14:16:54 +02:00
Aj - Thomas
e9c758b63d Create module.yml 2022-05-13 14:10:18 +02:00
Walkx
6a462f640a 📝 Adds set of rules for modules 2022-05-13 13:50:23 +02:00
Walkx
6120eb53cd 🧑‍💻 Adds pull request template 2022-05-13 13:41:56 +02:00
Walkx
a3a3846848 📝 Adds Contributor Covenant Code of Conduct 2022-05-13 13:35:01 +02:00
Walkx
f547c56c1f 📝 Updates Readme to point to Contribution Guidelines 2022-05-13 13:32:00 +02:00
Walkx
242d09e932 🧑‍💻 Adds contribution guide 2022-05-13 13:29:32 +02:00
Aj - Thomas
036922328f Merge pull request #66 from ajnart/note
fix: Make note look better
2022-05-13 13:16:20 +02:00
Aj - Thomas
c7257b8c2f Merge pull request #65 from ajnart/discordcta
fix: Center align Discord CTA
2022-05-13 13:16:00 +02:00
Walkx
eecd8b0faa fix: Make note look better 2022-05-13 13:06:13 +02:00
Walkx
63940ab5ff fix: Center align Discord CTA 2022-05-13 13:03:33 +02:00
Aj - Thomas
62f62db17c Update README.md 2022-05-13 10:37:36 +02:00
144 changed files with 22748 additions and 14266 deletions

View File

@@ -20,6 +20,7 @@ module.exports = {
},
rules: {
'react/react-in-jsx-scope': 'off',
'react/no-children-prop': 'off',
"unused-imports/no-unused-imports": "warn",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-unused-imports": "off",

View File

@@ -1,9 +1,7 @@
name: 🐛 Bug Report
description: Report something that's broken, or not working like intented!
title: '[🐛 Bug] <title>'
title: '<title>'
labels: ['🐛 Bug']
assignees:
- ajnart
body:
- type: dropdown
id: environment

View File

@@ -1,14 +1,13 @@
name: ✨ Feature Request
description: Request a feature to help improve Homarr!
title: '[✨ Feature] <title>'
title: '<title>'
labels: ['✨ Feature']
assignees:
- ajnart
body:
- type: textarea
id: feature
attributes:
label: Describe the feature you would like to see
label: Description
description: Describe the feature you would like to see. Tell us how you imagine it and try to provide as much useful information as possible. **PLEASE** use images/screenshots, include details about X & Y when requesting changes like X & Y service does, make your description atleast 300 characters. Having an unclear issue with too little detail will result in your issue being marked as invalid and closed.
placeholder: An outline of the feature you would like to see implemented, include as much detail as possible!
validations:
required: true
@@ -23,3 +22,13 @@ body:
- High (App breaking feature)
validations:
required: true
- type: checkboxes
id: idiot-check
attributes:
label: Please tick the boxes
description: Before submitting, please ensure that
options:
- label: You've read the [docs](https://github.com/ajnart/homarr#readme)
required: true
- label: You've checked for [duplicate issues](https://github.com/ajnart/homarr/issues)
required: true

23
.github/ISSUE_TEMPLATE/idea.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: 🤔 Idea
description: Tell us your idea! We may implement it.
title: '<title>'
labels: ['🤔 Idea']
body:
- type: textarea
id: feature
attributes:
label: Description
description: Tell us your idea! Please add as much details as possible.
placeholder: Maybe move ... to ...! Maybe add the version of Homarr somewhere...! Etc.
validations:
required: true
- type: checkboxes
id: idiot-check
attributes:
label: Please tick the boxes
description: Before submitting, please ensure that
options:
- label: You've read the [docs](https://github.com/ajnart/homarr#readme)
required: true
- label: You've checked for [duplicate issues](https://github.com/ajnart/homarr/issues)
required: true

23
.github/ISSUE_TEMPLATE/module.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: 🏗️ Module request
description: Request for a module to be added / an integration with you favourite service !
title: '<title>'
body:
- type: textarea
id: name
attributes:
label: Name the integration
description: Please describe the name of the Module/Integration you want to see and info that could help us with adding it. For example screenshots/mockups for inspiration. API links or already existing JavaScript/TypeScript integration of the API
validations:
required: true
- type: checkboxes
id: idiot-check
attributes:
label: Please tick the boxes
description: Before submitting, please ensure that
options:
- label: You've read the [docs](https://github.com/ajnart/homarr#readme)
required: true
- label: You've checked for [duplicate issues](https://github.com/ajnart/homarr/issues)
required: true
- label: You're not just putting an idea out there and actually give usefull information about how to implement your module idea
required: true

16
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,16 @@
*Thank you for contributing to Homarr! So that your Pull Request can be handled effectively, please populate the following fields (delete sections that are not applicable)*
### Category
> One of: Bugfix / Feature / Code style update / Refactoring Only / Build related changes / Documentation / Other (Please specify!)
### Overview
> Briefly outline your new changes...
### Issue Number _(if applicable)_
> Related issue: #00
### New Vars _(if applicable)_
> If you've added any new build scripts, environmental variables, config file options, dependency please outline here.
### Screenshot _(if applicable)_
> If you've introduced any significant UI changes, please include a screenshot.

27
.github/release-note.md vendored Normal file
View File

@@ -0,0 +1,27 @@
## 🦞 Homarr [v0.0.0](https://github.com/ajnart/homarr/compare/v0.0.0...v0.0.0) (2022-01-01)
<!-- Small release message -->
<!-- Bigger announcement marked in bold -->
### Upgrade Steps
*Upgrading without a mounted config? Make sure to download your config from the settings first! You can add it back later by drag and dropping it into your browser.*
* `docker pull ghcr.io/ajnart/homarr:latest`
* `docker stop [container_id]`
* `docker rm [container_id]`
* `docker run --name homarr -p 7575:7575 -v /data/docker/homarr:/app/data/configs -d ghcr.io/ajnart/homarr:latest`
* *(or use our [docker_compose.yml](https://github.com/ajnart/homarr#-installation))*
### Breaking Changes
### New Features
### Bug Fixes
### UI Changes
### GitHub Changes
### Other Changes
_**Special thanks to our contributors: @ajnart, @c00ldude1oo, @walkxcode, and of course all people using our project.**_

View File

@@ -1,18 +1,28 @@
name: Build and publish Docker image
name: Master docker CI
# Workflow to build and publish docker image
on:
push:
branches: [master]
paths-ignore:
- '.github/**'
- '**.md'
tags:
- v*
workflow_dispatch:
env:
IMAGE_NAME: homarr
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
build:
yarn_install_and_build:
# Will run yarn install && yarn build
runs-on: ubuntu-latest
steps:
- name: Setup
@@ -20,9 +30,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Get yarn cache directory path
# to help speed up build times
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn cache
# to help speed up build times
uses: actions/cache@v3
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
@@ -40,9 +52,10 @@ jobs:
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
# If source files changed but packages didn't, rebuild from a prior cache.
restore-keys: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
- run: yarn install --frozen-lockfile
- run: yarn install --immutable
- run: yarn build
- name: Cache build output
# to copy needed files to docker build job
uses: actions/cache@v2
id: restore-build
with:
@@ -52,11 +65,11 @@ jobs:
./public/
./.next/static/
./.next/standalone/
./packages.jsan
./packages.json
key: ${{ github.sha }}
docker:
needs: [build]
docker_image_build_and_push:
needs: [yarn_install_and_build]
runs-on: ubuntu-latest
permissions:
packages: write
@@ -73,14 +86,14 @@ jobs:
./public/
./.next/static/
./.next/standalone/
./packages.jsan
./packages.json
key: ${{ github.sha }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
# list of Docker images to use as base name for tags
images: ghcr.io/${{ github.repository }}
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# generate Docker tags based on the following events/attributes
tags: |
type=raw,value=latest
@@ -90,7 +103,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ghcr.io
@@ -102,6 +114,6 @@ jobs:
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

136
.github/workflows/docker_dev.yml vendored Normal file
View File

@@ -0,0 +1,136 @@
name: Development CI
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
push:
branches: [dev]
paths-ignore:
- '.github/**'
- '**.md'
pull_request:
paths-ignore:
- '.github/**'
- '**.md'
workflow_dispatch:
inputs:
tags:
required: true
description: 'Tags to deploy to'
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
yarn_install_and_build:
runs-on: ubuntu-latest
steps:
- name: Setup
uses: actions/setup-node@v3
- name: Checkout
uses: actions/checkout@v3
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn cache
uses: actions/cache@v3
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-
- name: Nextjs cache
uses: actions/cache@v2
with:
# See here for caching with `yarn` https://github.com/actions/cache/blob/main/examples.md#node---yarn or you can leverage caching with actions/setup-node https://github.com/actions/setup-node
path: |
~/.npm
${{ github.workspace }}/.next/cache
# Generate a new cache whenever packages or source files change.
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
# If source files changed but packages didn't, rebuild from a prior cache.
restore-keys: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
- run: yarn install --immutable
- run: yarn build
- name: Cache build output
uses: actions/cache@v2
id: restore-build
with:
path: |
./next.config.js
./pages/
./public/
./.next/static/
./.next/standalone/
./packages.json
key: ${{ github.sha }}
docker_image_build_and_push:
needs: [yarn_install_and_build]
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/cache@v2
id: restore-build
with:
path: |
./next.config.js
./pages/
./public/
./.next/static/
./.next/standalone/
./packages.json
key: ${{ github.sha }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
# list of Docker images to use as base name for tags
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# generate Docker tags based on the following events/attributes
tags: |
type=ref,event=pr
tpye=raw,value=dev,priority=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

13
.gitignore vendored
View File

@@ -35,4 +35,15 @@ yarn-error.log*
*.tsbuildinfo
# storybook
storybook-static
storybook-static
data/configs
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
# Yarn v2
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View File

@@ -1,13 +1,9 @@
module.exports = {
stories: ['../components/**/*.story.mdx', '../components/**/*.story.*'],
stories: ['../src/components/**/*.story.mdx', '../src/components/**/*.story.*'],
addons: [
'storybook-dark-mode',
'@storybook/addon-links',
'storybook-addon-mock/register',
'@storybook/addon-essentials',
{
name: 'storybook-addon-turbo-build',
options: { optimizationLevel: 2 },
},
],
typescript: {
check: false,

View File

@@ -1,4 +1,3 @@
import { useDarkMode } from 'storybook-dark-mode';
import { MantineProvider, ColorSchemeProvider } from '@mantine/core';
import { NotificationsProvider } from '@mantine/notifications';
@@ -7,11 +6,7 @@ export const parameters = { layout: 'fullscreen' };
function ThemeWrapper(props: { children: React.ReactNode }) {
return (
<ColorSchemeProvider colorScheme="light" toggleColorScheme={() => {}}>
<MantineProvider
theme={{ colorScheme: useDarkMode() ? 'dark' : 'light' }}
withGlobalStyles
withNormalizeCSS
>
<MantineProvider withGlobalStyles withNormalizeCSS>
<NotificationsProvider>{props.children}</NotificationsProvider>
</MantineProvider>
</ColorSchemeProvider>

786
.yarn/releases/yarn-3.2.1.cjs vendored Normal file

File diff suppressed because one or more lines are too long

5
.yarnrc Normal file
View File

@@ -0,0 +1,5 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
yarn-path ".yarn/releases/yarn-1.22.19.cjs"

3
.yarnrc.yml Normal file
View File

@@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.2.1.cjs

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
ajnart@pm.me.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

106
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,106 @@
# Contributing to Homarr
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
## Code of Conduct
This project and everyone participating in it is governed by the
[Homarr Code of Conduct](https://github.com/ajnart/homarr/blob/master/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
to [@ajnart](https://github.com/ajnart).
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/ajnart/homarr/#readme).
Before you ask a question, it is best to search for existing [Issues](https://github.com/ajnart/homarr/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Issue](https://github.com/ajnart/homarr/issues/new).
- Provide as much context as you can about what you're running into.
- Provide project and platform versions (nodejs, docker, etc), depending on what seems relevant.
We will then take care of the issue as soon as possible.
## I Want To Contribute
> ### Legal Notice
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
### Reporting Bugs
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/ajnart/homarr/#readme). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/ajnart/homarr/issues?q=is%3Aopen+is%3Aissue+label%3A%22%F0%9F%90%9B+Bug%22).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of yarn, nodejs, docker, npm, next, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to ajnart@pm.me.
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/ajnart/homarr/issues/new).
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for Homarr, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation](https://github.com/ajnart/homarr/#readme) carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/ajnart/homarr/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://github.com/ajnart/homarr//issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. <!-- this should only be included if the project has a GUI -->
- **Explain why this enhancement would be useful** to most Homarr users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
## Styleguides
### Commit Messages
Homarr uses [GitMoji](https://gitmoji.dev/).
We would appreciate it if everyone keeps their commit messages withing these rulings.

View File

@@ -1,20 +1,14 @@
FROM node:16-alpine
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY /next.config.js ./
COPY /public ./public
COPY /package.json ./package.json
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --chown=nextjs:nodejs /.next/standalone ./
COPY --chown=nextjs:nodejs /.next/static ./.next/static
USER nextjs
# Automatically leverage output traces to reduce image size. https://nextjs.org/docs/advanced-features/output-file-tracing
COPY /.next/standalone ./
COPY /.next/static ./.next/static
EXPOSE 7575
ENV PORT 7575
RUN apk add tzdata
VOLUME /app/data/configs
CMD ["node", "server.js"]

203
README.md
View File

@@ -1,86 +1,201 @@
<p align = "center">
<h3 align = "center"> Homarr <h3>
<p align = "center">
A homepage for <i>your</i> server.
<br/>
<a href = "https://github.com/ajnart/homarr/deployments/activity_log?environment=Production" > <strong> Demo ↗️ </strong> </a> • <a href = "#install" > <strong> Install ➡️ </strong> </a>
<br />
<br />
<a href = "https://discord.gg/aCsmEV5RgA" > <img src="https://discordapp.com/api/guilds/972958686051962910/widget.png?style=shield" > </a>
</p>
<!-- Project Title -->
<h1 align="center">Homarr</h1>
<!-- Badges -->
<p align="center">
<img src="https://img.shields.io/github/stars/ajnart/homarr?label=%E2%AD%90%20Stars&style=flat-square?branch=master&kill_cache=1%22">
<a href="https://github.com/ajnart/homarr/releases/latest">
<img alt="Latest Release (Semver)" src="https://img.shields.io/github/v/release/ajnart/homarr?label=%F0%9F%9A%80%20Release">
</a>
<a href="https://github.com/ajnart/homarr/actions/workflows/docker.yml">
<img title="Docker CI Status" src="https://github.com/ajnart/homarr/actions/workflows/docker.yml/badge.svg" alt="CI Status">
</a>
<a href="https://discord.gg/aCsmEV5RgA">
<img title="Discord" src="https://discordapp.com/api/guilds/972958686051962910/widget.png?style=shield">
</a>
</p>
# 📃 Table of Contents
- [📃 Table of Contents](#-table-of-contents)
- [🚀 Getting Started](#-getting-started)
- [ About](#-about)
- [⚡ Installation](#-installation)
- [Deploying from Docker Image 🐳](#deploying-from-docker-image-)
- [Building from Source 🛠️](#building-from-source-)
- [💖 Contributing](#-contributing)
<!-- Links -->
<p align="center">
<i>Join the discord! — Don't forget to star the repo if you are enjoying the project!</i>
</p>
<p align="center">
<a href="https://homarr.ajnart.fr/"><strong> Demo ↗️ </strong></a> • <a href="#-installation"><strong> Install ➡️ </strong></a> • <a href="https://github.com/ajnart/homarr/wiki"><strong> Read the Wiki 📄 </strong></a>
</p>
<!-- Getting Started -->
# 🚀 Getting Started
---
## About
<!-- Homarr Description -->
<img align="right" width=250 src="public/imgs/logo-color.svg" />
Homarr is a simple and lightweight homepage for your server, that helps you easily access all of your services in one place.
**[⤴️ Back to Top](#-table-of-contents)**
## ⚡ Installation
It integrates with the services you use to display information on the homepage (E.g. Show upcoming Sonarr/Radarr releases).
### Deploying from Docker Image 🐳
For a full list of integrations look at: [wiki/integrations](#).
If you have any questions about Homarr or want to share information with us, please go to one of the following places:
- [Github Discussions](https://github.com/ajnart/homarr/discussions)
- [Discord Server](https://discord.gg/aCsmEV5RgA)
*Before you file an [issue](https://github.com/ajnart/homarr/issues/new/choose), make sure you have the read [known issues](#-known-issues) section.*
**For more information, [read the wiki!](https://github.com/ajnart/homarr/wiki)**
<details>
<summary><b>Table of Contents</b></summary>
<p>
- [✨ Features](#-features)
- [👀 Preview](#-preview)
- [💥 Known Issues](#-known-issues)
- [🚀 Installation](#-installation)
- [🐳 Deploying from Docker Image](#-deploying-from-docker-image)
- [🛠️ Building from Source](#-building-from-source)
- [💖 Contributing](#-contributing)
- [📜 License](#-license)
</p>
</details>
---
## ✨ Features
- Integrates with services you use.
- Search the web direcetly from your homepage.
- Real-time status indicator for every service.
- Automatically finds icons while you type the name of a serivce.
- Widgets that can display all types of information.
- Easy deployment with Docker.
- Very light-weight and fast.
- Free and Open-Source.
- And more...
**[⤴️ Back to Top](#homarr)**
---
## 👀 Preview
<img alt="Homarr Preview" align="center" width="100%" src="https://user-images.githubusercontent.com/71191962/169860380-856634fb-4f41-47cb-ba54-6a9e7b3b9c81.gif" />
**[⤴️ Back to Top](#homarr)**
---
## 💥 Known Issues
- Posters on the Calendar get blocked by adblockers. (IMDb posters)
**[⤴️ Back to Top](#homarr)**
---
## 🚀 Installation
### 🐳 Deploying from Docker Image
> Supported architectures: x86-64, ARM, ARM64
_Requirements_:
- [Docker](https://docs.docker.com/get-docker/)
**Standard Docker Install**
```sh
docker run --name homarr -p 7575:7575 -d ghcr.io/ajnart/homarr
```bash
docker run \
--name homarr \
--restart unless-stopped \
-p 7575:7575 \
-v ./homarr/configs:/app/data/configs \
-v ./homarr/icons:/app/public/icons \
-d ghcr.io/ajnart/homarr:latest
```
**Docker Compose**
```yml
---
version: '3'
#--------------------------------------------------------------------------------------------#
# Homarr - A homepage for your server. #
#--------------------------------------------------------------------------------------------#
#---------------------------------------------------------------------#
# Homarr - A homepage for your server. #
#---------------------------------------------------------------------#
services:
homarr:
container_name: homarr
image: ghcr.io/ajnart/homarr
image: ghcr.io/ajnart/homarr:latest
restart: unless-stopped
volumes:
- ./homarr/configs:/app/data/configs
- ./homarr/icons:/app/public/icons
ports:
- '7575:7575'
```
### Building from Source 🛠️
```sh
docker compose up -d
```
*Getting EACCESS errors in the logs? Try running `sudo chmod 777 /directory-you-mounted-to`!*
**[⤴️ Back to Top](#homarr)**
### 🛠️ Building from Source
_Requirements_:
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/) _(Latest or LTS)_
- [Yarn](https://yarnpkg.com/)
- Some web server
**Installing**
- Clone the GitHub repo: `git clone https://github.com/ajnart/homarr.git` & `cd homarr`
- Install all dependencies: `yarn install`
- Build the source: `yarn export`
- Start a web server (Any web server will work):
- _Examples:_
- NodeJS serve: `npm i -g serve` or `yarn global add serve` & `serve ./out`
- python http.server: `python -m http.server 7474 --directory out`
**[⤴️ Back to Top](#-table-of-contents)**
- Build the source: `yarn build`
- Start the NextJS web server: ``yarn start``
- *Note: If you want to update the code in real time, launch with ``yarn dev``*
# 💖 Contributing
You can contribute by [Starting a discussion](https://github.com/ajnart/homarr/discussions), [Submitting Bugs](https://github.com/ajnart/homarr/issues/new), [Requesting Features](https://github.com/ajnart/homarr/issues/new), or [Making a pull request](https://github.com/ajnart/homarr/compare)!
**[⤴️ Back to Top](#homarr)**
---
## 💖 Contributing
**Please read our [Contribution Guidelines](/CONTRIBUTING.md)**
All contributions are highly appreciated.
**[⤴️ Back to Top](#-table-of-contents)**
**[⤴️ Back to Top](#homarr)**
---
## 📜 License
Homarr is Licensed under [MIT](https://en.wikipedia.org/wiki/MIT_License)
```txt
Copyright © 2022 Thomas "ajnart" Camlong
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
**[⤴️ Back to Top](#homarr)**
---
<p align="center">
<i>Thank you for visiting! <b>For more information <a href="https://github.com/ajnart/homarr/wiki">read the wiki!</a></b></i>
<br/>
<br/>
</p>

View File

@@ -1,210 +0,0 @@
import {
Modal,
Center,
Group,
TextInput,
Image,
Button,
Select,
AspectRatio,
Text,
Card,
LoadingOverlay,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { motion } from 'framer-motion';
import { useState } from 'react';
import { Apps } from 'tabler-icons-react';
import { useConfig } from '../../tools/state';
import { ServiceTypeList } from '../../tools/types';
import { AppShelfItemWrapper } from './AppShelfItemWrapper';
export default function AddItemShelfItem(props: any) {
const [opened, setOpened] = useState(false);
return (
<>
<Modal
size="xl"
radius="lg"
opened={props.opened || opened}
onClose={() => setOpened(false)}
title="Add a service"
>
<AddAppShelfItemForm setOpened={setOpened} />
</Modal>
<AppShelfItemWrapper>
<Card.Section>
<Group position="center" mx="lg">
<Text
// TODO: #1 Remove this hack to get the text to be centered.
ml={15}
style={{
alignSelf: 'center',
alignContent: 'center',
alignItems: 'center',
justifyContent: 'center',
justifyItems: 'center',
}}
mt="sm"
weight={500}
>
Add a service
</Text>
</Group>
</Card.Section>
<Card.Section>
<AspectRatio ratio={5 / 3} m="xl">
<motion.i
whileHover={{
cursor: 'pointer',
scale: 1.1,
}}
>
<Apps style={{ cursor: 'pointer' }} onClick={() => setOpened(true)} size={60} />
</motion.i>
</AspectRatio>
</Card.Section>
</AppShelfItemWrapper>
</>
);
}
function MatchIcon(name: string, form: any) {
fetch(
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
.replace(/\s+/g, '-')
.toLowerCase()}.png`
)
.then((res) => {
if (res.status === 200) {
form.setFieldValue('icon', res.url);
}
})
.catch(() => {
// Do nothing
});
return false;
}
export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & any) {
const { setOpened } = props;
const { config, setConfig } = useConfig();
const [isLoading, setLoading] = useState(false);
const form = useForm({
initialValues: {
type: props.type ?? 'Other',
name: props.name ?? '',
icon: props.icon ?? '',
url: props.url ?? '',
apiKey: props.apiKey ?? (undefined as unknown as string),
},
validate: {
apiKey: () => null,
// Validate icon with a regex
icon: (value: string) => {
if (!value.match(/^https?:\/\/.+\.(png|jpg|jpeg|gif)$/)) {
return 'Please enter a valid icon URL';
}
return null;
},
// Validate url with a regex http/https
url: (value: string) => {
if (!value.match(/^https?:\/\/.+\/$/)) {
return 'Please enter a valid URL (that ends with a /)';
}
return null;
},
},
});
return (
<>
<Center>
<Image height={120} width={120} src={form.values.icon} alt="Placeholder" withPlaceholder />
</Center>
<form
onSubmit={form.onSubmit(() => {
// If service already exists, update it.
if (config.services && config.services.find((s) => s.name === form.values.name)) {
setConfig({
...config,
services: config.services.map((s) => {
if (s.name === form.values.name) {
return {
...form.values,
};
}
return s;
}),
});
} else {
setConfig({
...config,
services: [...config.services, form.values],
});
}
setOpened(false);
form.reset();
})}
>
<Group direction="column" grow>
<TextInput
required
label="Service name"
placeholder="Plex"
value={form.values.name}
onChange={(event) => {
form.setFieldValue('name', event.currentTarget.value);
const match = MatchIcon(event.currentTarget.value, form);
if (match) {
form.setFieldValue('icon', match);
}
}}
error={form.errors.name && 'Invalid icon url'}
/>
<TextInput
required
label="Icon url"
placeholder="https://i.gifer.com/ANPC.gif"
{...form.getInputProps('icon')}
/>
<TextInput
required
label="Service url"
placeholder="http://localhost:8989"
{...form.getInputProps('url')}
/>
<Select
label="Select the type of service (used for API calls)"
defaultValue="Other"
placeholder="Pick one"
required
searchable
data={ServiceTypeList}
{...form.getInputProps('type')}
/>
<LoadingOverlay visible={isLoading} />
{(form.values.type === 'Sonarr' || form.values.type === 'Radarr') && (
<TextInput
required
label="API key"
placeholder="Your API key"
value={form.values.apiKey}
onChange={(event) => {
form.setFieldValue('apiKey', event.currentTarget.value);
}}
error={form.errors.apiKey && 'Invalid API key'}
/>
)}
</Group>
<Group grow position="center" mt="xl">
<Button type="submit">{props.message ?? 'Add service'}</Button>
</Group>
</form>
</>
);
}

View File

@@ -1,93 +0,0 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { Text, AspectRatio, SimpleGrid, Card, Image, Group, Space } from '@mantine/core';
import { useConfig } from '../../tools/state';
import { serviceItem } from '../../tools/types';
import AddItemShelfItem from './AddAppShelfItem';
import { AppShelfItemWrapper } from './AppShelfItemWrapper';
import AppShelfMenu from './AppShelfMenu';
const AppShelf = () => {
const { config } = useConfig();
return (
<SimpleGrid m="xl" cols={5} spacing="xl">
{config.services.map((service) => (
<AppShelfItem key={service.name} service={service} />
))}
<AddItemShelfItem />
</SimpleGrid>
);
};
export function AppShelfItem(props: any) {
const { service }: { service: serviceItem } = props;
const [hovering, setHovering] = useState(false);
return (
<motion.div
key={service.name}
onHoverStart={() => {
setHovering(true);
}}
onHoverEnd={() => {
setHovering(false);
}}
>
<AppShelfItemWrapper hovering={hovering}>
<Card.Section>
<Group position="apart" mx="lg">
<Space />
<Text
// TODO: #1 Remove this hack to get the text to be centered.
ml={15}
style={{
alignSelf: 'center',
alignContent: 'center',
alignItems: 'center',
justifyContent: 'center',
justifyItems: 'center',
}}
mt="sm"
weight={500}
>
{service.name}
</Text>
<motion.div
style={{
alignSelf: 'flex-end',
}}
animate={{
opacity: hovering ? 1 : 0,
}}
>
<AppShelfMenu service={service} />
</motion.div>
</Group>
</Card.Section>
<Card.Section>
<AspectRatio ratio={5 / 3} m="xl">
<motion.i
whileHover={{
cursor: 'pointer',
scale: 1.1,
}}
>
<Image
onClick={() => {
window.open(service.url);
}}
style={{
maxWidth: '50%',
marginBottom: 10,
}}
src={service.icon}
/>
</motion.i>
</AspectRatio>
</Card.Section>
</AppShelfItemWrapper>
</motion.div>
);
}
export default AppShelf;

View File

@@ -1,21 +0,0 @@
import { useMantineTheme, Card } from '@mantine/core';
export function AppShelfItemWrapper(props: any) {
const { children, hovering } = props;
const theme = useMantineTheme();
return (
<Card
style={{
boxShadow: hovering ? '0px 0px 3px rgba(0, 0, 0, 0.5)' : '0px 0px 1px rgba(0, 0, 0, 0.5)',
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
//TODO: #3 Fix this temporary fix and make the width and height dynamic / responsive
width: 200,
height: 180,
}}
radius="md"
>
{children}
</Card>
);
}

View File

@@ -1,18 +0,0 @@
import { Button } from '@mantine/core';
import fileDownload from 'js-file-download';
import { Download } from 'tabler-icons-react';
import { useConfig } from '../../tools/state';
export default function SaveConfigComponent(props: any) {
const { config } = useConfig();
function onClick(e: any) {
if (config) {
fileDownload(JSON.stringify(config, null, '\t'), `${config.name}.json`);
}
}
return (
<Button leftIcon={<Download />} variant="outline" onClick={onClick}>
Download your config
</Button>
);
}

View File

@@ -1,16 +0,0 @@
import { Select } from '@mantine/core';
import { useState } from 'react';
export default function SelectConfig(props: any) {
const [value, setValue] = useState<string | null>('');
return (
<Select
value={value}
onChange={setValue}
data={[
{ value: 'default', label: 'Default' },
{ value: 'yourmom', label: 'Your mom' },
]}
/>
);
}

View File

@@ -1,90 +0,0 @@
import { TextInput, Text, Popover, Box } from '@mantine/core';
import { useForm } from '@mantine/hooks';
import { useState } from 'react';
import { Search, BrandYoutube, Download } from 'tabler-icons-react';
import { useConfig } from '../../tools/state';
export default function SearchBar(props: any) {
const { config, setConfig } = useConfig();
const [opened, setOpened] = useState(false);
const [icon, setIcon] = useState(<Search />);
const querryUrl = config.settings.searchUrl || 'https://www.google.com/search?q=';
const form = useForm({
initialValues: {
querry: '',
},
});
if (config.settings.searchBar === false) {
return null;
}
return (
<Box
style={{
width: '100%',
}}
>
<form
onChange={() => {
// If querry contains !yt or !t add "Searching on YouTube" or "Searching torrent"
const querry = form.values.querry.trim();
const isYoutube = querry.startsWith('!yt');
const isTorrent = querry.startsWith('!t');
if (isYoutube) {
setIcon(<BrandYoutube size={22} />);
} else if (isTorrent) {
setIcon(<Download size={22} />);
} else {
setIcon(<Search size={22} />);
}
}}
onSubmit={form.onSubmit((values) => {
// Find if querry is prefixed by !yt or !t
const querry = values.querry.trim();
const isYoutube = querry.startsWith('!yt');
const isTorrent = querry.startsWith('!t');
if (isYoutube) {
window.open(`https://www.youtube.com/results?search_query=${querry.substring(3)}`);
} else if (isTorrent) {
window.open(`https://bitsearch.to/search?q=${querry.substring(3)}`);
} else {
window.open(`${querryUrl}${values.querry}`);
}
})}
>
<Popover
opened={opened}
style={{
width: '100%',
}}
position="bottom"
placement="start"
withArrow
trapFocus={false}
transition="pop-top-left"
onFocusCapture={() => setOpened(true)}
onBlurCapture={() => setOpened(false)}
target={
<TextInput
variant="filled"
color="blue"
icon={icon}
radius="md"
size="md"
placeholder="Search the web"
{...props}
{...form.getInputProps('querry')}
/>
}
>
<Text>
tip: You can prefix your querry with <b>!yt</b> or <b>!t</b> to research on youtube or
for a torrent
</Text>
</Popover>
</form>
</Box>
);
}

View File

@@ -1,41 +0,0 @@
import { Group, Switch } from '@mantine/core';
import * as Modules from '../modules';
import { useConfig } from '../../tools/state';
export default function ModuleEnabler(props: any) {
const { config, setConfig } = useConfig();
const modules = Object.values(Modules).map((module) => module);
const enabledModules = config.settings.enabledModules ?? [];
modules.filter((module) => enabledModules.includes(module.title));
return (
<Group direction="column">
{modules.map((module) => (
<Switch
key={module.title}
size="md"
checked={enabledModules.includes(module.title)}
label={`Enable ${module.title} module`}
onChange={(e) => {
if (e.currentTarget.checked) {
setConfig({
...config,
settings: {
...config.settings,
enabledModules: [...enabledModules, module.title],
},
});
} else {
setConfig({
...config,
settings: {
...config.settings,
enabledModules: enabledModules.filter((m) => m !== module.title),
},
});
}
}}
/>
))}
</Group>
);
}

View File

@@ -1,144 +0,0 @@
import {
ActionIcon,
Group,
Modal,
Switch,
Title,
Text,
Tooltip,
SegmentedControl,
Indicator,
Alert,
} from '@mantine/core';
import { useColorScheme } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { AlertCircle, Settings as SettingsIcon } from 'tabler-icons-react';
import { CURRENT_VERSION, REPO_URL } from '../../data/constants';
import { useConfig } from '../../tools/state';
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
import ConfigChanger from '../Config/ConfigChanger';
import SaveConfigComponent from '../Config/SaveConfig';
import ModuleEnabler from './ModuleEnabler';
function SettingsMenu(props: any) {
const { config, setConfig } = useConfig();
const colorScheme = useColorScheme();
const { current, latest } = props;
const matches = [
{ label: 'Google', value: 'https://google.com/search?q=' },
{ label: 'DuckDuckGo', value: 'https://duckduckgo.com/?q=' },
{ label: 'Bing', value: 'https://bing.com/search?q=' },
];
return (
<Group direction="column" grow>
<Alert
icon={<AlertCircle size={16} />}
title="Update available"
radius="lg"
hidden={current === latest}
>
Version {latest} is available. Current : {current}
</Alert>
<Group>
<SegmentedControl
title="Search engine"
value={
// Match config.settings.searchUrl with a key in the matches array
matches.find((match) => match.value === config.settings.searchUrl)?.value ?? 'Google'
}
onChange={
// Set config.settings.searchUrl to the value of the selected item
(e) =>
setConfig({
...config,
settings: {
...config.settings,
searchUrl: e,
},
})
}
data={matches}
/>
<Text>Search engine</Text>
</Group>
<Group direction="column">
<Switch
size="md"
onChange={(e) =>
setConfig({
...config,
settings: {
...config.settings,
searchBar: e.currentTarget.checked,
},
})
}
checked={config.settings.searchBar}
label="Enable search bar"
/>
</Group>
<ModuleEnabler />
<ColorSchemeSwitch />
<ConfigChanger />
<SaveConfigComponent />
<Text
style={{
alignSelf: 'center',
fontSize: '0.75rem',
textAlign: 'center',
color: '#a0aec0',
}}
>
tip: You can upload your config file by dragging and dropping it onto the page
</Text>
</Group>
);
}
export function SettingsMenuButton(props: any) {
const [update, setUpdate] = useState(false);
const [opened, setOpened] = useState(false);
const [latestVersion, setLatestVersion] = useState(CURRENT_VERSION);
useEffect(() => {
// Fetch Data here when component first mounted
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
res.json().then((data) => {
setLatestVersion(data.tag_name);
if (data.tag_name !== CURRENT_VERSION) {
setUpdate(true);
}
});
});
}, []);
return (
<>
<Modal
size="md"
title={<Title order={3}>Settings</Title>}
opened={props.opened || opened}
onClose={() => setOpened(false)}
>
<SettingsMenu current={CURRENT_VERSION} latest={latestVersion} />
</Modal>
<ActionIcon
variant="default"
radius="xl"
size="xl"
color="blue"
style={props.style}
onClick={() => setOpened(true)}
>
<Tooltip label="Settings">
<Indicator
size={12}
disabled={CURRENT_VERSION === latestVersion}
offset={-3}
position="top-end"
>
<SettingsIcon />
</Indicator>
</Tooltip>
</ActionIcon>
</>
);
}

View File

@@ -1,7 +0,0 @@
import { Welcome } from './Welcome';
export default {
title: 'Welcome',
};
export const Usage = () => <Welcome />;

View File

@@ -1,14 +0,0 @@
import { createStyles } from '@mantine/core';
export default createStyles((theme) => ({
title: {
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
fontSize: 100,
fontWeight: 900,
letterSpacing: -2,
[theme.fn.smallerThan('md')]: {
fontSize: 50,
},
},
}));

View File

@@ -1,12 +0,0 @@
import { render, screen } from '@testing-library/react';
import { Welcome } from './Welcome';
describe('Welcome component', () => {
it('has correct Next.js theming section link', () => {
render(<Welcome />);
expect(screen.getByText('this guide')).toHaveAttribute(
'href',
'https://mantine.dev/theming/next/'
);
});
});

View File

@@ -1,25 +0,0 @@
import { Title, Text, Anchor } from '@mantine/core';
import useStyles from './Welcome.styles';
export function Welcome() {
const { classes } = useStyles();
return (
<>
<Title className={classes.title} align="center" mt={100}>
Welcome to{' '}
<Text inherit variant="gradient" component="span">
Mantine
</Text>
</Title>
<Text color="dimmed" align="center" size="lg" sx={{ maxWidth: 580 }} mx="auto" mt="xl">
This starter Next.js project includes a minimal setup for server side rendering, if you want
to learn more on Mantine + Next.js integration follow{' '}
<Anchor href="https://mantine.dev/theming/next/" size="lg">
this guide
</Anchor>
. To get started edit index.tsx file.
</Text>
</>
);
}

View File

@@ -1,18 +0,0 @@
import { Aside as MantineAside } from '@mantine/core';
import { CalendarModule } from '../modules/calendar/CalendarModule';
import ModuleWrapper from '../modules/moduleWrapper';
export default function Aside() {
return (
<MantineAside
height="100%"
hiddenBreakpoint="md"
hidden
width={{
base: 'auto',
}}
>
<ModuleWrapper module={CalendarModule} />
</MantineAside>
);
}

View File

@@ -1,81 +0,0 @@
import React from 'react';
import { createStyles, Anchor, Text, Group, ActionIcon } from '@mantine/core';
import { BrandGithub } from 'tabler-icons-react';
const useStyles = createStyles((theme) => ({
footer: {
borderTop: `1px solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]
}`,
},
inner: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: `${theme.spacing.md}px ${theme.spacing.md}px`,
[theme.fn.smallerThan('sm')]: {
flexDirection: 'column',
},
},
links: {
[theme.fn.smallerThan('sm')]: {
marginTop: theme.spacing.lg,
marginBottom: theme.spacing.sm,
},
},
}));
interface FooterCenteredProps {
links: { link: string; label: string }[];
}
export function Footer({ links }: FooterCenteredProps) {
const { classes } = useStyles();
const items = links.map((link) => (
<Anchor<'a'>
color="dimmed"
key={link.label}
href={link.link}
sx={{ lineHeight: 1 }}
onClick={(event) => event.preventDefault()}
size="sm"
>
{link.label}
</Anchor>
));
return (
<Group
sx={{
position: 'fixed',
bottom: 0,
right: 15,
}}
direction="row"
align="center"
mb={15}
>
<Group className={classes.links}>{items}</Group>
<Group spacing="xs" position="right" noWrap>
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
<BrandGithub size={18} />
</ActionIcon>
</Group>
<Text
style={{
fontSize: '0.75rem',
textAlign: 'center',
color: '#a0aec0',
}}
>
Made with by @
<Anchor href="https://github.com/ajnart" style={{ color: 'inherit', fontStyle: 'inherit' }}>
ajnart
</Anchor>
</Text>
</Group>
);
}

View File

@@ -1,155 +0,0 @@
import React, { useState } from 'react';
import {
createStyles,
Header as Head,
Container,
Group,
Burger,
Drawer,
Center,
} from '@mantine/core';
import { useBooleanToggle } from '@mantine/hooks';
import { NextLink } from '@mantine/next';
import { Logo } from './Logo';
import { SettingsMenuButton } from '../Settings/SettingsMenu';
import CalendarComponent from '../modules/calendar/CalendarModule';
const HEADER_HEIGHT = 60;
const useStyles = createStyles((theme) => ({
root: {
position: 'relative',
zIndex: 1,
},
dropdown: {
position: 'absolute',
top: HEADER_HEIGHT,
left: 0,
right: 0,
zIndex: 0,
borderTopRightRadius: 0,
borderTopLeftRadius: 0,
borderTopWidth: 0,
overflow: 'hidden',
[theme.fn.largerThan('md')]: {
display: 'none',
},
},
header: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
height: '100%',
},
links: {
[theme.fn.smallerThan('md')]: {
display: 'none',
},
},
burger: {
[theme.fn.largerThan('md')]: {
display: 'none',
},
},
link: {
display: 'block',
lineHeight: 1,
padding: '8px 12px',
borderRadius: theme.radius.sm,
textDecoration: 'none',
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.colors.gray[7],
fontSize: theme.fontSizes.sm,
fontWeight: 500,
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
},
[theme.fn.smallerThan('sm')]: {
borderRadius: 0,
padding: theme.spacing.md,
},
},
linkActive: {
'&, &:hover': {
backgroundColor:
theme.colorScheme === 'dark'
? theme.fn.rgba(theme.colors[theme.primaryColor][9], 0.25)
: theme.colors[theme.primaryColor][0],
color: theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 3 : 7],
},
},
}));
interface HeaderResponsiveProps {
links: { link: string; label: string }[];
}
export function Header({ links }: HeaderResponsiveProps) {
const [opened, toggleOpened] = useBooleanToggle(false);
const [active, setActive] = useState('/');
const { classes, cx } = useStyles();
const items = (
<>
{links.map((link) => (
<NextLink
key={link.label}
href={link.link}
className={cx(classes.link, { [classes.linkActive]: active === link.link })}
onClick={(event) => {
setActive(link.link);
toggleOpened(false);
}}
>
{link.label}
</NextLink>
))}
</>
);
return (
<Head height={HEADER_HEIGHT} mb={10} className={classes.root}>
<Container className={classes.header}>
<Group>
<NextLink style={{ textDecoration: 'none' }} href="/">
<Logo style={{ fontSize: 22 }} />
</NextLink>
</Group>
<Group spacing={5} className={classes.links}>
{items}
</Group>
<Group>
<SettingsMenuButton />
<Burger
opened={opened}
onClick={() => toggleOpened()}
className={classes.burger}
size="sm"
/>
</Group>
<Drawer
opened={opened}
overlayOpacity={0.55}
overlayBlur={3}
onClose={() => toggleOpened()}
position="right"
>
{opened ?? (
<Center>
<CalendarComponent />
</Center>
)}
</Drawer>
</Container>
</Head>
);
}

View File

@@ -1,36 +0,0 @@
import { AppShell, Center, createStyles } from '@mantine/core';
import { Header } from './Header';
import { Footer } from './Footer';
import Aside from './Aside';
import Navbar from './Navbar';
const useStyles = createStyles((theme) => ({
main: {
[theme.fn.largerThan('md')]: {
width: 1200,
},
},
}));
export default function Layout({ children, style }: any) {
const { classes, cx } = useStyles();
return (
<AppShell
navbar={<Navbar />}
aside={<Aside />}
header={<Header links={[]} />}
footer={<Footer links={[]} />}
>
<Center>
<main
className={cx(classes.main)}
style={{
...style,
}}
>
{children}
</main>
</Center>
</AppShell>
);
}

View File

@@ -1,15 +0,0 @@
import { Text } from '@mantine/core';
import * as React from 'react';
export function Logo({ style }: any) {
return (
<Text
sx={style}
weight="bold"
variant="gradient"
gradient={{ from: 'red', to: 'orange', deg: 145 }}
>
Homarr
</Text>
);
}

View File

@@ -1,18 +0,0 @@
import { Navbar as MantineNavbar } from '@mantine/core';
import { DateModule } from '../modules/date/DateModule';
import ModuleWrapper from '../modules/moduleWrapper';
export default function Navbar() {
return (
<MantineNavbar
height="100%"
hiddenBreakpoint="md"
hidden
width={{
base: 'auto',
}}
>
<ModuleWrapper module={DateModule} />
</MantineNavbar>
);
}

View File

@@ -1,153 +0,0 @@
/* eslint-disable react/no-children-prop */
import { Popover, Box, ScrollArea, Divider, Indicator } from '@mantine/core';
import React, { useEffect, useState } from 'react';
import { Calendar } from '@mantine/dates';
import { CalendarIcon } from '@modulz/radix-icons';
import { showNotification } from '@mantine/notifications';
import { Check } from 'tabler-icons-react';
import { RadarrMediaDisplay, SonarrMediaDisplay } from './MediaDisplay';
import { useConfig } from '../../../tools/state';
import { IModule } from '../modules';
export const CalendarModule: IModule = {
title: 'Calendar',
description:
'A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.',
icon: CalendarIcon,
component: CalendarComponent,
};
export default function CalendarComponent(props: any) {
const { config } = useConfig();
const [sonarrMedias, setSonarrMedias] = useState([] as any);
const [radarrMedias, setRadarrMedias] = useState([] as any);
useEffect(() => {
// Filter only sonarr and radarr services
const filtered = config.services.filter(
(service) => service.type === 'Sonarr' || service.type === 'Radarr'
);
// Get the url and apiKey for all Sonarr and Radarr services
const sonarrService = filtered.filter((service) => service.type === 'Sonarr').at(0);
const radarrService = filtered.filter((service) => service.type === 'Radarr').at(0);
const nextMonth = new Date(new Date().setMonth(new Date().getMonth() + 2)).toISOString();
if (sonarrService && sonarrService.apiKey) {
fetch(
`${sonarrService?.url}api/calendar?apikey=${sonarrService?.apiKey}&end=${nextMonth}`
).then((response) => {
response.ok &&
response.json().then((data) => {
setSonarrMedias(data);
showNotification({
title: 'Sonarr',
icon: <Check />,
color: 'green',
autoClose: 1500,
radius: 'md',
message: `Loaded ${data.length} releases`,
});
});
});
}
if (radarrService && radarrService.apiKey) {
fetch(
`${radarrService?.url}api/v3/calendar?apikey=${radarrService?.apiKey}&end=${nextMonth}`
).then((response) => {
response.ok &&
response.json().then((data) => {
setRadarrMedias(data);
showNotification({
title: 'Radarr',
icon: <Check />,
color: 'green',
autoClose: 1500,
radius: 'md',
message: `Loaded ${data.length} releases`,
});
});
});
}
}, [config.services]);
if (sonarrMedias === undefined && radarrMedias === undefined) {
return <Calendar />;
}
return (
<Calendar
onChange={(day: any) => {}}
renderDay={(renderdate) => (
<DayComponent
renderdate={renderdate}
sonarrmedias={sonarrMedias}
radarrmedias={radarrMedias}
/>
)}
/>
);
}
function DayComponent(props: any) {
const {
renderdate,
sonarrmedias,
radarrmedias,
}: { renderdate: Date; sonarrmedias: []; radarrmedias: [] } = props;
const [opened, setOpened] = useState(false);
const day = renderdate.getDate();
// Itterate over the medias and filter the ones that are on the same day
const sonarrFiltered = sonarrmedias.filter((media: any) => {
const date = new Date(media.airDate);
// Return true if the date is renerdate without counting hours and minutes
return date.getDate() === day && date.getMonth() === renderdate.getMonth();
});
const radarrFiltered = radarrmedias.filter((media: any) => {
const date = new Date(media.inCinemas);
// Return true if the date is renerdate without counting hours and minutes
return date.getDate() === day && date.getMonth() === renderdate.getMonth();
});
if (sonarrFiltered.length === 0 && radarrFiltered.length === 0) {
return <div>{day}</div>;
}
return (
<Box
onClick={() => {
setOpened(true);
}}
>
{radarrFiltered.length > 0 && <Indicator size={7} color="yellow" children={null} />}
{sonarrFiltered.length > 0 && <Indicator size={7} offset={8} color="blue" children={null} />}
<Popover
position="left"
radius="lg"
shadow="xl"
transition="pop"
width={700}
onClose={() => setOpened(false)}
opened={opened}
// TODO: Fix this !! WTF ?
target={` ${day}`}
>
<ScrollArea style={{ height: 400 }}>
{sonarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<SonarrMediaDisplay media={media} />
{index < sonarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
{radarrFiltered.length > 0 && sonarrFiltered.length > 0 && (
<Divider variant="dashed" my="xl" />
)}
{radarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<RadarrMediaDisplay media={media} />
{index < radarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
</ScrollArea>
</Popover>
</Box>
);
}

View File

@@ -1,100 +0,0 @@
import { Stack, Image, Group, Title, Badge, Text, ActionIcon, Anchor } from '@mantine/core';
import { Link } from 'tabler-icons-react';
export interface IMedia {
overview: string;
imdbId: any;
title: string;
poster: string;
genres: string[];
seasonNumber?: number;
episodeNumber?: number;
}
function MediaDisplay(props: { media: IMedia }) {
const { media }: { media: IMedia } = props;
return (
<Group noWrap align="self-start" mr={15}>
<Image
radius="md"
fit="cover"
src={media.poster}
alt={media.title}
width={300}
height={400}
/>
<Stack
justify="space-between"
sx={(theme) => ({
height: 400,
})}
>
<Group direction="column">
<Group>
<Title order={3}>{media.title}</Title>
<Anchor href={`https://www.imdb.com/title/${media.imdbId}`} target="_blank">
<ActionIcon>
<Link />
</ActionIcon>
</Anchor>
</Group>
{media.episodeNumber && media.seasonNumber && (
<Text
style={{
textAlign: 'center',
color: '#a0aec0',
}}
>
Season {media.seasonNumber} episode {media.episodeNumber}
</Text>
)}
<Text align="justify">{media.overview}</Text>
</Group>
{/*Add the genres at the bottom of the poster*/}
<Group>
{media.genres.map((genre: string, i: number) => (
<Badge key={i}>{genre}</Badge>
))}
</Group>
</Stack>
</Group>
);
}
export function RadarrMediaDisplay(props: any) {
const { media }: { media: any } = props;
// Find a poster CoverType
const poster = media.images.find((image: any) => image.coverType === 'poster');
// Return a movie poster containting the title and the description
return (
<MediaDisplay
media={{
imdbId: media.imdbId,
title: media.title,
overview: media.overview,
poster: poster.url,
genres: media.genres,
}}
/>
);
}
export function SonarrMediaDisplay(props: any) {
const { media }: { media: any } = props;
// Find a poster CoverType
const poster = media.series.images.find((image: any) => image.coverType === 'poster');
// Return a movie poster containting the title and the description
return (
<MediaDisplay
media={{
imdbId: media.series.imdbId,
title: media.series.title,
overview: media.series.overview,
poster: poster.url,
genres: media.series.genres,
seasonNumber: media.seasonNumber,
episodeNumber: media.episodeNumber,
}}
/>
);
}

View File

@@ -1,41 +0,0 @@
import { Group, Text, Title } from '@mantine/core';
import dayjs from 'dayjs';
import { useEffect, useState } from 'react';
import { Clock } from 'tabler-icons-react';
import { IModule } from '../modules';
export const DateModule: IModule = {
title: 'Date',
description: 'Show the current time and date in a card',
icon: Clock,
component: DateComponent,
};
export default function DateComponent(props: any) {
const [date, setDate] = useState(new Date());
const hours = date.getHours();
const minutes = date.getMinutes();
// Change date on minute change
// Note: Using 10 000ms instead of 1000ms to chill a little :)
useEffect(() => {
setInterval(() => {
setDate(new Date());
}, 10000);
}, []);
return (
<Group p="sm" direction="column">
<Title>
{hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes}
</Title>
<Text size="xl">
{
// Use dayjs to format the date
// https://day.js.org/en/getting-started/installation/
dayjs(date).format('dddd, MMMM D')
}
</Text>
</Group>
);
}

View File

@@ -1,2 +0,0 @@
export * from './date';
export * from './calendar';

View File

@@ -1,29 +0,0 @@
import { Card, useMantineTheme } from '@mantine/core';
import { useConfig } from '../../tools/state';
import { IModule } from './modules';
export default function ModuleWrapper(props: any) {
const { module }: { module: IModule } = props;
const { config } = useConfig();
const enabledModules = config.settings.enabledModules ?? [];
// Remove 'Module' from enabled modules titles
const isShown = enabledModules.includes(module.title);
const theme = useMantineTheme();
if (!isShown) {
return null;
}
return (
<Card
hidden={!isShown}
mx="sm"
radius="lg"
shadow="sm"
style={{
// Make background color of the card depend on the theme
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : 'white',
}}
>
<module.component />
</Card>
);
}

3
crowdin.yml Normal file
View File

@@ -0,0 +1,3 @@
files:
- source: /public/locales/en/*.json
translation: /public/locales/%two_letters_code%/%original_file_name%.json

View File

@@ -1,12 +0,0 @@
{
"name": "config",
"services": [],
"settings": {
"searchBar": true,
"searchUrl": "https://duckduckgo.com/?q=",
"enabledModules": [
"Date",
"Calendar"
]
}
}

View File

@@ -1,16 +0,0 @@
{
"name": "config_new",
"services": [
{
"type": "Other",
"name": "example",
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
],
"settings": {
"searchBar": true,
"searchUrl": "https://google.com/search?q=",
"enabledModules": []
}
}

View File

@@ -2,15 +2,22 @@
"name": "default",
"services": [
{
"type": "Other",
"name": "example",
"id": "09c45847-8afc-4c1a-9697-f03192de948a",
"type": "Other",
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
],
"settings": {
"searchBar": true,
"searchUrl": "https://bing.com/search?q=",
"enabledModules": []
"searchUrl": "https://google.com/search?q="
},
"modules": {
"Search Bar": {
"enabled": true
},
"Date": {
"enabled": false
}
}
}

View File

@@ -1,2 +1,2 @@
export const REPO_URL = 'ajnart/homarr';
export const CURRENT_VERSION = 'v0.2.0';
export const CURRENT_VERSION = 'v0.7.1';

View File

@@ -1,16 +0,0 @@
const nextJest = require('next/jest');
const createJestConfig = nextJest({
dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/pages/(.*)$': '<rootDir>/pages/$1',
},
testEnvironment: 'jest-environment-jsdom',
};
module.exports = createJestConfig(customJestConfig);

View File

@@ -1 +0,0 @@
import '@testing-library/jest-dom/extend-expect';

View File

@@ -1,13 +1,16 @@
const { env } = require('process');
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
reactStrictMode: true,
reactStrictMode: false,
eslint: {
ignoreDuringBuilds: true,
},
experimental: {
outputStandalone: true,
},
basePath: env.BASE_URL,
});

View File

@@ -1,87 +1,88 @@
{
"name": "homarr",
"version": "0.1.6",
"private": "false",
"description": "Homarr - A homepage for your server.",
"version": "0.7.1",
"description": "Homarr - A homepage for your server.",
"repository": {
"type": "git",
"url": "https://github.com/ajnart/homarr"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"analyze": "ANALYZE=true next build",
"start": "next start --port 7575",
"typecheck": "tsc --noEmit",
"export": "next build && next export",
"lint": "next lint",
"jest": "jest",
"jest:watch": "jest --watch",
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
"test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest",
"storybook": "start-storybook -p 7001",
"storybook:build": "build-storybook",
"ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write"
},
"dependencies": {
"@mantine/core": "^4.2.4",
"@mantine/dates": "^4.2.4",
"@mantine/dropzone": "^4.2.4",
"@mantine/form": "^4.2.4",
"@mantine/hooks": "^4.2.4",
"@mantine/modals": "^4.2.4",
"@mantine/next": "^4.2.4",
"@mantine/notifications": "^4.2.4",
"@mantine/prism": "^4.2.4",
"@mantine/rte": "^4.2.4",
"@mantine/spotlight": "^4.2.4",
"@modulz/radix-icons": "^4.0.0",
"axios": "^0.27.2",
"cookies-next": "^2.0.4",
"dayjs": "^1.11.2",
"framer-motion": "^6.3.1",
"js-file-download": "^0.4.12",
"next": "12.1.5-canary.4",
"prism-react-renderer": "^1.3.1",
"react": "18.0.0",
"react-dom": "18.0.0",
"tabler-icons-react": "^1.46.0"
},
"devDependencies": {
"@babel/core": "^7.17.8",
"@next/bundle-analyzer": "^12.1.4",
"@next/eslint-plugin-next": "^12.1.4",
"@storybook/addon-essentials": "^6.4.22",
"@storybook/addon-links": "^6.4.22",
"@storybook/react": "^6.4.22",
"@testing-library/dom": "^8.12.0",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^14.0.4",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.23",
"@types/react": "17.0.43",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"babel-loader": "^8.2.4",
"eslint": "^8.11.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-airbnb-typescript": "^16.1.4",
"eslint-config-mantine": "1.1.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.3",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-storybook": "^0.5.11",
"eslint-plugin-testing-library": "^5.2.0",
"eslint-plugin-unused-imports": "^2.0.0",
"jest": "^27.5.1",
"prettier": "^2.6.2",
"storybook-addon-turbo-build": "^1.1.0",
"storybook-dark-mode": "^1.0.9",
"ts-jest": "^27.1.4",
"typescript": "4.6.3"
}
"scripts": {
"dev": "next dev",
"build": "next build",
"analyze": "ANALYZE=true next build",
"start": "next start",
"typecheck": "tsc --noEmit",
"export": "next build && next export",
"lint": "next lint",
"jest": "jest",
"jest:watch": "jest --watch",
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
"test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest",
"storybook": "start-storybook -p 7001",
"storybook:build": "build-storybook",
"ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write"
},
"dependencies": {
"@ctrl/deluge": "^4.1.0",
"@ctrl/qbittorrent": "^4.0.0",
"@ctrl/shared-torrent": "^4.1.0",
"@ctrl/transmission": "^4.1.1",
"@dnd-kit/core": "^6.0.1",
"@dnd-kit/sortable": "^7.0.0",
"@dnd-kit/utilities": "^3.2.0",
"@mantine/core": "^4.2.8",
"@mantine/dates": "^4.2.8",
"@mantine/dropzone": "^4.2.8",
"@mantine/form": "^4.2.8",
"@mantine/hooks": "^4.2.8",
"@mantine/next": "^4.2.8",
"@mantine/notifications": "^4.2.8",
"@mantine/prism": "^4.2.8",
"@nivo/core": "^0.79.0",
"@nivo/line": "^0.79.1",
"@tabler/icons": "^1.68.0",
"axios": "^0.27.2",
"cookies-next": "^2.0.4",
"dayjs": "^1.11.3",
"framer-motion": "^6.3.1",
"js-file-download": "^0.4.12",
"next": "12.1.6",
"prism-react-renderer": "^1.3.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"systeminformation": "^5.11.16",
"uuid": "^8.3.2"
},
"devDependencies": {
"@babel/core": "^7.17.8",
"@next/bundle-analyzer": "^12.1.4",
"@next/eslint-plugin-next": "^12.1.4",
"@storybook/react": "^6.5.4",
"@types/node": "^17.0.23",
"@types/react": "17.0.43",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"eslint": "^8.11.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-mantine": "1.1.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.3",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-storybook": "^0.5.11",
"eslint-plugin-testing-library": "^5.2.0",
"eslint-plugin-unused-imports": "^2.0.0",
"jest": "^28.1.0",
"prettier": "^2.6.2",
"require-from-string": "^2.0.2",
"typescript": "4.6.4"
},
"resolutions": {
"@types/react": "17.0.30"
},
"packageManager": "yarn@3.2.1"
}

View File

@@ -1,54 +0,0 @@
import { GetServerSidePropsContext } from 'next';
import { useState } from 'react';
import { AppProps } from 'next/app';
import { getCookie, setCookies } from 'cookies-next';
import Head from 'next/head';
import { MantineProvider, ColorScheme, ColorSchemeProvider } from '@mantine/core';
import { NotificationsProvider } from '@mantine/notifications';
import Layout from '../components/layout/Layout';
import { ConfigProvider } from '../tools/state';
import { theme } from '../tools/theme';
export default function App(props: AppProps & { colorScheme: ColorScheme }) {
const { Component, pageProps } = props;
const [colorScheme, setColorScheme] = useState<ColorScheme>(props.colorScheme);
const toggleColorScheme = (value?: ColorScheme) => {
const nextColorScheme = value || (colorScheme === 'dark' ? 'light' : 'dark');
setColorScheme(nextColorScheme);
setCookies('color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 });
};
return (
<>
<Head>
<title>Homarr - A homepage for your server!</title>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<link rel="shortcut icon" href="/favicon.svg" />
</Head>
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
<MantineProvider
theme={{
...theme,
colorScheme,
}}
withGlobalStyles
withNormalizeCSS
>
<NotificationsProvider limit={2} position="top-right">
<ConfigProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</ConfigProvider>
</NotificationsProvider>
</MantineProvider>
</ColorSchemeProvider>
</>
);
}
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
colorScheme: getCookie('color-scheme', ctx) || 'light',
});

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 438 B

After

Width:  |  Height:  |  Size: 14 KiB

0
public/icons/.gitkeep Normal file
View File

BIN
public/imgs/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/imgs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

247
public/imgs/logo.svg Normal file
View File

@@ -0,0 +1,247 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1000.000000pt" height="1000.000000pt" viewBox="0 0 1000.000000 1000.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,1000.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M6470 9752 c-179 -11 -423 -57 -605 -113 -94 -29 -116 -37 -212 -73
-381 -144 -693 -333 -1030 -621 -6 -5 -63 -61 -126 -123 l-114 -113 -179 88
c-223 110 -305 143 -386 158 -12 3 -34 8 -48 11 -14 3 -100 8 -191 10 -154 4
-171 3 -253 -21 -159 -46 -241 -93 -355 -201 -55 -53 -101 -100 -101 -104 0
-10 76 18 110 41 39 26 206 108 253 124 61 20 202 42 294 44 56 1 105 -6 178
-24 55 -15 110 -28 122 -31 76 -15 317 -112 410 -165 l52 -30 -78 -97 c-152
-190 -244 -324 -230 -333 8 -5 17 -9 20 -9 4 0 12 -13 19 -30 18 -44 5 -139
-39 -269 -20 -62 -34 -114 -31 -116 4 -2 24 14 45 35 22 22 43 40 49 40 5 0
23 -10 40 -23 56 -42 210 -135 276 -167 36 -17 72 -35 80 -39 37 -20 139 -43
187 -43 35 0 64 6 88 21 l36 21 57 -38 c31 -21 76 -46 100 -55 52 -21 149 -32
188 -23 l28 7 -29 23 -29 24 54 34 c78 49 180 84 191 67 37 -57 119 -207 165
-304 75 -157 122 -306 134 -421 16 -157 43 -193 152 -199 56 -4 62 -2 90 26
29 29 30 33 25 89 -13 122 -191 536 -326 759 -53 87 -58 100 -41 102 6 1 13 2
18 3 4 1 39 5 77 10 104 11 156 17 195 21 19 3 62 7 95 10 112 11 246 45 470
120 370 124 716 314 905 497 278 269 418 532 380 714 l-12 58 -35 -43 c-48
-57 -116 -90 -216 -102 -117 -15 -165 -29 -198 -56 -23 -19 -32 -22 -48 -14
-51 27 -95 7 -160 -75 -30 -37 -60 -64 -73 -66 -13 -2 -50 2 -83 10 -79 17
-112 10 -189 -43 -107 -73 -120 -78 -196 -70 -84 8 -97 2 -176 -82 -56 -59
-63 -63 -94 -58 -18 3 -37 8 -41 11 -4 2 -25 7 -47 10 -37 5 -43 2 -111 -60
l-71 -66 -65 0 c-90 0 -160 -28 -221 -90 -47 -47 -52 -50 -103 -50 -49 0 -57
-3 -94 -41 -48 -47 -65 -57 -92 -53 -11 1 -39 6 -62 9 -29 4 -43 11 -43 21 0
30 20 52 53 58 23 4 38 15 45 31 16 35 49 53 112 59 66 7 99 29 169 113 28 34
60 67 72 73 11 6 55 14 97 16 84 5 91 9 133 71 20 29 40 45 65 52 35 10 47 9
141 -10 61 -12 82 -4 163 65 57 49 103 62 173 52 41 -6 43 -5 85 44 68 79 92
86 268 80 84 -3 113 9 175 75 95 100 156 119 227 73 44 -29 61 -28 110 6 57
39 107 56 141 50 26 -6 38 1 92 49 72 66 120 79 184 53 21 -9 39 -15 40 -14 1
2 9 14 18 28 51 75 -5 176 -155 283 -146 104 -224 134 -478 182 -61 11 -221
34 -280 40 -69 8 -327 12 -400 7z"/>
<path d="M8256 8794 c-3 -8 -3 -46 0 -82 4 -47 1 -86 -10 -128 -9 -34 -16 -93
-16 -131 0 -44 -7 -92 -20 -128 -23 -66 -25 -108 -9 -186 9 -47 8 -60 -10
-102 -26 -60 -26 -101 -1 -196 20 -74 20 -75 0 -145 -13 -48 -19 -102 -19
-166 1 -135 -25 -322 -56 -403 -28 -75 -31 -112 -14 -185 13 -54 8 -78 -42
-189 -23 -52 -23 -53 -5 -108 l17 -55 -35 -79 c-33 -71 -36 -87 -36 -168 0
-122 -47 -273 -85 -273 -21 0 -19 21 14 138 17 57 32 131 36 165 3 34 17 104
31 155 23 81 25 99 15 137 -17 67 -14 90 21 158 34 67 36 86 57 467 6 95 14
160 25 190 19 49 15 93 -10 121 -15 16 -14 23 10 83 31 78 32 109 6 176 -27
70 -25 89 15 146 40 56 46 111 19 189 -23 69 -21 113 9 188 26 62 27 74 21
156 -6 80 -5 93 15 124 28 45 27 54 -10 73 -92 47 -184 -42 -338 -326 -161
-298 -197 -386 -285 -705 -103 -375 -123 -584 -96 -1025 12 -204 25 -312 52
-458 11 -62 18 -116 14 -119 -3 -3 -79 10 -168 30 -175 39 -357 74 -429 83
-24 3 -51 7 -58 10 -24 8 -159 18 -241 19 l-75 0 -3 -81 c-4 -127 -9 -124 233
-139 55 -3 111 -7 125 -10 14 -2 48 -6 77 -9 28 -3 77 -9 110 -15 32 -6 76
-13 98 -16 22 -4 56 -10 75 -15 19 -5 51 -11 70 -14 47 -8 85 -15 148 -30 l32
-8 0 -64 c0 -77 13 -110 75 -195 66 -90 81 -119 63 -130 -21 -14 -51 -10 -91
11 -44 24 -45 20 -18 -71 45 -149 37 -167 -56 -132 -10 4 5 -19 34 -50 28 -32
73 -92 99 -135 138 -222 189 -263 378 -302 107 -22 170 -49 187 -82 13 -23 14
-73 3 -120 -5 -25 -3 -28 21 -28 34 0 52 10 136 75 73 56 115 70 181 60 68
-11 138 46 104 84 -20 22 -21 18 62 191 35 74 80 183 100 242 l35 107 56 -40
c74 -53 164 -142 201 -199 91 -142 192 -648 151 -754 -19 -47 -5 -41 24 11 58
105 70 156 69 308 -1 121 -5 152 -27 226 -32 107 -94 250 -142 329 -45 75
-166 204 -234 250 -28 19 -51 39 -51 44 0 5 29 99 65 210 35 111 73 235 84
276 30 112 75 399 86 550 4 47 9 99 11 115 8 66 4 496 -6 593 -32 335 -162
788 -297 1042 -130 243 -283 386 -487 456 -82 28 -82 28 -90 8z"/>
<path d="M6300 7695 c-1 -143 -35 -481 -66 -659 -49 -276 -106 -466 -195 -644
-56 -112 -56 -113 -35 -127 41 -29 54 -19 102 76 149 298 211 539 229 904 7
142 -10 484 -25 499 -7 7 -10 -12 -10 -49z"/>
<path d="M4095 7698 c-3 -7 -6 -56 -7 -108 -3 -89 -4 -95 -23 -92 -11 2 -35
19 -52 39 -18 20 -34 34 -34 32 -1 -2 -3 -33 -4 -69 -4 -82 -13 -153 -27 -207
l-10 -43 -46 40 c-24 22 -48 40 -52 40 -11 0 -24 -221 -17 -309 6 -83 37 -178
66 -205 73 -66 245 -71 421 -12 58 19 130 45 160 56 30 12 69 26 86 31 31 9
31 10 36 87 3 56 12 94 33 140 15 34 25 65 22 68 -3 4 -18 7 -32 7 -40 0 -35
14 50 133 59 81 99 160 91 180 -3 8 -44 13 -123 17 -65 2 -127 7 -138 10 -127
33 -204 66 -321 137 -74 45 -73 45 -79 28z"/>
<path d="M3450 7544 c-123 -70 -410 -433 -412 -522 -2 -71 84 -177 137 -170
19 3 41 40 96 158 9 19 31 61 48 93 22 38 36 81 42 127 10 68 62 184 136 303
28 44 16 47 -47 11z"/>
<path d="M4972 7109 c-21 -6 -32 -15 -29 -23 3 -6 13 -40 22 -75 17 -60 19
-62 46 -56 15 3 45 2 66 -4 90 -23 132 -20 98 9 -33 28 -15 32 98 26 109 -7
226 -20 273 -32 22 -6 24 -4 18 27 -4 19 -9 37 -13 41 -7 8 -256 73 -316 82
-71 12 -229 14 -263 5z"/>
<path d="M4810 7085 c-36 -7 -81 -18 -100 -24 -67 -21 -66 -18 -35 -135 15
-58 33 -106 38 -106 14 0 135 68 142 80 3 5 26 12 50 16 25 3 45 8 45 11 0 3
-6 31 -14 62 -8 31 -17 68 -20 84 -7 30 -9 31 -106 12z"/>
<path d="M7315 6989 c-227 -53 -672 -275 -915 -457 -73 -55 -231 -196 -284
-252 l-48 -53 26 -25 25 -25 83 84 c119 121 227 202 478 361 113 72 265 152
437 232 216 100 260 123 268 136 7 12 -15 11 -70 -1z"/>
<path d="M2974 6954 c-7 -29 1 -307 14 -484 10 -129 25 -174 70 -212 48 -40
97 -53 127 -34 13 9 29 25 35 37 20 37 22 160 6 313 -9 82 -16 172 -16 202 l0
53 -30 -9 c-17 -5 -33 -9 -35 -10 -32 -5 -130 93 -140 141 -8 36 -23 37 -31 3z"/>
<path d="M2484 6858 c-73 -124 -162 -305 -184 -376 -24 -78 -17 -102 47 -142
57 -36 90 -39 103 -7 17 41 49 232 59 359 6 69 13 131 16 139 11 30 22 109 15
109 -4 0 -29 -37 -56 -82z"/>
<path d="M4563 6824 c-64 -39 -317 -95 -423 -94 -96 0 -119 4 -180 25 l-42 15
7 -28 c8 -32 34 -67 105 -142 28 -30 77 -84 108 -120 30 -36 89 -100 131 -142
l76 -78 -63 0 c-34 0 -62 -3 -62 -7 0 -32 78 -102 230 -208 47 -33 135 -96
195 -141 61 -44 120 -86 133 -93 29 -16 50 -4 127 69 33 31 80 70 105 86 25
16 47 31 50 35 5 6 110 66 185 106 85 45 76 75 -57 189 -57 48 -208 192 -336
318 -127 127 -235 232 -240 233 -4 1 -26 -9 -49 -23z"/>
<path d="M5840 6684 c-30 -8 -82 -14 -115 -13 -33 0 -66 0 -73 0 -10 -1 -12
-21 -10 -82 3 -75 5 -84 29 -101 49 -37 193 -36 216 1 14 23 29 211 17 210 -5
-1 -34 -7 -64 -15z"/>
<path d="M3914 6633 c-49 -114 -125 -336 -135 -397 -15 -86 -7 -135 27 -175
46 -55 140 -50 163 7 5 15 11 59 13 97 3 56 13 91 51 179 26 60 47 114 47 122
0 7 -20 36 -45 64 -25 28 -54 68 -65 90 -28 54 -37 56 -56 13z"/>
<path d="M5620 6407 c-12 -34 -36 -88 -53 -120 -16 -31 -28 -60 -25 -62 3 -3
41 3 84 13 43 11 112 20 153 21 41 1 78 4 81 6 4 3 10 30 14 60 3 31 13 72 21
92 14 33 14 35 -3 28 -9 -4 -51 -9 -92 -11 -57 -2 -85 1 -117 16 l-41 18 -22
-61z"/>
<path d="M2257 6308 c7 -119 45 -313 89 -448 54 -167 71 -198 116 -202 42 -4
98 9 120 29 22 20 19 36 -48 243 -61 190 -84 277 -84 314 0 17 -7 29 -17 32
-46 15 -100 41 -134 66 -22 16 -40 28 -42 28 -2 0 -2 -28 0 -62z"/>
<path d="M5935 6220 c-31 -44 -212 -222 -264 -259 -54 -38 -24 2 65 89 91 89
149 158 141 167 -7 6 -266 -22 -357 -39 -225 -42 -501 -216 -745 -470 -199
-206 -250 -272 -266 -349 -32 -150 60 -415 211 -604 152 -190 373 -343 525
-361 161 -19 456 6 511 43 23 16 231 377 296 513 36 76 104 281 153 464 35
128 30 177 -34 351 -69 185 -71 193 -71 248 0 44 -2 48 -16 36 -24 -20 -69
-114 -108 -229 -43 -124 -64 -154 -35 -49 33 121 103 284 150 351 11 14 19 30
19 35 0 5 -25 24 -55 42 -30 18 -59 39 -66 47 -17 21 -24 18 -54 -26z"/>
<path d="M6446 6230 c-44 -16 -83 -35 -88 -42 -9 -14 -2 -158 11 -204 12 -45
34 -52 78 -23 21 14 50 33 66 43 l27 17 -1 119 c0 66 -4 120 -7 119 -4 0 -43
-13 -86 -29z"/>
<path d="M3010 6219 c0 -59 289 -869 310 -869 4 0 14 24 20 53 25 105 54 140
113 133 20 -2 37 -3 37 -1 0 1 -24 70 -54 152 -30 83 -83 241 -119 351 l-63
200 -28 -34 c-23 -30 -33 -34 -72 -34 -53 0 -108 23 -129 54 -15 20 -15 20
-15 -5z"/>
<path d="M6244 6124 c-99 -79 -102 -86 -80 -181 10 -43 30 -114 43 -157 25
-80 25 -80 38 -50 7 16 36 60 64 99 42 56 51 74 46 95 -4 14 -12 73 -19 133
-7 59 -15 109 -17 112 -3 2 -36 -20 -75 -51z"/>
<path d="M3980 6022 c0 -6 -14 -19 -32 -28 -32 -16 -118 -16 -161 1 -12 5 12
-54 79 -191 53 -110 143 -298 201 -419 57 -121 111 -227 119 -235 13 -13 18
-11 41 20 15 19 50 59 80 88 29 29 53 57 53 63 0 6 -63 122 -139 257 -77 136
-162 294 -190 351 -28 57 -51 99 -51 93z"/>
<path d="M6710 5911 c0 -5 20 -53 44 -107 73 -168 156 -442 156 -520 0 -45 4
-55 27 -74 30 -23 30 -23 118 14 33 13 76 26 95 28 31 4 35 8 38 38 6 62 -52
170 -92 170 -12 0 -14 -5 -6 -25 10 -25 4 -55 -10 -55 -4 0 -17 30 -29 68 -34
105 -161 299 -270 415 -51 54 -71 68 -71 48z"/>
<path d="M2571 5645 c-46 -37 -105 -47 -129 -23 -24 24 -11 -5 45 -100 127
-218 184 -296 292 -402 58 -58 138 -126 176 -152 l70 -46 5 53 c5 56 23 85 72
115 l28 17 -55 37 c-69 48 -253 262 -405 473 -25 34 -47 63 -50 63 -3 0 -25
-16 -49 -35z"/>
<path d="M3451 5493 c-38 -19 -78 -74 -87 -121 -6 -27 0 -40 35 -86 81 -105
479 -476 511 -476 6 0 122 145 128 159 7 19 -301 349 -441 473 -42 37 -85 68
-95 68 -9 0 -32 -8 -51 -17z"/>
<path d="M6261 5478 c-5 -18 -26 -95 -46 -172 -43 -164 -75 -256 -122 -350
-19 -37 -33 -69 -31 -70 87 -59 168 -125 365 -303 50 -44 100 -86 112 -92 18
-10 21 -9 21 9 0 11 -9 38 -20 60 -35 69 -24 89 27 46 15 -13 96 -70 178 -127
152 -105 292 -181 354 -193 l33 -6 -16 25 c-76 115 -126 250 -166 440 -12 61
-31 146 -41 190 l-18 80 -88 83 c-47 46 -159 137 -247 203 -89 65 -187 139
-218 164 -32 25 -59 45 -62 45 -2 0 -9 -15 -15 -32z"/>
<path d="M4348 5238 c-144 -142 -397 -435 -508 -588 -30 -41 -57 -77 -61 -80
-3 -3 -47 -66 -96 -140 -171 -258 -272 -462 -317 -640 -15 -63 -15 -69 5 -140
25 -90 96 -235 155 -315 75 -101 203 -224 329 -313 233 -166 540 -300 669
-293 26 1 149 124 355 352 192 213 185 205 287 334 298 379 438 604 535 862
22 57 39 106 39 108 0 3 -10 0 -22 -6 -59 -31 -124 -39 -318 -35 -214 4 -266
13 -379 68 -226 110 -420 341 -526 628 -25 66 -28 90 -29 192 -1 65 -2 118 -2
118 -1 0 -53 -51 -116 -112z"/>
<path d="M7119 5202 c-40 -20 -88 -35 -123 -39 l-58 -6 6 -30 c3 -16 6 -54 6
-83 0 -70 22 -84 90 -59 25 9 71 25 103 34 32 9 62 23 66 29 8 13 -4 139 -17
169 -6 17 -12 16 -73 -15z"/>
<path d="M7547 5062 c-107 -101 -138 -112 -183 -67 -19 19 -25 35 -24 63 1 20
2 41 1 45 -1 5 -19 -14 -41 -42 -25 -31 -60 -61 -91 -77 -55 -27 -60 -29 -159
-49 -63 -13 -90 -32 -90 -64 0 -75 49 -233 138 -441 53 -125 57 -130 106 -130
22 0 68 7 101 14 33 8 105 17 160 20 55 4 108 9 118 12 16 5 16 7 -3 31 -13
17 -17 30 -11 36 5 5 76 16 158 24 81 7 159 16 173 19 l25 6 -22 18 c-13 10
-23 22 -23 27 0 4 63 8 139 8 124 0 181 9 143 22 -31 11 -107 70 -110 84 -2
10 16 31 46 53 28 20 53 43 56 51 17 46 -45 76 -176 85 -80 6 -105 12 -145 35
-71 41 -115 93 -163 189 -23 47 -46 86 -52 86 -5 0 -37 -26 -71 -58z"/>
<path d="M3110 5050 c-30 -32 -60 -89 -60 -113 0 -30 218 -189 335 -245 28
-13 70 -34 95 -45 25 -12 83 -46 129 -76 l83 -54 19 24 c38 49 99 140 99 148
0 7 -271 201 -294 210 -6 2 -38 23 -71 47 -94 65 -234 134 -273 134 -25 0 -41
-8 -62 -30z"/>
<path d="M6768 4376 c-23 -32 -125 -100 -168 -114 -19 -5 -61 -13 -94 -16 -32
-4 -66 -11 -77 -16 -18 -10 -17 -11 3 -45 16 -29 19 -47 16 -93 -3 -31 -9 -65
-13 -76 -8 -18 -4 -18 61 -11 90 9 200 47 272 92 31 20 91 69 132 111 69 68
73 75 56 86 -11 6 -48 30 -84 54 -82 56 -84 56 -104 28z"/>
<path d="M7617 4137 l-168 -258 19 -42 c29 -64 55 -82 118 -81 64 0 107 30
167 114 45 64 47 78 8 51 -59 -42 -69 -12 -20 62 37 57 51 147 47 306 l-3 105
-168 -257z"/>
<path d="M5727 4246 c-18 -28 -87 -175 -87 -186 0 -10 16 -15 90 -29 142 -26
251 -34 442 -35 240 -1 239 -2 245 86 5 62 -18 117 -57 135 -14 6 -63 14 -110
18 -47 4 -98 9 -115 10 -39 4 -264 17 -337 20 -49 2 -60 -1 -71 -19z"/>
<path d="M7380 3778 c-132 -121 -284 -208 -435 -248 -44 -12 -84 -26 -89 -31
-12 -11 56 -94 103 -124 51 -34 83 -37 155 -15 61 19 75 27 181 101 28 19 86
56 130 82 57 34 95 66 133 112 28 35 52 66 52 69 0 4 -18 3 -39 0 -51 -8 -98
18 -120 68 l-16 36 -55 -50z"/>
<path d="M5472 3742 c-73 -100 -95 -133 -91 -136 11 -10 139 -84 169 -98 83
-39 250 -94 340 -113 8 -2 54 -12 101 -24 99 -24 102 -24 94 -11 -3 5 5 31 19
57 23 45 32 58 91 124 l23 27 -96 36 c-272 104 -445 166 -472 171 -16 3 -40 9
-54 15 -15 5 -26 7 -26 5 0 -2 -11 0 -24 5 -23 8 -29 3 -74 -58z"/>
<path d="M3247 3598 c-86 -146 -197 -363 -197 -384 0 -31 93 -210 170 -329
111 -171 289 -335 485 -450 61 -36 171 -90 205 -100 14 -4 30 -12 37 -17 6 -5
34 -12 60 -15 l49 -5 175 150 c194 167 261 232 239 233 -16 1 -36 4 -90 15
-19 4 -40 8 -47 10 -15 3 -103 30 -168 51 -27 9 -66 26 -85 37 -38 22 -192
127 -200 136 -3 3 -36 31 -75 62 -150 119 -296 276 -368 395 -21 34 -59 116
-86 183 l-48 122 -56 -94z"/>
<path d="M6223 3515 c-36 -15 -91 -77 -99 -112 -9 -34 5 -72 36 -94 79 -58
450 -92 655 -60 86 13 205 49 205 63 0 5 -4 7 -8 4 -5 -3 -31 5 -58 18 -57 26
-130 103 -139 147 -4 16 -10 27 -14 25 -26 -16 -282 -25 -341 -12 -140 31
-200 36 -237 21z"/>
<path d="M5280 3493 c0 -4 -13 -24 -29 -43 -124 -152 -122 -149 -103 -167 10
-10 60 -58 112 -107 52 -50 145 -138 205 -196 169 -162 200 -185 252 -185 39
0 49 5 79 37 19 22 34 48 34 62 0 55 -269 393 -388 487 -94 75 -162 122 -162
112z"/>
<path d="M7664 3103 c-32 -43 -63 -83 -68 -89 -6 -6 -29 -31 -51 -55 -63 -68
-155 -144 -205 -169 -113 -56 -129 -89 -73 -151 19 -21 51 -48 70 -60 l34 -21
22 20 c12 11 39 25 59 31 32 11 108 79 108 98 0 4 -5 2 -12 -5 -18 -18 -38
-15 -38 6 0 10 34 69 75 132 74 113 127 229 138 303 3 20 4 37 2 37 -2 0 -30
-35 -61 -77z"/>
<path d="M2978 3107 c-21 -34 -53 -87 -73 -116 -66 -103 -66 -103 10 -253 37
-73 93 -169 124 -212 69 -96 201 -232 279 -284 125 -86 361 -182 444 -182 15
0 47 16 75 36 53 40 173 154 173 166 0 3 -26 12 -57 18 -159 33 -220 63 -387
189 -235 178 -341 305 -483 576 -35 69 -65 125 -66 125 -1 0 -19 -28 -39 -63z"/>
<path d="M5845 2923 c28 -66 6 -126 -53 -142 -20 -6 -43 -11 -51 -11 -25 0 7
-27 125 -106 208 -138 505 -293 577 -301 57 -7 65 7 28 50 -35 39 -53 80 -45
101 3 8 21 20 41 27 20 6 39 21 45 35 13 28 4 42 -32 49 -44 8 -95 28 -145 55
-90 49 -178 96 -320 170 -77 40 -144 77 -149 81 -17 15 -29 10 -21 -8z"/>
<path d="M2810 2819 c0 -6 -41 -92 -91 -192 l-90 -182 80 -160 c44 -88 100
-186 125 -217 98 -123 325 -272 503 -329 71 -22 72 -22 115 -4 28 13 87 63
172 149 l130 130 -55 11 c-174 36 -347 131 -537 295 -107 92 -167 175 -260
357 -70 139 -92 173 -92 142z"/>
<path d="M7160 2680 c-33 -16 -114 -36 -305 -74 -88 -18 -270 -25 -297 -12
-15 7 -18 3 -18 -20 0 -39 -12 -56 -51 -68 -18 -6 -34 -17 -37 -24 -6 -18 66
-83 122 -111 55 -27 122 -24 310 13 321 64 429 96 464 139 11 12 7 16 -20 21
-57 12 -118 81 -118 136 0 23 -4 24 -50 0z"/>
<path d="M2517 2261 c-49 -79 -87 -151 -87 -165 0 -136 113 -336 251 -444 112
-88 271 -171 355 -186 16 -3 40 -8 54 -11 106 -21 112 -20 169 31 29 26 75 80
103 118 l50 71 -29 3 c-128 12 -337 119 -478 245 -87 78 -147 164 -231 334
l-70 144 -87 -140z"/>
<path d="M1575 2228 c-31 -18 -52 -48 -91 -126 -55 -113 -65 -150 -71 -254
-10 -168 27 -325 92 -394 30 -31 38 -35 65 -29 71 16 287 113 626 281 l251
125 -24 87 c-59 212 -46 189 -106 201 -28 6 -65 13 -82 16 -28 5 -46 8 -105
20 -14 2 -41 7 -60 10 -19 3 -71 12 -115 20 -44 8 -96 17 -115 20 -19 3 -46 7
-60 10 -117 21 -185 25 -205 13z"/>
<path d="M2365 1759 c-44 -21 -168 -81 -275 -133 -240 -116 -441 -226 -497
-272 l-42 -34 15 -53 c21 -69 86 -190 145 -269 96 -128 273 -278 329 -278 27
0 34 8 165 175 52 66 277 383 375 529 l91 136 -102 120 c-56 66 -107 120 -113
119 -6 0 -47 -18 -91 -40z"/>
<path d="M2635 1413 c-44 -60 -136 -187 -205 -283 -69 -96 -147 -204 -172
-240 -26 -36 -58 -79 -71 -95 -14 -17 -35 -47 -47 -68 -31 -53 -16 -84 71
-147 96 -70 184 -113 294 -145 309 -90 341 -79 360 120 7 71 65 426 111 673
13 73 24 140 24 148 0 8 -24 25 -52 38 -29 13 -92 42 -140 65 -48 22 -88 41
-90 41 -2 0 -39 -48 -83 -107z"/>
<path d="M3130 1390 c-14 -4 -39 -8 -57 -9 -17 -1 -34 -5 -37 -10 -22 -36
-127 -689 -133 -833 -7 -162 -9 -160 106 -157 47 2 100 6 116 10 17 4 44 10
60 12 86 13 298 79 368 114 66 33 74 54 47 134 -29 88 -159 348 -283 564 -95
166 -105 180 -133 182 -16 1 -40 -2 -54 -7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,379 @@
import {
Modal,
Center,
Group,
TextInput,
Image,
Button,
Select,
LoadingOverlay,
ActionIcon,
Tooltip,
Title,
Anchor,
Text,
Tabs,
MultiSelect,
ScrollArea,
Switch,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { IconApps as Apps } from '@tabler/icons';
import { v4 as uuidv4 } from 'uuid';
import { useDebouncedValue } from '@mantine/hooks';
import { useConfig } from '../../tools/state';
import { ServiceTypeList, StatusCodes } from '../../tools/types';
export function AddItemShelfButton(props: any) {
const [opened, setOpened] = useState(false);
return (
<>
<Modal
size="xl"
radius="md"
title={<Title order={3}>Add service</Title>}
opened={props.opened || opened}
onClose={() => setOpened(false)}
>
<AddAppShelfItemForm setOpened={setOpened} />
</Modal>
<ActionIcon
variant="default"
radius="md"
size="xl"
color="blue"
style={props.style}
onClick={() => setOpened(true)}
>
<Tooltip label="Add a service">
<Apps />
</Tooltip>
</ActionIcon>
</>
);
}
function MatchIcon(name: string, form: any) {
fetch(
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
.replace(/\s+/g, '-')
.toLowerCase()}.png`
).then((res) => {
if (res.ok) {
form.setFieldValue('icon', res.url);
}
});
return false;
}
function MatchService(name: string, form: any) {
const service = ServiceTypeList.find((s) => s.toLowerCase() === name.toLowerCase());
if (service) {
form.setFieldValue('type', service);
}
}
function MatchPort(name: string, form: any) {
const portmap = [
{ name: 'qbittorrent', value: '8080' },
{ name: 'sonarr', value: '8989' },
{ name: 'radarr', value: '7878' },
{ name: 'lidarr', value: '8686' },
{ name: 'readarr', value: '8686' },
{ name: 'deluge', value: '8112' },
{ name: 'transmission', value: '9091' },
];
// Match name with portmap key
const port = portmap.find((p) => p.name === name.toLowerCase());
if (port) {
form.setFieldValue('url', `http://localhost:${port.value}`);
}
}
export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & any) {
const { setOpened } = props;
const { config, setConfig } = useConfig();
const [isLoading, setLoading] = useState(false);
// Extract all the categories from the services in config
const categoryList = config.services.reduce((acc, cur) => {
if (cur.category && !acc.includes(cur.category)) {
acc.push(cur.category);
}
return acc;
}, [] as string[]);
const form = useForm({
initialValues: {
id: props.id ?? uuidv4(),
type: props.type ?? 'Other',
category: props.category ?? undefined,
name: props.name ?? '',
icon: props.icon ?? '/favicon.svg',
url: props.url ?? '',
apiKey: props.apiKey ?? (undefined as unknown as string),
username: props.username ?? (undefined as unknown as string),
password: props.password ?? (undefined as unknown as string),
openedUrl: props.openedUrl ?? (undefined as unknown as string),
status: props.status ?? ['200'],
newTab: props.newTab ?? true,
},
validate: {
apiKey: () => null,
// Validate icon with a regex
icon: (value: string) => {
// Regex to match everything that ends with and icon extension
if (!value.match(/\.(png|jpg|jpeg|gif|svg)$/)) {
return 'Please enter a valid icon URL';
}
return null;
},
// Validate url with a regex http/https
url: (value: string) => {
try {
const _isValid = new URL(value);
} catch (e) {
return 'Please enter a valid URL';
}
return null;
},
status: (value: string[]) => {
if (!value.length) {
return 'Please select a status code';
}
return null;
},
},
});
const [debounced, cancel] = useDebouncedValue(form.values.name, 250);
useEffect(() => {
if (form.values.name !== debounced || props.name || props.type) return;
MatchIcon(form.values.name, form);
MatchService(form.values.name, form);
MatchPort(form.values.name, form);
}, [debounced]);
// Try to set const hostname to new URL(form.values.url).hostname)
// If it fails, set it to the form.values.url
let hostname = form.values.url;
try {
hostname = new URL(form.values.url).origin;
} catch (e) {
// Do nothing
}
return (
<>
<Center>
<Image
height={120}
width={120}
fit="contain"
src={form.values.icon}
alt="Placeholder"
withPlaceholder
/>
</Center>
<form
onSubmit={form.onSubmit(() => {
if (JSON.stringify(form.values.status) === JSON.stringify(['200'])) {
form.values.status = undefined;
}
if (form.values.newTab === true) {
form.values.newTab = undefined;
}
// If service already exists, update it.
if (config.services && config.services.find((s) => s.id === form.values.id)) {
setConfig({
...config,
// replace the found item by matching ID
services: config.services.map((s) => {
if (s.id === form.values.id) {
return {
...form.values,
};
}
return s;
}),
});
} else {
setConfig({
...config,
services: [...config.services, form.values],
});
}
setOpened(false);
form.reset();
})}
>
<Tabs grow>
<Tabs.Tab label="Options">
<ScrollArea style={{ height: 500 }} scrollbarSize={4}>
<Group direction="column" grow>
<TextInput
required
label="Service name"
placeholder="Plex"
{...form.getInputProps('name')}
/>
<TextInput
required
label="Icon URL"
placeholder="/favicon.svg"
{...form.getInputProps('icon')}
/>
<TextInput
required
label="Service URL"
placeholder="http://localhost:7575"
{...form.getInputProps('url')}
/>
<TextInput
label="On Click URL"
placeholder="http://sonarr.example.com"
{...form.getInputProps('openedUrl')}
/>
<Select
label="Service type"
defaultValue="Other"
placeholder="Pick one"
required
searchable
data={ServiceTypeList}
{...form.getInputProps('type')}
/>
<Select
label="Category"
data={categoryList}
placeholder="Select a category or create a new one"
nothingFound="Nothing found"
searchable
clearable
creatable
onClick={(e) => {
e.preventDefault();
}}
getCreateLabel={(query) => `+ Create "${query}"`}
onCreate={(query) => {}}
{...form.getInputProps('category')}
/>
<LoadingOverlay visible={isLoading} />
{(form.values.type === 'Sonarr' ||
form.values.type === 'Radarr' ||
form.values.type === 'Lidarr' ||
form.values.type === 'Readarr') && (
<>
<TextInput
required
label="API key"
placeholder="Your API key"
value={form.values.apiKey}
onChange={(event) => {
form.setFieldValue('apiKey', event.currentTarget.value);
}}
error={form.errors.apiKey && 'Invalid API key'}
/>
<Text
style={{
alignSelf: 'center',
fontSize: '0.75rem',
textAlign: 'center',
color: 'gray',
}}
>
Tip: Get your API key{' '}
<Anchor
target="_blank"
weight="bold"
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
href={`${hostname}/settings/general`}
>
here.
</Anchor>
</Text>
</>
)}
{form.values.type === 'qBittorrent' && (
<>
<TextInput
required
label="Username"
placeholder="admin"
value={form.values.username}
onChange={(event) => {
form.setFieldValue('username', event.currentTarget.value);
}}
error={form.errors.username && 'Invalid username'}
/>
<TextInput
required
label="Password"
placeholder="adminadmin"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
{(form.values.type === 'Deluge' ||
form.values.type === 'Transmission' ||
form.values.type === 'qBittorrent') && (
<>
<TextInput
label="Username"
placeholder="admin"
value={form.values.username}
onChange={(event) => {
form.setFieldValue('username', event.currentTarget.value);
}}
error={form.errors.username && 'Invalid username'}
/>
<TextInput
label="Password"
placeholder="password"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
</Group>
</ScrollArea>
</Tabs.Tab>
<Tabs.Tab label="Advanced Options">
<Group direction="column" grow>
<MultiSelect
required
label="HTTP Status Codes"
data={StatusCodes}
placeholder="Select valid status codes"
clearButtonLabel="Clear selection"
nothingFound="Nothing found"
defaultValue={['200']}
clearable
searchable
{...form.getInputProps('status')}
/>
<Switch
label="Open service in new tab"
defaultChecked={form.values.newTab}
{...form.getInputProps('newTab')}
/>
</Group>
</Tabs.Tab>
</Tabs>
<Group grow position="center" mt="xl">
<Button type="submit">{props.message ?? 'Add service'}</Button>
</Group>
</form>
</>
);
}

View File

@@ -1,4 +1,6 @@
import AppShelf, { AppShelfItem } from './AppShelf';
import { SimpleGrid } from '@mantine/core';
import AppShelf from './AppShelf';
import { AppShelfItem } from './AppShelfItem';
export default {
title: 'Item Shelf',
@@ -16,3 +18,10 @@ export default {
export const Default = (args: any) => <AppShelf {...args} />;
export const One = (args: any) => <AppShelfItem {...args} />;
export const Ten = (args: any) => (
<SimpleGrid>
{Array.from(Array(10)).map((_, i) => (
<AppShelfItem {...args} key={i} />
))}
</SimpleGrid>
);

View File

@@ -0,0 +1,196 @@
import React, { useState } from 'react';
import { Accordion, createStyles, Grid, Group, Paper, useMantineColorScheme } from '@mantine/core';
import {
closestCenter,
DndContext,
DragOverlay,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { arrayMove, SortableContext } from '@dnd-kit/sortable';
import { useLocalStorage } from '@mantine/hooks';
import { useConfig } from '../../tools/state';
import { SortableAppShelfItem, AppShelfItem } from './AppShelfItem';
import { ModuleMenu, ModuleWrapper } from '../modules/moduleWrapper';
import { DownloadsModule } from '../modules';
import DownloadComponent from '../modules/downloads/DownloadsModule';
const useStyles = createStyles((theme, _params) => ({
item: {
borderBottom: 0,
overflow: 'hidden',
border: '1px solid transparent',
borderRadius: theme.radius.lg,
marginTop: theme.spacing.md,
},
itemOpened: {
borderColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[3],
},
}));
const AppShelf = (props: any) => {
const { classes, cx } = useStyles(props);
const [toggledCategories, settoggledCategories] = useLocalStorage({
key: 'app-shelf-toggled',
// This is a bit of a hack to get the 5 first categories to be toggled on by default
defaultValue: { 0: true, 1: true, 2: true, 3: true, 4: true } as Record<string, boolean>,
});
const [activeId, setActiveId] = useState(null);
const { config, setConfig } = useConfig();
const { colorScheme } = useMantineColorScheme();
const sensors = useSensors(
useSensor(TouchSensor, {
activationConstraint: {
delay: 500,
tolerance: 5,
},
}),
useSensor(MouseSensor, {
// Require the mouse to move by 10 pixels before activating
activationConstraint: {
delay: 500,
tolerance: 5,
},
})
);
function handleDragStart(event: any) {
const { active } = event;
setActiveId(active.id);
}
function handleDragEnd(event: any) {
const { active, over } = event;
if (active.id !== over.id) {
const newConfig = { ...config };
const activeIndex = newConfig.services.findIndex((e) => e.id === active.id);
const overIndex = newConfig.services.findIndex((e) => e.id === over.id);
newConfig.services = arrayMove(newConfig.services, activeIndex, overIndex);
setConfig(newConfig);
}
setActiveId(null);
}
// Extract all the categories from the services in config
const categoryList = config.services.reduce((acc, cur) => {
if (cur.category && !acc.includes(cur.category)) {
acc.push(cur.category);
}
return acc;
}, [] as string[]);
const item = (filter?: string) => {
// If filter is not set, return all the services without a category or a null category
let filtered = config.services;
if (!filter) {
filtered = config.services.filter((e) => !e.category || e.category === null);
}
if (filter) {
filtered = config.services.filter((e) => e.category === filter);
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext items={config.services}>
<Grid gutter="xl" align="center">
{filtered.map((service) => (
<Grid.Col
key={service.id}
span={6}
xl={config.settings.appCardWidth || 2}
xs={4}
sm={3}
md={3}
>
<SortableAppShelfItem service={service} key={service.id} id={service.id} />
</Grid.Col>
))}
</Grid>
</SortableContext>
<DragOverlay
style={{
// Add a shadow to the drag overlay
boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)',
}}
>
{activeId ? (
<AppShelfItem service={config.services.find((e) => e.id === activeId)} id={activeId} />
) : null}
</DragOverlay>
</DndContext>
);
};
if (categoryList.length > 0) {
const noCategory = config.services.filter(
(e) => e.category === undefined || e.category === null
);
// Create an item with 0: true, 1: true, 2: true... For each category
return (
// Return one item for each category
<Group grow direction="column">
<Accordion
disableIconRotation
classNames={classes}
order={2}
iconPosition="right"
multiple
styles={{
item: {
borderRadius: '20px',
},
}}
initialState={toggledCategories}
onChange={(idx) => settoggledCategories(idx)}
>
{categoryList.map((category, idx) => (
<Accordion.Item key={category} label={category}>
{item(category)}
</Accordion.Item>
))}
{/* Return the item for all services without category */}
{noCategory && noCategory.length > 0 ? (
<Accordion.Item key="Other" label="Other">
{item()}
</Accordion.Item>
) : null}
<Accordion.Item key="Downloads" label="Your downloads">
<Paper
p="lg"
radius="lg"
style={{
background: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '255, 255, 255,'} \
${(config.settings.appOpacity || 100) / 100}`,
borderColor: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '233, 236, 239,'} \
${(config.settings.appOpacity || 100) / 100}`,
}}
>
<ModuleMenu module={DownloadsModule} />
<DownloadComponent />
</Paper>
</Accordion.Item>
</Accordion>
</Group>
);
}
return (
<Group grow direction="column">
{item()}
<ModuleWrapper mt="xl" module={DownloadsModule} />
</Group>
);
};
export default AppShelf;

View File

@@ -0,0 +1,143 @@
import {
Text,
Card,
Anchor,
AspectRatio,
Image,
Center,
createStyles,
useMantineColorScheme,
} from '@mantine/core';
import { motion } from 'framer-motion';
import { useState } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { serviceItem } from '../../tools/types';
import PingComponent from '../modules/ping/PingModule';
import AppShelfMenu from './AppShelfMenu';
import { useConfig } from '../../tools/state';
const useStyles = createStyles((theme) => ({
item: {
transition: 'box-shadow 150ms ease, transform 100ms ease',
'&:hover': {
boxShadow: `${theme.shadows.md} !important`,
transform: 'scale(1.05)',
},
[theme.fn.smallerThan('sm')]: {
WebkitUserSelect: 'none',
},
},
}));
export function SortableAppShelfItem(props: any) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id: props.id,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<AppShelfItem service={props.service} />
</div>
);
}
export function AppShelfItem(props: any) {
const { service }: { service: serviceItem } = props;
const [hovering, setHovering] = useState(false);
const { config } = useConfig();
const { colorScheme } = useMantineColorScheme();
const { classes } = useStyles();
return (
<motion.div
animate={{
scale: [0.9, 1.06, 1],
rotate: [0, 5, 0],
}}
transition={{ duration: 0.6, type: 'spring', damping: 10, mass: 0.75, stiffness: 100 }}
key={service.name}
onHoverStart={() => {
setHovering(true);
}}
onHoverEnd={() => {
setHovering(false);
}}
>
<Card
withBorder
radius="lg"
shadow="md"
className={classes.item}
style={{
background: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '255, 255, 255,'} \
${(config.settings.appOpacity || 100) / 100}`,
borderColor: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '233, 236, 239,'} \
${(config.settings.appOpacity || 100) / 100}`,
}}
>
<Card.Section>
<Anchor
target={service.newTab === false ? '_top' : '_blank'}
href={service.openedUrl ? service.openedUrl : service.url}
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
<Text mt="sm" align="center" lineClamp={1} weight={550}>
{service.name}
</Text>
</Anchor>
<motion.div
style={{
position: 'absolute',
top: 15,
right: 15,
alignSelf: 'flex-end',
}}
animate={{
opacity: hovering ? 1 : 0,
}}
>
<AppShelfMenu service={service} />
</motion.div>
</Card.Section>
<Center>
<Card.Section>
<AspectRatio
ratio={3 / 5}
m="xl"
style={{
width: 150,
height: 90,
}}
>
<motion.i
whileHover={{
scale: 1.1,
}}
>
<Image
styles={{ root: { cursor: 'pointer' } }}
width={80}
height={80}
src={service.icon}
fit="contain"
onClick={() => {
if (service.openedUrl) {
window.open(service.openedUrl, service.newTab === false ? '_top' : '_blank');
} else window.open(service.url, service.newTab === false ? '_top' : '_blank');
}}
/>
</motion.i>
</AspectRatio>
<PingComponent url={service.url} status={service.status} />
</Card.Section>
</Center>
</Card>
</motion.div>
);
}

View File

@@ -1,38 +1,46 @@
import { Menu, Modal, Text } from '@mantine/core';
import { Menu, Modal, Text, useMantineTheme } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { useState } from 'react';
import { Check, Edit, Trash } from 'tabler-icons-react';
import { IconCheck as Check, IconEdit as Edit, IconTrash as Trash } from '@tabler/icons';
import { useConfig } from '../../tools/state';
import { serviceItem } from '../../tools/types';
import { AddAppShelfItemForm } from './AddAppShelfItem';
export default function AppShelfMenu(props: any) {
const { service } = props;
const { service }: { service: serviceItem } = props;
const { config, setConfig } = useConfig();
const theme = useMantineTheme();
const [opened, setOpened] = useState(false);
return (
<>
<Modal
size="xl"
radius="lg"
radius="md"
opened={props.opened || opened}
onClose={() => setOpened(false)}
title="Modify a service"
>
<AddAppShelfItemForm
setOpened={setOpened}
name={service.name}
type={service.type}
url={service.url}
icon={service.icon}
apiKey={service.apiKey}
{...service}
message="Save service"
/>
</Modal>
<Menu position="right">
<Menu
position="right"
radius="md"
shadow="xl"
styles={{
body: {
// Add shadow and elevation to the body
boxShadow: '0 0 14px 14px rgba(0, 0, 0, 0.05)',
},
}}
>
<Menu.Label>Settings</Menu.Label>
<Menu.Item
color="primary"
icon={<Edit size={14} />}
icon={<Edit />}
// TODO: #2 Add the ability to edit the service.
onClick={() => setOpened(true)}
>
@@ -44,13 +52,13 @@ export default function AppShelfMenu(props: any) {
onClick={(e: any) => {
setConfig({
...config,
services: config.services.filter((s) => s.name !== service.name),
services: config.services.filter((s) => s.id !== service.id),
});
showNotification({
autoClose: 5000,
title: (
<Text>
Service <b>{service.name}</b> removed successfully
Service <b>{service.name}</b> removed successfully!
</Text>
),
color: 'green',
@@ -58,7 +66,7 @@ export default function AppShelfMenu(props: any) {
message: undefined,
});
}}
icon={<Trash size={14} />}
icon={<Trash />}
>
Delete
</Menu.Item>

View File

@@ -0,0 +1,2 @@
export { default as AppShelf } from './AppShelf';
export * from './AppShelfItem';

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { createStyles, Switch, Group, useMantineColorScheme } from '@mantine/core';
import { Sun, MoonStars } from 'tabler-icons-react';
import { createStyles, Switch, Group, useMantineColorScheme, Kbd } from '@mantine/core';
import { IconSun as Sun, IconMoonStars as MoonStars } from '@tabler/icons';
import { useConfig } from '../../tools/state';
const useStyles = createStyles((theme) => ({
root: {
@@ -29,6 +30,7 @@ const useStyles = createStyles((theme) => ({
}));
export function ColorSchemeSwitch() {
const { config } = useConfig();
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const { classes, cx } = useStyles();
@@ -40,6 +42,9 @@ export function ColorSchemeSwitch() {
<Switch checked={colorScheme === 'dark'} onChange={() => toggleColorScheme()} size="md" />
</div>
Switch to {colorScheme === 'dark' ? 'light' : 'dark'} mode
<Group spacing={2}>
<Kbd>Ctrl</Kbd>+<Kbd>J</Kbd>
</Group>
</Group>
);
}

View File

@@ -1,5 +1,5 @@
import { Box, useMantineColorScheme } from '@mantine/core';
import { Sun, MoonStars } from 'tabler-icons-react';
import { IconSun as Sun, IconMoonStars as MoonStars } from '@tabler/icons';
import { motion } from 'framer-motion';
export function ColorSchemeToggle() {

View File

@@ -26,7 +26,10 @@ export default function ConfigChanger() {
label="Config loader"
onChange={(e) => {
loadConfig(e ?? 'default');
setCookies('config-name', e ?? 'default', { maxAge: 60 * 60 * 24 * 30 });
setCookies('config-name', e ?? 'default', {
maxAge: 60 * 60 * 24 * 30,
sameSite: 'strict',
});
}}
data={
// If config list is empty, return the current config

View File

@@ -1,5 +1,11 @@
import { Group, Text, useMantineTheme, MantineTheme } from '@mantine/core';
import { Upload, Photo, X, Icon as TablerIcon, Check } from 'tabler-icons-react';
import {
IconUpload as Upload,
IconPhoto as Photo,
IconX as X,
IconCheck as Check,
TablerIcon,
} from '@tabler/icons';
import { DropzoneStatus, FullScreenDropzone } from '@mantine/dropzone';
import { showNotification } from '@mantine/notifications';
import { useRef } from 'react';
@@ -7,6 +13,7 @@ import { useRouter } from 'next/router';
import { setCookies } from 'cookies-next';
import { useConfig } from '../../tools/state';
import { Config } from '../../tools/types';
import { migrateToIdConfig } from '../../tools/migrate';
function getIconColor(status: DropzoneStatus, theme: MantineTheme) {
return status.accepted
@@ -83,8 +90,12 @@ export default function LoadConfigComponent(props: any) {
icon: <Check />,
message: undefined,
});
setCookies('config-name', newConfig.name, { maxAge: 60 * 60 * 24 * 30 });
setConfig(newConfig);
setCookies('config-name', newConfig.name, {
maxAge: 60 * 60 * 24 * 30,
sameSite: 'strict',
});
const migratedConfig = migrateToIdConfig(newConfig);
setConfig(migratedConfig);
});
}}
accept={['application/json']}

View File

@@ -0,0 +1,102 @@
import { Button, Group, Modal, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { showNotification } from '@mantine/notifications';
import axios from 'axios';
import fileDownload from 'js-file-download';
import { useState } from 'react';
import {
IconCheck as Check,
IconDownload as Download,
IconPlus as Plus,
IconTrash as Trash,
IconX as X,
} from '@tabler/icons';
import { useConfig } from '../../tools/state';
export default function SaveConfigComponent(props: any) {
const [opened, setOpened] = useState(false);
const { config, setConfig } = useConfig();
const form = useForm({
initialValues: {
configName: config.name,
},
});
function onClick(e: any) {
if (config) {
fileDownload(JSON.stringify(config, null, '\t'), `${config.name}.json`);
}
}
return (
<Group spacing="xs">
<Modal
radius="md"
opened={opened}
onClose={() => setOpened(false)}
title="Choose the name of your new config"
>
<form
onSubmit={form.onSubmit((values) => {
setConfig({ ...config, name: values.configName });
setOpened(false);
showNotification({
title: 'Config saved',
icon: <Check />,
color: 'green',
autoClose: 1500,
radius: 'md',
message: `Config saved as ${values.configName}`,
});
})}
>
<TextInput
required
label="Config name"
placeholder="Your new config name"
{...form.getInputProps('configName')}
/>
<Group position="right" mt="md">
<Button type="submit">Confirm</Button>
</Group>
</form>
</Modal>
<Button size="xs" leftIcon={<Download />} variant="outline" onClick={onClick}>
Download config
</Button>
<Button
size="xs"
leftIcon={<Trash />}
variant="outline"
onClick={() => {
axios
.delete(`/api/configs/${config.name}`)
.then(() => {
showNotification({
title: 'Config deleted',
icon: <Check />,
color: 'green',
autoClose: 1500,
radius: 'md',
message: 'Config deleted',
});
})
.catch(() => {
showNotification({
title: 'Config delete failed',
icon: <X />,
color: 'red',
autoClose: 1500,
radius: 'md',
message: 'Config delete failed',
});
});
setConfig({ ...config, name: 'default' });
}}
>
Delete config
</Button>
<Button size="xs" leftIcon={<Plus />} variant="outline" onClick={() => setOpened(true)}>
Save a copy
</Button>
</Group>
);
}

View File

@@ -0,0 +1,65 @@
import { TextInput, Group, Button } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useConfig } from '../../tools/state';
import { ColorSelector } from './ColorSelector';
import { OpacitySelector } from './OpacitySelector';
import { AppCardWidthSelector } from './AppCardWidthSelector';
import { ShadeSelector } from './ShadeSelector';
export default function TitleChanger() {
const { config, setConfig } = useConfig();
const form = useForm({
initialValues: {
title: config.settings.title,
logo: config.settings.logo,
favicon: config.settings.favicon,
background: config.settings.background,
},
});
const saveChanges = (values: {
title?: string;
logo?: string;
favicon?: string;
background?: string;
}) => {
setConfig({
...config,
settings: {
...config.settings,
title: values.title,
logo: values.logo,
favicon: values.favicon,
background: values.background,
},
});
};
return (
<Group direction="column" grow>
<form onSubmit={form.onSubmit((values) => saveChanges(values))}>
<Group grow direction="column">
<TextInput label="Page title" placeholder="Homarr 🦞" {...form.getInputProps('title')} />
<TextInput label="Logo" placeholder="/img/logo.png" {...form.getInputProps('logo')} />
<TextInput
label="Favicon"
placeholder="/favicon.svg"
{...form.getInputProps('favicon')}
/>
<TextInput
label="Background"
placeholder="/img/background.png"
{...form.getInputProps('background')}
/>
<Button type="submit">Save</Button>
</Group>
</form>
<ColorSelector type="primary" />
<ColorSelector type="secondary" />
<ShadeSelector />
<OpacitySelector />
<AppCardWidthSelector />
</Group>
);
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { Group, Text, Slider } from '@mantine/core';
import { useConfig } from '../../tools/state';
export function AppCardWidthSelector() {
const { config, setConfig } = useConfig();
const setappCardWidth = (appCardWidth: number) => {
setConfig({
...config,
settings: {
...config.settings,
appCardWidth,
},
});
};
return (
<Group direction="column" spacing="xs" grow>
<Text>App Width</Text>
<Slider
label={null}
defaultValue={config.settings.appCardWidth}
step={0.2}
min={0.8}
max={2}
styles={{ markLabel: { fontSize: 'xx-small' } }}
onChange={(value) => setappCardWidth(value)}
/>
</Group>
);
}

View File

@@ -0,0 +1,96 @@
import React, { useState } from 'react';
import { ColorSwatch, Group, Popover, Text, useMantineTheme } from '@mantine/core';
import { useConfig } from '../../tools/state';
import { useColorTheme } from '../../tools/color';
interface ColorControlProps {
type: string;
}
export function ColorSelector({ type }: ColorControlProps) {
const { config, setConfig } = useConfig();
const [opened, setOpened] = useState(false);
const { primaryColor, secondaryColor, setPrimaryColor, setSecondaryColor } = useColorTheme();
const theme = useMantineTheme();
const colors = Object.keys(theme.colors).map((color) => ({
swatch: theme.colors[color][6],
color,
}));
const configColor = type === 'primary' ? primaryColor : secondaryColor;
const setConfigColor = (color: string) => {
if (type === 'primary') {
setPrimaryColor(color);
setConfig({
...config,
settings: {
...config.settings,
primaryColor: color,
},
});
} else {
setSecondaryColor(color);
setConfig({
...config,
settings: {
...config.settings,
secondaryColor: color,
},
});
}
};
const swatches = colors.map(({ color, swatch }) => (
<ColorSwatch
component="button"
type="button"
onClick={() => setConfigColor(color)}
key={color}
color={swatch}
size={22}
style={{ color: theme.white, cursor: 'pointer' }}
/>
));
return (
<Group direction="row" spacing={3}>
<Popover
opened={opened}
onClose={() => setOpened(false)}
transitionDuration={0}
target={
<ColorSwatch
component="button"
type="button"
color={theme.colors[configColor][6]}
onClick={() => setOpened((o) => !o)}
size={22}
style={{ display: 'block', cursor: 'pointer' }}
/>
}
styles={{
root: {
marginRight: theme.spacing.xs,
},
body: {
width: 152,
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white,
},
arrow: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white,
},
}}
position="bottom"
placement="end"
withArrow
arrowSize={3}
>
<Group spacing="xs">{swatches}</Group>
</Popover>
<Text>{type[0].toUpperCase() + type.slice(1)} color</Text>
</Group>
);
}

View File

@@ -0,0 +1,119 @@
import { ActionIcon, Group, Text, SegmentedControl, TextInput, Anchor } from '@mantine/core';
import { useState } from 'react';
import { IconBrandGithub as BrandGithub } from '@tabler/icons';
import { CURRENT_VERSION } from '../../../data/constants';
import { useConfig } from '../../tools/state';
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch';
import ConfigChanger from '../Config/ConfigChanger';
import SaveConfigComponent from '../Config/SaveConfig';
import ModuleEnabler from './ModuleEnabler';
export default function CommonSettings(args: any) {
const { config, setConfig } = useConfig();
const matches = [
{ label: 'Google', value: 'https://google.com/search?q=' },
{ label: 'DuckDuckGo', value: 'https://duckduckgo.com/?q=' },
{ label: 'Bing', value: 'https://bing.com/search?q=' },
{ label: 'Custom', value: 'Custom' },
];
const [customSearchUrl, setCustomSearchUrl] = useState(config.settings.searchUrl);
const [searchUrl, setSearchUrl] = useState(
matches.find((match) => match.value === config.settings.searchUrl)?.value ?? 'Custom'
);
return (
<Group direction="column" grow>
<Group grow direction="column" spacing={0}>
<Text>Search engine</Text>
<SegmentedControl
fullWidth
title="Search engine"
value={
// Match config.settings.searchUrl with a key in the matches array
searchUrl
}
onChange={
// Set config.settings.searchUrl to the value of the selected item
(e) => {
setSearchUrl(e);
setConfig({
...config,
settings: {
...config.settings,
searchUrl: e,
},
});
}
}
data={matches}
/>
{searchUrl === 'Custom' && (
<TextInput
label="Query URL"
placeholder="Custom query url"
value={customSearchUrl}
onChange={(event) => {
setCustomSearchUrl(event.currentTarget.value);
setConfig({
...config,
settings: {
...config.settings,
searchUrl: event.currentTarget.value,
},
});
}}
/>
)}
</Group>
<ColorSchemeSwitch />
<WidgetsPositionSwitch />
<ModuleEnabler />
<ConfigChanger />
<SaveConfigComponent />
<Text
style={{
alignSelf: 'center',
fontSize: '0.75rem',
textAlign: 'center',
color: 'gray',
}}
>
Tip: You can upload your config file by dragging and dropping it onto the page!
</Text>
<Group position="center" direction="row" mr="xs">
<Group spacing={0}>
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
<BrandGithub size={18} />
</ActionIcon>
<Text
style={{
position: 'relative',
fontSize: '0.90rem',
color: 'gray',
}}
>
{CURRENT_VERSION}
</Text>
</Group>
<Text
style={{
fontSize: '0.90rem',
textAlign: 'center',
color: 'gray',
}}
>
Made with by @
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
ajnart
</Anchor>
</Text>
</Group>
</Group>
);
}

View File

@@ -0,0 +1,35 @@
import { Checkbox, Group, SimpleGrid, Title } from '@mantine/core';
import * as Modules from '../modules';
import { useConfig } from '../../tools/state';
export default function ModuleEnabler(props: any) {
const { config, setConfig } = useConfig();
const modules = Object.values(Modules).map((module) => module);
return (
<Group direction="column">
<Title order={4}>Module enabler</Title>
<SimpleGrid cols={2} spacing="md">
{modules.map((module) => (
<Checkbox
key={module.title}
size="md"
checked={config.modules?.[module.title]?.enabled ?? false}
label={`${module.title}`}
onChange={(e) => {
setConfig({
...config,
modules: {
...config.modules,
[module.title]: {
...config.modules?.[module.title],
enabled: e.currentTarget.checked,
},
},
});
}}
/>
))}
</SimpleGrid>
</Group>
);
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { Group, Text, Slider } from '@mantine/core';
import { useConfig } from '../../tools/state';
export function OpacitySelector() {
const { config, setConfig } = useConfig();
const MARKS = [
{ value: 10, label: '10' },
{ value: 20, label: '20' },
{ value: 30, label: '30' },
{ value: 40, label: '40' },
{ value: 50, label: '50' },
{ value: 60, label: '60' },
{ value: 70, label: '70' },
{ value: 80, label: '80' },
{ value: 90, label: '90' },
{ value: 100, label: '100' },
];
const setConfigOpacity = (opacity: number) => {
setConfig({
...config,
settings: {
...config.settings,
appOpacity: opacity,
},
});
};
return (
<Group direction="column" spacing="xs" grow>
<Text>App Opacity</Text>
<Slider
defaultValue={config.settings.appOpacity || 100}
step={10}
min={10}
marks={MARKS}
styles={{ markLabel: { fontSize: 'xx-small' } }}
onChange={(value) => setConfigOpacity(value)}
/>
</Group>
);
}

View File

@@ -0,0 +1,51 @@
import { ActionIcon, Title, Tooltip, Drawer, Tabs } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { useState } from 'react';
import { IconSettings } from '@tabler/icons';
import AdvancedSettings from './AdvancedSettings';
import CommonSettings from './CommonSettings';
function SettingsMenu(props: any) {
return (
<Tabs grow>
<Tabs.Tab data-autofocus label="Common">
<CommonSettings />
</Tabs.Tab>
<Tabs.Tab label="Customizations">
<AdvancedSettings />
</Tabs.Tab>
</Tabs>
);
}
export function SettingsMenuButton(props: any) {
useHotkeys([['ctrl+L', () => setOpened(!opened)]]);
const [opened, setOpened] = useState(false);
return (
<>
<Drawer
size="xl"
padding="xl"
position="right"
title={<Title order={3}>Settings</Title>}
opened={props.opened || opened}
onClose={() => setOpened(false)}
>
<SettingsMenu />
</Drawer>
<ActionIcon
variant="default"
radius="md"
size="xl"
color="blue"
style={props.style}
onClick={() => setOpened(true)}
>
<Tooltip label="Settings">
<IconSettings />
</Tooltip>
</ActionIcon>
</>
);
}

View File

@@ -0,0 +1,97 @@
import React, { useState } from 'react';
import { ColorSwatch, Group, Popover, Text, useMantineTheme, MantineTheme } from '@mantine/core';
import { useConfig } from '../../tools/state';
import { useColorTheme } from '../../tools/color';
export function ShadeSelector() {
const { config, setConfig } = useConfig();
const [opened, setOpened] = useState(false);
const { primaryColor, secondaryColor, primaryShade, setPrimaryShade } = useColorTheme();
const theme = useMantineTheme();
const primaryShades = theme.colors[primaryColor].map((s, i) => ({
swatch: theme.colors[primaryColor][i],
shade: i as MantineTheme['primaryShade'],
}));
const secondaryShades = theme.colors[secondaryColor].map((s, i) => ({
swatch: theme.colors[secondaryColor][i],
shade: i as MantineTheme['primaryShade'],
}));
const setConfigShade = (shade: MantineTheme['primaryShade']) => {
setPrimaryShade(shade);
setConfig({
...config,
settings: {
...config.settings,
primaryShade: shade,
},
});
};
const primarySwatches = primaryShades.map(({ swatch, shade }) => (
<ColorSwatch
component="button"
type="button"
onClick={() => setConfigShade(shade)}
key={Number(shade)}
color={swatch}
size={22}
style={{ color: theme.white, cursor: 'pointer' }}
/>
));
const secondarySwatches = secondaryShades.map(({ swatch, shade }) => (
<ColorSwatch
component="button"
type="button"
onClick={() => setConfigShade(shade)}
key={Number(shade)}
color={swatch}
size={22}
style={{ color: theme.white, cursor: 'pointer' }}
/>
));
return (
<Group direction="row" spacing={3}>
<Popover
opened={opened}
onClose={() => setOpened(false)}
transitionDuration={0}
target={
<ColorSwatch
component="button"
type="button"
color={theme.colors[primaryColor][Number(primaryShade)]}
onClick={() => setOpened((o) => !o)}
size={22}
style={{ display: 'block', cursor: 'pointer' }}
/>
}
styles={{
root: {
marginRight: theme.spacing.xs,
},
body: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white,
},
arrow: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white,
},
}}
position="bottom"
placement="end"
withArrow
arrowSize={3}
>
<Group direction="column" spacing="xs">
<Group spacing="xs">{primarySwatches}</Group>
<Group spacing="xs">{secondarySwatches}</Group>
</Group>
</Popover>
<Text>Shade</Text>
</Group>
);
}

View File

@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import { createStyles, Switch, Group } from '@mantine/core';
import { useConfig } from '../../tools/state';
const useStyles = createStyles((theme) => ({
root: {
position: 'relative',
'& *': {
cursor: 'pointer',
},
},
icon: {
pointerEvents: 'none',
position: 'absolute',
zIndex: 1,
top: 3,
},
iconLight: {
left: 4,
color: theme.white,
},
iconDark: {
right: 4,
color: theme.colors.gray[6],
},
}));
export function WidgetsPositionSwitch() {
const { config, setConfig } = useConfig();
const { classes, cx } = useStyles();
const defaultPosition = config?.settings?.widgetPosition || 'right';
const [widgetPosition, setWidgetPosition] = useState(defaultPosition);
const toggleWidgetPosition = () => {
const position = widgetPosition === 'right' ? 'left' : 'right';
setWidgetPosition(position);
setConfig({
...config,
settings: {
...config.settings,
widgetPosition: position,
},
});
};
return (
<Group>
<div className={classes.root}>
<Switch
checked={widgetPosition === 'left'}
onChange={() => toggleWidgetPosition()}
size="md"
/>
</div>
Position widgets on left
</Group>
);
}

View File

@@ -0,0 +1,36 @@
import { Aside as MantineAside, createStyles } from '@mantine/core';
import Widgets from './Widgets';
const useStyles = createStyles((theme) => ({
hide: {
[theme.fn.smallerThan('xs')]: {
display: 'none',
},
},
burger: {
[theme.fn.largerThan('sm')]: {
display: 'none',
},
},
}));
export default function Aside(props: any) {
const { classes, cx } = useStyles();
return (
<MantineAside
pr="md"
hiddenBreakpoint="sm"
hidden
className={cx(classes.hide)}
style={{
border: 'none',
background: 'none',
}}
width={{
base: 'auto',
}}
>
<Widgets />
</MantineAside>
);
}

View File

@@ -0,0 +1,20 @@
import { Global } from '@mantine/core';
import { useConfig } from '../../tools/state';
export function Background() {
const { config } = useConfig();
return (
<Global
styles={{
body: {
minHeight: '100vh',
backgroundImage: `url('${config.settings.background}')` || '',
backgroundPosition: 'center center',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
},
}}
/>
);
}

View File

@@ -0,0 +1,74 @@
import React, { useEffect } from 'react';
import { createStyles, Footer as FooterComponent } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconAlertCircle as AlertCircle } from '@tabler/icons';
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
const useStyles = createStyles((theme) => ({
footer: {
borderTop: `1px solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]
}`,
},
inner: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: `${theme.spacing.md}px ${theme.spacing.md}px`,
[theme.fn.smallerThan('sm')]: {
flexDirection: 'column',
},
},
links: {
[theme.fn.smallerThan('sm')]: {
marginTop: theme.spacing.lg,
marginBottom: theme.spacing.sm,
},
},
}));
interface FooterCenteredProps {
links: { link: string; label: string }[];
}
export function Footer({ links }: FooterCenteredProps) {
useEffect(() => {
// Fetch Data here when component first mounted
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
res.json().then((data) => {
if (data.tag_name > CURRENT_VERSION) {
showNotification({
color: 'yellow',
autoClose: false,
title: 'New version available',
icon: <AlertCircle />,
message: `Version ${data.tag_name} is available, update now!`,
});
} else if (data.tag_name < CURRENT_VERSION) {
showNotification({
color: 'orange',
autoClose: 5000,
title: 'You are using a development version',
icon: <AlertCircle />,
message: 'This version of Homarr is still in development! Bugs are expected 🐛',
});
}
});
});
}, []);
return (
<FooterComponent
height="auto"
style={{
background: 'none',
border: 'none',
clear: 'both',
}}
children={undefined}
/>
);
}

View File

@@ -0,0 +1,97 @@
import React from 'react';
import {
createStyles,
Header as Head,
Group,
Box,
Burger,
Drawer,
Title,
ScrollArea,
ActionIcon,
Transition,
} from '@mantine/core';
import { useBooleanToggle } from '@mantine/hooks';
import { Logo } from './Logo';
import SearchBar from '../modules/search/SearchModule';
import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem';
import { SettingsMenuButton } from '../Settings/SettingsMenu';
import { ModuleWrapper } from '../modules/moduleWrapper';
import { CalendarModule, TotalDownloadsModule, WeatherModule, DateModule } from '../modules';
const HEADER_HEIGHT = 60;
const useStyles = createStyles((theme) => ({
hide: {
[theme.fn.smallerThan('xs')]: {
display: 'none',
},
},
burger: {
[theme.fn.largerThan('sm')]: {
display: 'none',
},
},
}));
export function Header(props: any) {
const [opened, toggleOpened] = useBooleanToggle(false);
const { classes, cx } = useStyles();
const [hidden, toggleHidden] = useBooleanToggle(true);
return (
<Head height="auto">
<Group p="xs" position="apart">
<Box className={classes.hide}>
<Logo style={{ fontSize: 22 }} />
</Box>
<Group noWrap>
<SearchBar />
<SettingsMenuButton />
<AddItemShelfButton />
<ActionIcon className={classes.burger} variant="default" radius="md" size="xl">
<Burger
opened={!hidden}
onClick={(_) => {
toggleHidden();
toggleOpened();
}}
/>
</ActionIcon>
<Drawer
size="auto"
padding="xl"
position="right"
hidden={hidden}
title={<Title order={3}>Modules</Title>}
opened
onClose={() => {
toggleHidden();
}}
>
<Transition
mounted={opened}
transition="pop-top-right"
duration={300}
timingFunction="ease"
onExit={() => toggleOpened()}
>
{(styles) => (
<div style={styles}>
<ScrollArea style={{ height: '90vh' }}>
<Group my="sm" grow direction="column" style={{ width: 300 }}>
<ModuleWrapper module={CalendarModule} />
<ModuleWrapper module={TotalDownloadsModule} />
<ModuleWrapper module={WeatherModule} />
<ModuleWrapper module={DateModule} />
</Group>
</ScrollArea>
</div>
)}
</Transition>
</Drawer>
</Group>
</Group>
</Head>
);
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
import Head from 'next/head';
import { useConfig } from '../../tools/state';
export function HeaderConfig(props: any) {
const { config } = useConfig();
return (
<Head>
<title>{config.settings.title || 'Homarr 🦞'}</title>
<link rel="shortcut icon" href={config.settings.favicon || '/favicon.svg'} />
</Head>
);
}

View File

@@ -0,0 +1,38 @@
import { AppShell, createStyles } from '@mantine/core';
import { Header } from './Header';
import { Footer } from './Footer';
import Aside from './Aside';
import Navbar from './Navbar';
import { HeaderConfig } from './HeaderConfig';
import { Background } from './Background';
import { useConfig } from '../../tools/state';
const useStyles = createStyles((theme) => ({
main: {},
}));
export default function Layout({ children, style }: any) {
const { classes, cx } = useStyles();
const { config } = useConfig();
const widgetPosition = config?.settings?.widgetPosition === 'left';
return (
<AppShell
header={<Header />}
navbar={widgetPosition ? <Navbar /> : <></>}
aside={widgetPosition ? <></> : <Aside />}
footer={<Footer links={[]} />}
>
<HeaderConfig />
<Background />
<main
className={cx(classes.main)}
style={{
...style,
}}
>
{children}
</main>
</AppShell>
);
}

View File

@@ -0,0 +1,42 @@
import { Group, Image, Text } from '@mantine/core';
import { NextLink } from '@mantine/next';
import * as React from 'react';
import { useColorTheme } from '../../tools/color';
import { useConfig } from '../../tools/state';
export function Logo({ style }: any) {
const { config } = useConfig();
const { primaryColor, secondaryColor } = useColorTheme();
return (
<Group spacing="xs">
<Image
width={50}
src={config.settings.logo || '/imgs/logo.png'}
style={{
position: 'relative',
}}
/>
<NextLink
href="/"
style={{
textDecoration: 'none',
position: 'relative',
}}
>
<Text
sx={style}
weight="bold"
variant="gradient"
gradient={{
from: primaryColor,
to: secondaryColor,
deg: 145,
}}
>
{config.settings.title || 'Homarr'}
</Text>
</NextLink>
</Group>
);
}

View File

@@ -0,0 +1,37 @@
import { createStyles, Navbar as MantineNavbar } from '@mantine/core';
import Widgets from './Widgets';
const useStyles = createStyles((theme) => ({
hide: {
[theme.fn.smallerThan('xs')]: {
display: 'none',
},
},
burger: {
[theme.fn.largerThan('sm')]: {
display: 'none',
},
},
}));
export default function Navbar() {
const { classes, cx } = useStyles();
return (
<MantineNavbar
pl="md"
hiddenBreakpoint="sm"
hidden
className={cx(classes.hide)}
style={{
border: 'none',
background: 'none',
}}
width={{
base: 'auto',
}}
>
<Widgets />
</MantineNavbar>
);
}

View File

@@ -0,0 +1,21 @@
import { Group } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules';
import { ModuleWrapper } from '../modules/moduleWrapper';
export default function Widgets(props: any) {
const matches = useMediaQuery('(min-width: 800px)');
return (
<>
{matches && (
<Group my="sm" grow direction="column" style={{ width: 300 }}>
<ModuleWrapper module={CalendarModule} />
<ModuleWrapper module={TotalDownloadsModule} />
<ModuleWrapper module={WeatherModule} />
<ModuleWrapper module={DateModule} />
</Group>
)}
</>
);
}

View File

@@ -0,0 +1,306 @@
/* eslint-disable react/no-children-prop */
import {
Box,
Divider,
Indicator,
Popover,
ScrollArea,
createStyles,
useMantineTheme,
} from '@mantine/core';
import React, { useEffect, useState } from 'react';
import { Calendar } from '@mantine/dates';
import { IconCalendar as CalendarIcon } from '@tabler/icons';
import axios from 'axios';
import { useConfig } from '../../../tools/state';
import { IModule } from '../modules';
import {
SonarrMediaDisplay,
RadarrMediaDisplay,
LidarrMediaDisplay,
ReadarrMediaDisplay,
} from '../common';
import { serviceItem } from '../../../tools/types';
import { useColorTheme } from '../../../tools/color';
export const CalendarModule: IModule = {
title: 'Calendar',
description:
'A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.',
icon: CalendarIcon,
component: CalendarComponent,
options: {
sundaystart: {
name: 'Start the week on Sunday',
value: false,
},
},
};
export default function CalendarComponent(props: any) {
const { config } = useConfig();
const theme = useMantineTheme();
const { secondaryColor } = useColorTheme();
const useStyles = createStyles((theme) => ({
weekend: {
color: `${secondaryColor} !important`,
},
}));
const [sonarrMedias, setSonarrMedias] = useState([] as any);
const [lidarrMedias, setLidarrMedias] = useState([] as any);
const [radarrMedias, setRadarrMedias] = useState([] as any);
const [readarrMedias, setReadarrMedias] = useState([] as any);
const sonarrServices = config.services.filter((service) => service.type === 'Sonarr');
const radarrServices = config.services.filter((service) => service.type === 'Radarr');
const lidarrServices = config.services.filter((service) => service.type === 'Lidarr');
const readarrServices = config.services.filter((service) => service.type === 'Readarr');
const today = new Date();
const { classes, cx } = useStyles();
function getMedias(service: serviceItem | undefined, type: string) {
if (!service || !service.apiKey) {
return Promise.resolve({ data: [] });
}
return axios.post(`/api/modules/calendar?type=${type}`, { ...service });
}
useEffect(() => {
// Create each Sonarr service and get the medias
const currentSonarrMedias: any[] = [];
Promise.all(
sonarrServices.map((service) =>
getMedias(service, 'sonarr').then((res) => {
currentSonarrMedias.push(...res.data);
}).catch(() => {
currentSonarrMedias.push([]);
})
)
).then(() => {
setSonarrMedias(currentSonarrMedias);
});
const currentRadarrMedias: any[] = [];
Promise.all(
radarrServices.map((service) =>
getMedias(service, 'radarr').then((res) => {
currentRadarrMedias.push(...res.data);
}).catch(() => {
currentRadarrMedias.push([]);
})
)
).then(() => {
setRadarrMedias(currentRadarrMedias);
});
const currentLidarrMedias: any[] = [];
Promise.all(
lidarrServices.map((service) =>
getMedias(service, 'lidarr').then((res) => {
currentLidarrMedias.push(...res.data);
}).catch(() => {
currentLidarrMedias.push([]);
})
)
).then(() => {
setLidarrMedias(currentLidarrMedias);
});
const currentReadarrMedias: any[] = [];
Promise.all(
readarrServices.map((service) =>
getMedias(service, 'readarr').then((res) => {
currentReadarrMedias.push(...res.data);
}).catch(() => {
currentReadarrMedias.push([]);
})
)
).then(() => {
setReadarrMedias(currentReadarrMedias);
});
}, [config.services]);
const weekStartsAtSunday =
(config?.modules?.[CalendarModule.title]?.options?.sundaystart?.value as boolean) ?? false;
return (
<Calendar
firstDayOfWeek={weekStartsAtSunday ? 'sunday' : 'monday'}
onChange={(day: any) => {}}
dayStyle={(date) =>
date.getDay() === today.getDay() && date.getDate() === today.getDate()
? {
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[0],
}
: {}
}
styles={{
calendarHeader: {
marginRight: 15,
marginLeft: 15,
},
}}
allowLevelChange={false}
dayClassName={(date, modifiers) => cx({ [classes.weekend]: modifiers.weekend })}
renderDay={(renderdate) => (
<DayComponent
renderdate={renderdate}
sonarrmedias={sonarrMedias}
radarrmedias={radarrMedias}
lidarrmedias={lidarrMedias}
readarrmedias={readarrMedias}
/>
)}
/>
);
}
function DayComponent(props: any) {
const {
renderdate,
sonarrmedias,
radarrmedias,
lidarrmedias,
readarrmedias,
}: { renderdate: Date; sonarrmedias: []; radarrmedias: []; lidarrmedias: []; readarrmedias: [] } =
props;
const [opened, setOpened] = useState(false);
const day = renderdate.getDate();
const readarrFiltered = readarrmedias.filter((media: any) => {
const date = new Date(media.releaseDate);
return date.toDateString() === renderdate.toDateString();
});
const lidarrFiltered = lidarrmedias.filter((media: any) => {
const date = new Date(media.releaseDate);
return date.toDateString() === renderdate.toDateString();
});
const sonarrFiltered = sonarrmedias.filter((media: any) => {
const date = new Date(media.airDateUtc);
return date.toDateString() === renderdate.toDateString();
});
const radarrFiltered = radarrmedias.filter((media: any) => {
const date = new Date(media.inCinemas);
return date.toDateString() === renderdate.toDateString();
});
if (
sonarrFiltered.length === 0 &&
radarrFiltered.length === 0 &&
lidarrFiltered.length === 0 &&
readarrFiltered.length === 0
) {
return <div>{day}</div>;
}
return (
<Box
onClick={() => {
setOpened(true);
}}
>
{readarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
bottom: 8,
left: 8,
}}
color="red"
children={null}
/>
)}
{radarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
top: 8,
left: 8,
}}
color="yellow"
children={null}
/>
)}
{sonarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
top: 8,
right: 8,
}}
color="blue"
children={null}
/>
)}
{lidarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
bottom: 8,
right: 8,
}}
color="green"
children={undefined}
/>
)}
<Popover
position="bottom"
radius="lg"
shadow="xl"
transition="pop"
styles={{
body: {
boxShadow: '0 0 14px 14px rgba(0, 0, 0, 0.1), 0 14px 11px rgba(0, 0, 0, 0.1)',
},
}}
width="auto"
onClose={() => setOpened(false)}
opened={opened}
target={day}
>
<ScrollArea style={{ height: 400 }}>
{sonarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<SonarrMediaDisplay media={media} />
{index < sonarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
{radarrFiltered.length > 0 && sonarrFiltered.length > 0 && (
<Divider variant="dashed" my="xl" />
)}
{radarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<RadarrMediaDisplay media={media} />
{index < radarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
{sonarrFiltered.length > 0 && lidarrFiltered.length > 0 && (
<Divider variant="dashed" my="xl" />
)}
{lidarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<LidarrMediaDisplay media={media} />
{index < lidarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
{lidarrFiltered.length > 0 && readarrFiltered.length > 0 && (
<Divider variant="dashed" my="xl" />
)}
{readarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<ReadarrMediaDisplay media={media} />
{index < readarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
</ScrollArea>
</Popover>
</Box>
);
}

View File

@@ -0,0 +1,193 @@
import {
Image,
Group,
Title,
Badge,
Text,
ActionIcon,
Anchor,
ScrollArea,
createStyles,
} from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { IconLink as Link } from '@tabler/icons';
import { useConfig } from '../../../tools/state';
import { serviceItem } from '../../../tools/types';
export interface IMedia {
overview: string;
imdbId?: any;
artist?: string;
title: string;
poster?: string;
genres: string[];
seasonNumber?: number;
episodeNumber?: number;
}
const useStyles = createStyles((theme) => ({
overview: {
[theme.fn.largerThan('sm')]: {
width: 400,
},
},
}));
export function MediaDisplay(props: { media: IMedia }) {
const { media }: { media: IMedia } = props;
const { classes, cx } = useStyles();
const phone = useMediaQuery('(min-width: 800px)');
return (
<Group position="apart">
<Text>
{media.poster && (
<Image
width={phone ? 250 : 100}
height={phone ? 400 : 160}
style={{
float: 'right',
}}
radius="md"
fit="cover"
src={media.poster}
alt={media.title}
/>
)}
<Group direction="column" style={{ minWidth: phone ? 450 : '65vw' }}>
<Group noWrap mr="sm" className={classes.overview}>
<Title order={3}>{media.title}</Title>
{media.imdbId && (
<Anchor href={`https://www.imdb.com/title/${media.imdbId}`} target="_blank">
<ActionIcon>
<Link />
</ActionIcon>
</Anchor>
)}
</Group>
{media.artist && (
<Text
style={{
textAlign: 'center',
color: 'gray',
}}
>
New release from {media.artist}
</Text>
)}
{media.episodeNumber && media.seasonNumber && (
<Text
style={{
textAlign: 'center',
color: 'gray',
}}
>
Season {media.seasonNumber} episode {media.episodeNumber}
</Text>
)}
</Group>
<Group direction="column" position="apart">
<ScrollArea style={{ height: 280, maxWidth: 700 }}>{media.overview}</ScrollArea>
<Group align="center" position="center" spacing="xs">
{media.genres.slice(-5).map((genre: string, i: number) => (
<Badge size="sm" key={i}>
{genre}
</Badge>
))}
</Group>
</Group>
</Text>
</Group>
);
}
export function ReadarrMediaDisplay(props: any) {
const { media }: { media: any } = props;
const { config } = useConfig();
// Find lidarr in services
const readarr = config.services.find((service: serviceItem) => service.type === 'Readarr');
// Find a poster CoverType
const poster = media.images.find((image: any) => image.coverType === 'cover');
if (!readarr) {
return null;
}
const baseUrl = new URL(readarr.url).origin;
// Remove '/' from the end of the lidarr url
const fullLink = `${baseUrl}${poster.url}`;
// Return a movie poster containting the title and the description
return (
<MediaDisplay
media={{
title: media.title,
poster: fullLink,
artist: media.author.authorName,
overview: media.overview,
genres: media.genres,
}}
/>
);
}
export function LidarrMediaDisplay(props: any) {
const { media }: { media: any } = props;
const { config } = useConfig();
// Find lidarr in services
const lidarr = config.services.find((service: serviceItem) => service.type === 'Lidarr');
// Find a poster CoverType
const poster = media.images.find((image: any) => image.coverType === 'cover');
if (!lidarr) {
return null;
}
const baseUrl = new URL(lidarr.url).origin;
// Remove '/' from the end of the lidarr url
const fullLink = poster ? `${baseUrl}${poster.url}` : undefined;
// Return a movie poster containting the title and the description
return (
<MediaDisplay
media={{
title: media.title,
poster: fullLink,
artist: media.artist.artistName,
overview: media.overview,
genres: media.genres,
}}
/>
);
}
export function RadarrMediaDisplay(props: any) {
const { media }: { media: any } = props;
// Find a poster CoverType
const poster = media.images.find((image: any) => image.coverType === 'poster');
// Return a movie poster containting the title and the description
return (
<MediaDisplay
media={{
imdbId: media.imdbId,
title: media.title,
overview: media.overview,
poster: poster.url,
genres: media.genres,
}}
/>
);
}
export function SonarrMediaDisplay(props: any) {
const { media }: { media: any } = props;
// Find a poster CoverType
const poster = media.series.images.find((image: any) => image.coverType === 'poster');
// Return a movie poster containting the title and the description
return (
<MediaDisplay
media={{
imdbId: media.series.imdbId,
title: media.series.title,
overview: media.series.overview,
poster: poster.url,
genres: media.series.genres,
seasonNumber: media.seasonNumber,
episodeNumber: media.episodeNumber,
}}
/>
);
}

View File

@@ -0,0 +1 @@
export * from './MediaDisplay';

View File

@@ -0,0 +1,42 @@
import { Group, Text, Title } from '@mantine/core';
import dayjs from 'dayjs';
import { useEffect, useState } from 'react';
import { IconClock as Clock } from '@tabler/icons';
import { useConfig } from '../../../tools/state';
import { IModule } from '../modules';
import { useSetSafeInterval } from '../../../tools/hooks/useSetSafeInterval';
export const DateModule: IModule = {
title: 'Date',
description: 'Show the current time and date in a card',
icon: Clock,
component: DateComponent,
options: {
full: {
name: 'Display full time (24-hour)',
value: true,
},
},
};
export default function DateComponent(props: any) {
const [date, setDate] = useState(new Date());
const setSafeInterval = useSetSafeInterval();
const { config } = useConfig();
const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? false;
const formatString = isFullTime ? 'HH:mm' : 'h:mm A';
// Change date on minute change
// Note: Using 10 000ms instead of 1000ms to chill a little :)
useEffect(() => {
setSafeInterval(() => {
setDate(new Date());
}, 1000 * 60);
}, []);
return (
<Group p="sm" spacing="xs" direction="column">
<Title>{dayjs(date).format(formatString)}</Title>
<Text size="xl">{dayjs(date).format('dddd, MMMM D')}</Text>
</Group>
);
}

View File

@@ -0,0 +1,190 @@
import {
Table,
Text,
Tooltip,
Title,
Group,
Progress,
Skeleton,
ScrollArea,
Center,
Image,
} from '@mantine/core';
import { IconDownload as Download } from '@tabler/icons';
import { useEffect, useState } from 'react';
import axios from 'axios';
import { NormalizedTorrent } from '@ctrl/shared-torrent';
import { useViewportSize } from '@mantine/hooks';
import { IModule } from '../modules';
import { useConfig } from '../../../tools/state';
import { AddItemShelfButton } from '../../AppShelf/AddAppShelfItem';
import { useSetSafeInterval } from '../../../tools/hooks/useSetSafeInterval';
import { humanFileSize } from '../../../tools/humanFileSize';
export const DownloadsModule: IModule = {
title: 'Torrent',
description: 'Show the current download speed of supported services',
icon: Download,
component: DownloadComponent,
options: {
hidecomplete: {
name: 'Hide completed torrents',
value: false,
},
},
};
export default function DownloadComponent() {
const { config } = useConfig();
const { height, width } = useViewportSize();
const downloadServices =
config.services.filter(
(service) =>
service.type === 'qBittorrent' ||
service.type === 'Transmission' ||
service.type === 'Deluge'
) ?? [];
const hideComplete: boolean =
(config?.modules?.[DownloadsModule.title]?.options?.hidecomplete?.value as boolean) ?? false;
const [torrents, setTorrents] = useState<NormalizedTorrent[]>([]);
const setSafeInterval = useSetSafeInterval();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
if (downloadServices.length === 0) return;
setSafeInterval(() => {
// Send one request with each download service inside
axios.post('/api/modules/downloads', { config }).then((response) => {
setTorrents(response.data);
setIsLoading(false);
});
}, 5000);
}, [config.services]);
if (downloadServices.length === 0) {
return (
<Group direction="column">
<Title order={3}>No supported download clients found!</Title>
<Group>
<Text>Add a download service to view your current downloads...</Text>
<AddItemShelfButton />
</Group>
</Group>
);
}
if (isLoading) {
return (
<>
<Skeleton height={40} mt={10} />
<Skeleton height={40} mt={10} />
<Skeleton height={40} mt={10} />
<Skeleton height={40} mt={10} />
<Skeleton height={40} mt={10} />
</>
);
}
const DEVICE_WIDTH = 576;
const ths = (
<tr>
<th>Name</th>
<th>Size</th>
{width > 576 ? <th>Down</th> : ''}
{width > 576 ? <th>Up</th> : ''}
<th>ETA</th>
<th>Progress</th>
</tr>
);
// Convert Seconds to readable format.
function calculateETA(givenSeconds: number) {
// If its superior than one day return > 1 day
if (givenSeconds > 86400) {
return '> 1 day';
}
// Transform the givenSeconds into a readable format. e.g. 1h 2m 3s
const hours = Math.floor(givenSeconds / 3600);
const minutes = Math.floor((givenSeconds % 3600) / 60);
const seconds = Math.floor(givenSeconds % 60);
// Only show hours if it's greater than 0.
const hoursString = hours > 0 ? `${hours}h ` : '';
const minutesString = minutes > 0 ? `${minutes}m ` : '';
const secondsString = seconds > 0 ? `${seconds}s` : '';
return `${hoursString}${minutesString}${secondsString}`;
}
// Loop over qBittorrent torrents merging with deluge torrents
const rows = torrents
.filter((torrent) => !(torrent.progress === 1 && hideComplete))
.map((torrent) => {
const downloadSpeed = torrent.downloadSpeed / 1024 / 1024;
const uploadSpeed = torrent.uploadSpeed / 1024 / 1024;
const size = torrent.totalSelected;
return (
<tr key={torrent.id}>
<td>
<Tooltip position="top" label={torrent.name}>
<Text
style={{
maxWidth: '30vw',
}}
size="xs"
lineClamp={1}
>
{torrent.name}
</Text>
</Tooltip>
</td>
<td>
<Text size="xs">{humanFileSize(size)}</Text>
</td>
{width > 576 ? (
<td>
<Text size="xs">{downloadSpeed > 0 ? `${downloadSpeed.toFixed(1)} Mb/s` : '-'}</Text>
</td>
) : (
''
)}
{width > 576 ? (
<td>
<Text size="xs">{uploadSpeed > 0 ? `${uploadSpeed.toFixed(1)} Mb/s` : '-'}</Text>
</td>
) : (
''
)}
<td>
<Text size="xs">{torrent.eta <= 0 ? '∞' : calculateETA(torrent.eta)}</Text>
</td>
<td>
<Text>{(torrent.progress * 100).toFixed(1)}%</Text>
<Progress
radius="lg"
color={
torrent.progress === 1 ? 'green' : torrent.state === 'paused' ? 'yellow' : 'blue'
}
value={torrent.progress * 100}
size="lg"
/>
</td>
</tr>
);
});
const easteregg = (
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Image fit="cover" height={300} src="https://danjohnvelasco.github.io/images/empty.png" />
</Center>
);
return (
<Group noWrap grow direction="column" mt="xl">
<ScrollArea sx={{ height: 300 }}>
{rows.length > 0 ? (
<Table highlightOnHover>
<thead>{ths}</thead>
<tbody>{rows}</tbody>
</Table>
) : (
easteregg
)}
</ScrollArea>
</Group>
);
}

Some files were not shown because too many files have changed in this diff Show More