Full Code of lamarios/Homedash2 for AI

master 6ffce89b4c41
420 files
1.5 MB
394.7k tokens
Showing preview only (1,626K chars total). The displayed content is truncated. Use the JSON API for full output.
Repository: lamarios/Homedash2
Branch: master
Commit: 6ffce89b4c41
Files: 420
Total size: 1.5 MB

Directory structure:
gitextract_7__wbq9_/

├── .drone.yml
├── .gitattributes
├── .gitignore
├── DevelopPlugin.md
├── LICENSE
├── README.md
├── docs/
│   ├── index.html
│   └── style.css
├── models/
│   ├── .gitignore
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   └── java/
│       │       └── com/
│       │           └── ftpix/
│       │               └── homedash/
│       │                   ├── Utils/
│       │                   │   ├── ByteUtils.java
│       │                   │   └── HomeDashClassPathTemplateLoader.java
│       │                   ├── models/
│       │                   │   ├── ExposedModule.java
│       │                   │   ├── ExternalEndPointDefinition.java
│       │                   │   ├── Layout.java
│       │                   │   ├── Module.java
│       │                   │   ├── ModuleData.java
│       │                   │   ├── ModuleExposedData.java
│       │                   │   ├── ModuleLayout.java
│       │                   │   ├── ModuleLocation.java
│       │                   │   ├── ModuleSettings.java
│       │                   │   ├── Page.java
│       │                   │   ├── RemoteFavorite.java
│       │                   │   ├── Schema.java
│       │                   │   ├── Settings.java
│       │                   │   ├── Version.java
│       │                   │   ├── WebSocketMessage.java
│       │                   │   ├── WebSocketSession.java
│       │                   │   └── export/
│       │                   │       ├── Export.java
│       │                   │       ├── LayoutExport.java
│       │                   │       ├── ModuleExport.java
│       │                   │       ├── ModuleLayoutExport.java
│       │                   │       └── PageExport.java
│       │                   └── plugins/
│       │                       ├── Plugin.java
│       │                       └── PluginListener.java
│       └── test/
│           └── java/
│               └── com/
│                   └── ftpix/
│                       └── homedash/
│                           └── plugins/
│                               └── AppTest.java
├── notifications/
│   ├── pom.xml
│   └── src/
│       └── main/
│           └── java/
│               └── com/
│                   └── ftpix/
│                       └── homedash/
│                           └── notifications/
│                               ├── NotificationProvider.java
│                               ├── Notifications.java
│                               └── implementations/
│                                   ├── PushBullet.java
│                                   ├── PushOver.java
│                                   └── Pushalot.java
├── plugins/
│   ├── couchpotato/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── couchpotato.js
│   │               │   ├── less/
│   │               │   │   └── couchpotato.less
│   │               │   └── templates/
│   │               │       ├── couchpotato-1x1.jade
│   │               │       ├── couchpotato-1x3.jade
│   │               │       ├── couchpotato-2x1.jade
│   │               │       ├── couchpotato-2x2.jade
│   │               │       ├── couchpotato-2x3.jade
│   │               │       ├── couchpotato-3x2.jade
│   │               │       ├── couchpotato-3x3.jade
│   │               │       ├── couchpotato-settings.jade
│   │               │       └── cp-modal.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── couchpotato/
│   │                                   ├── CouchPotatoPlugin.java
│   │                                   ├── apis/
│   │                                   │   ├── CouchPotatoApi.java
│   │                                   │   ├── MovieProviderAPI.java
│   │                                   │   ├── RadarrApi.java
│   │                                   │   └── radarr/
│   │                                   │       ├── RadarrImage.java
│   │                                   │       └── RadarrMovieRequest.java
│   │                                   └── models/
│   │                                       ├── ImagePath.java
│   │                                       ├── MovieObject.java
│   │                                       ├── MovieProviderBuilder.java
│   │                                       ├── MovieRequest.java
│   │                                       ├── MoviesRootFolder.java
│   │                                       ├── QualityProfile.java
│   │                                       └── Type.java
│   ├── docker/
│   │   ├── .gitignore
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── docker.js
│   │               │   ├── less/
│   │               │   │   └── docker.less
│   │               │   └── templates/
│   │               │       ├── docker-1x1.jade
│   │               │       ├── docker-full-screen.jade
│   │               │       ├── docker-image-modal.jade
│   │               │       ├── docker-logs-modal.jade
│   │               │       ├── docker-modal.jade
│   │               │       └── docker-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── docker/
│   │                                   ├── DockerPlugin.java
│   │                                   └── models/
│   │                                       ├── DockerExtendedInfo.java
│   │                                       ├── DockerImageInfo.java
│   │                                       └── DockerInfo.java
│   ├── dockercompose/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── dockercompose.js
│   │               │   ├── less/
│   │               │   │   └── dockercompose.less
│   │               │   └── templates/
│   │               │       ├── dockercompose-1x1.jade
│   │               │       ├── dockercompose-2x1.jade
│   │               │       ├── dockercompose-full-screen.jade
│   │               │       ├── dockercompose-modal.jade
│   │               │       └── dockercompose-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── dockercompose/
│   │                                   ├── DockerComposePlugin.java
│   │                                   ├── exceptions/
│   │                                   │   └── CommandException.java
│   │                                   └── models/
│   │                                       ├── CommandOutput.java
│   │                                       └── DockerContainer.java
│   ├── dynamicdns/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── dynamicdns.js
│   │               │   ├── less/
│   │               │   │   └── dynamicdns.less
│   │               │   └── templates/
│   │               │       ├── dynamicdns-2x1.jade
│   │               │       └── dynamicdns-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── dynamicdns/
│   │                                   ├── DynamicDnsPlugin.java
│   │                                   ├── inputs/
│   │                                   │   ├── FormInput.java
│   │                                   │   └── FormType.java
│   │                                   ├── models/
│   │                                   │   ├── Ip.java
│   │                                   │   └── IpFromWeb.java
│   │                                   └── providers/
│   │                                       ├── DynDNSProvider.java
│   │                                       └── implementations/
│   │                                           ├── DynDNS.java
│   │                                           ├── NoIP.java
│   │                                           ├── OVH.java
│   │                                           └── StandardProvider.java
│   ├── googlepubliccalendar/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── googlepubliccalendar.js
│   │               │   ├── less/
│   │               │   │   └── googlepubliccalendar.less
│   │               │   └── templates/
│   │               │       ├── googlepubliccalendar-3x1.jade
│   │               │       ├── googlepubliccalendar-3x4.jade
│   │               │       ├── googlepubliccalendar-4x4.jade
│   │               │       ├── googlepubliccalendar-full-screen.jade
│   │               │       ├── googlepubliccalendar-modal.jade
│   │               │       └── googlepubliccalendar-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── googlepubliccalendar/
│   │                                   └── GooglePublicCalendarPlugin.java
│   ├── harddisk/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── harddisk.js
│   │               │   ├── less/
│   │               │   │   └── harddisk.less
│   │               │   └── templates/
│   │               │       ├── harddisk-1x1.jade
│   │               │       ├── harddisk-2x1.jade
│   │               │       ├── harddisk-full-screen.jade
│   │               │       ├── harddisk-kiosk.jade
│   │               │       └── harddisk-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── harddisk/
│   │                                   ├── DiskFile.java
│   │                                   ├── FileOperation.java
│   │                                   ├── HarddiskPlugin.java
│   │                                   └── OperationException.java
│   ├── kvm/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── kvm.js
│   │               │   ├── less/
│   │               │   │   └── kvm.less
│   │               │   └── templates/
│   │               │       ├── kvm-1x1.jade
│   │               │       ├── kvm-full-screen.jade
│   │               │       └── kvm-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── kvm/
│   │                                   ├── KvmPlugin.java
│   │                                   ├── VMAction.java
│   │                                   ├── VMActionResponse.java
│   │                                   └── VMInfo.java
│   ├── logreader/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── logreader.js
│   │               │   ├── less/
│   │               │   │   └── logreader.less
│   │               │   └── templates/
│   │               │       ├── logreader-1x1.jade
│   │               │       ├── logreader-full-screen.jade
│   │               │       └── logreader-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── logreader/
│   │                           └── LogReaderPlugin.java
│   ├── mma/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── mma.js
│   │               │   ├── less/
│   │               │   │   └── mma.less
│   │               │   └── templates/
│   │               │       ├── mma-3x2.jade
│   │               │       ├── mma-3x4.jade
│   │               │       ├── mma-4x4.jade
│   │               │       ├── mma-full-screen.jade
│   │               │       ├── mma-kiosk.jade
│   │               │       ├── mma-modal.jade
│   │               │       └── mma-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── mma/
│   │                                   ├── MmaPlugin.java
│   │                                   └── model/
│   │                                       ├── HomeDashEvent.java
│   │                                       └── HomeDashOrganization.java
│   ├── networkmonitor/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── networkmonitor.js
│   │               │   ├── less/
│   │               │   │   └── networkmonitor.less
│   │               │   └── templates/
│   │               │       ├── graph.jade
│   │               │       ├── networkmonitor-2x1.jade
│   │               │       ├── networkmonitor-3x2.jade
│   │               │       ├── networkmonitor-kiosk.jade
│   │               │       └── networkmonitor-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── networkmonitor/
│   │                                   ├── NetworkMonitorPlugin.java
│   │                                   └── models/
│   │                                       └── NetworkInfo.java
│   ├── pihole/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── pihole.js
│   │               │   ├── less/
│   │               │   │   └── pihole.less
│   │               │   └── templates/
│   │               │       ├── pihole-1x1.jade
│   │               │       ├── pihole-2x2.jade
│   │               │       ├── pihole-3x2.jade
│   │               │       ├── pihole-4x2.jade
│   │               │       ├── pihole-full-screen.jade
│   │               │       ├── pihole-kiosk.jade
│   │               │       └── pihole-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── pihole/
│   │                                   ├── PiHoleClient.java
│   │                                   ├── PiHolePlugin.java
│   │                                   ├── UnauthorizedException.java
│   │                                   └── models/
│   │                                       ├── AnswerType.java
│   │                                       ├── PiHoleQuery.java
│   │                                       └── PiHoleStats.java
│   ├── plex/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── plex.js
│   │               │   ├── less/
│   │               │   │   └── plex.less
│   │               │   └── templates/
│   │               │       ├── plex-1x1.jade
│   │               │       ├── plex-2x1.jade
│   │               │       ├── plex-3x2.jade
│   │               │       ├── plex-3x3.jade
│   │               │       ├── plex-4x4.jade
│   │               │       ├── plex-kiosk.jade
│   │               │       └── plex-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugin/
│   │                               └── plex/
│   │                                   ├── PlexPlugin.java
│   │                                   ├── PlexResultParser.java
│   │                                   ├── api/
│   │                                   │   ├── ApiType.java
│   │                                   │   ├── MediaServerApi.java
│   │                                   │   └── impl/
│   │                                   │       ├── JellyFinApi.java
│   │                                   │       └── PlexApi.java
│   │                                   └── model/
│   │                                       ├── JellyfinNowPlaying.java
│   │                                       ├── JellyfinPlayState.java
│   │                                       ├── JellyfinSession.java
│   │                                       ├── MediaContainer.java
│   │                                       ├── NowPlaying.java
│   │                                       ├── Player.java
│   │                                       ├── PlexSession.java
│   │                                       └── Video.java
│   ├── pom.xml
│   ├── portmapper/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── portmapper.js
│   │               │   ├── less/
│   │               │   │   └── portmapper.less
│   │               │   └── templates/
│   │               │       ├── pm-modal.jade
│   │               │       ├── portmapper-2x1.jade
│   │               │       ├── portmapper-5x5.jade
│   │               │       ├── portmapper-6x5.jade
│   │               │       └── portmapper-full-screen.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── portmapper/
│   │                                   ├── MappingObject.java
│   │                                   ├── PortMapperPlugin.java
│   │                                   └── RouterObject.java
│   ├── sonarrtv/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── sonarrtv.js
│   │               │   ├── less/
│   │               │   │   └── sonarrtv.less
│   │               │   └── templates/
│   │               │       ├── sonarrtv-2x2.jade
│   │               │       ├── sonarrtv-3x1.jade
│   │               │       ├── sonarrtv-3x3.jade
│   │               │       ├── sonarrtv-4x4.jade
│   │               │       ├── sonarrtv-full-screen.jade
│   │               │       ├── sonarrtv-kiosk.jade
│   │               │       └── sonarrtv-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               ├── SonarrTvPlugin.java
│   │                               └── api/
│   │                                   ├── SonarrApi.java
│   │                                   ├── SonarrUnauthorizedException.java
│   │                                   └── models/
│   │                                       ├── SearchResults.java
│   │                                       └── SonarrCalendar.java
│   ├── spotify/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── spotify.js
│   │               │   ├── less/
│   │               │   │   └── spotify.less
│   │               │   └── templates/
│   │               │       ├── spotify-1x1.jade
│   │               │       ├── spotify-2x1.jade
│   │               │       ├── spotify-2x2.jade
│   │               │       ├── spotify-3x2.jade
│   │               │       ├── spotify-3x3.jade
│   │               │       ├── spotify-4x4.jade
│   │               │       ├── spotify-kiosk.jade
│   │               │       └── spotify-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── spotify/
│   │                                   ├── SpotifyPlugin.java
│   │                                   ├── SpotifyToken.java
│   │                                   └── models/
│   │                                       ├── Album.java
│   │                                       ├── Artist.java
│   │                                       ├── Error.java
│   │                                       ├── Image.java
│   │                                       ├── Item.java
│   │                                       └── SpotifyNowPlaying.java
│   ├── systeminfo/
│   │   ├── .gitignore
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── systeminfo.js
│   │               │   ├── less/
│   │               │   │   └── systeminfo.less
│   │               │   └── templates/
│   │               │       ├── systeminfo-1x1.jade
│   │               │       ├── systeminfo-2x1.jade
│   │               │       ├── systeminfo-full-screen.jade
│   │               │       ├── systeminfo-kiosk.jade
│   │               │       └── systeminfo-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               ├── SystemInfoPlugin.java
│   │                               └── models/
│   │                                   ├── CpuInfo.java
│   │                                   ├── HardwareInfo.java
│   │                                   ├── OsInfo.java
│   │                                   ├── Process.java
│   │                                   ├── RamInfo.java
│   │                                   └── SystemInfoData.java
│   ├── transmission/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── transmission.js
│   │               │   ├── less/
│   │               │   │   └── transmission.less
│   │               │   └── templates/
│   │               │       ├── transmission-2x1.jade
│   │               │       ├── transmission-2x2.jade
│   │               │       ├── transmission-3x2.jade
│   │               │       ├── transmission-full-screen.jade
│   │               │       ├── transmission-kiosk.jade
│   │               │       └── transmission-settings.jade
│   │               ├── ca/
│   │               │   └── benow/
│   │               │       └── transmission/
│   │               │           ├── AddTorrentParameters.java
│   │               │           ├── Base64.java
│   │               │           ├── SetTorrentParameters.java
│   │               │           ├── TorrentParameters.java
│   │               │           ├── TransmissionClient.java
│   │               │           ├── TransmissionException.java
│   │               │           ├── model/
│   │               │           │   ├── AddedTorrentInfo.java
│   │               │           │   ├── JSONAccessor.java
│   │               │           │   ├── SessionStatus.java
│   │               │           │   ├── TorrentStatus.java
│   │               │           │   ├── TrackerPair.java
│   │               │           │   ├── TransmissionSession.java
│   │               │           │   └── package.html
│   │               │           └── package.html
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               ├── TransmissionPlugin.java
│   │                               └── models/
│   │                                   ├── TorrentObject.java
│   │                                   └── TorrentSession.java
│   └── unifi/
│       ├── pom.xml
│       └── src/
│           └── main/
│               └── java/
│                   ├── assets/
│                   │   ├── js/
│                   │   │   └── unifi.js
│                   │   ├── less/
│                   │   │   └── unifi.less
│                   │   └── templates/
│                   │       ├── unifi-1x1.jade
│                   │       └── unifi-settings.jade
│                   └── com/
│                       └── ftpix/
│                           └── homedash/
│                               └── plugins/
│                                   └── unifi/
│                                       ├── UnifiApi.java
│                                       ├── UnifiPlugin.java
│                                       ├── UnifiResponse.java
│                                       └── UnifiThroughPut.java
├── pom.xml
├── updater/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── ftpix/
│       │   │           └── homedash/
│       │   │               └── updater/
│       │   │                   ├── Updater.java
│       │   │                   └── exceptions/
│       │   │                       ├── WrongInstallPathStructure.java
│       │   │                       └── WrongVersionPatternException.java
│       │   └── resources/
│       │       ├── log4j2.xml
│       │       └── version.properties
│       └── test/
│           └── java/
│               └── com/
│                   └── ftpix/
│                       └── homedash/
│                           └── updater/
│                               └── UpdaterTest.java
└── web/
    ├── .gitignore
    ├── assembly.xml
    ├── docker/
    │   ├── Dockerfile
    │   └── run.sh
    ├── pom.xml
    ├── src/
    │   ├── main/
    │   │   ├── java/
    │   │   │   ├── assets/
    │   │   │   │   ├── js/
    │   │   │   │   │   ├── add-remote.js
    │   │   │   │   │   ├── full-screen.js
    │   │   │   │   │   ├── index.js
    │   │   │   │   │   ├── ios-script.js
    │   │   │   │   │   ├── kiosk.js
    │   │   │   │   │   ├── layout-settings.js
    │   │   │   │   │   ├── pages.js
    │   │   │   │   │   ├── settings.js
    │   │   │   │   │   └── websocket.js
    │   │   │   │   └── less/
    │   │   │   │       ├── dropdown-multilevel.less
    │   │   │   │       ├── full-screen.less
    │   │   │   │       ├── index.less
    │   │   │   │       ├── kiosk.less
    │   │   │   │       ├── layout-settings.less
    │   │   │   │       ├── loader.less
    │   │   │   │       ├── login.less
    │   │   │   │       ├── main.less
    │   │   │   │       ├── module-modal.less
    │   │   │   │       ├── pages.less
    │   │   │   │       ├── remotes.less
    │   │   │   │       ├── settings.less
    │   │   │   │       ├── variables.less
    │   │   │   │       └── vendor/
    │   │   │   │           ├── bootstrap/
    │   │   │   │           │   ├── bootstrap.less
    │   │   │   │           │   └── config.json
    │   │   │   │           ├── font.less
    │   │   │   │           └── font_awesome/
    │   │   │   │               ├── animated.less
    │   │   │   │               ├── bordered-pulled.less
    │   │   │   │               ├── core.less
    │   │   │   │               ├── fixed-width.less
    │   │   │   │               ├── font-awesome.less
    │   │   │   │               ├── icons.less
    │   │   │   │               ├── larger.less
    │   │   │   │               ├── list.less
    │   │   │   │               ├── mixins.less
    │   │   │   │               ├── path.less
    │   │   │   │               ├── rotated-flipped.less
    │   │   │   │               ├── screen-reader.less
    │   │   │   │               ├── stacked.less
    │   │   │   │               └── variables.less
    │   │   │   └── com/
    │   │   │       └── ftpix/
    │   │   │           └── homedash/
    │   │   │               ├── app/
    │   │   │               │   ├── App.java
    │   │   │               │   ├── Constants.java
    │   │   │               │   ├── Endpoints.java
    │   │   │               │   ├── PluginModuleMaintainer.java
    │   │   │               │   └── controllers/
    │   │   │               │       ├── APIController.java
    │   │   │               │       ├── Controller.java
    │   │   │               │       ├── KioskController.java
    │   │   │               │       ├── LayoutController.java
    │   │   │               │       ├── ModuleController.java
    │   │   │               │       ├── ModuleLayoutController.java
    │   │   │               │       ├── ModuleSettingsController.java
    │   │   │               │       ├── PageController.java
    │   │   │               │       ├── PluginController.java
    │   │   │               │       ├── PluginUrlController.java
    │   │   │               │       ├── RemoteController.java
    │   │   │               │       ├── SettingsController.java
    │   │   │               │       └── UpdateController.java
    │   │   │               ├── db/
    │   │   │               │   ├── DB.java
    │   │   │               │   └── schemaManagement/
    │   │   │               │       ├── UpdateStep.java
    │   │   │               │       └── updates/
    │   │   │               │           └── Update20170722.java
    │   │   │               ├── jobs/
    │   │   │               │   └── BackgroundRefresh.java
    │   │   │               ├── utils/
    │   │   │               │   ├── HomeDashTemplateEngine.java
    │   │   │               │   └── Predicates.java
    │   │   │               └── websocket/
    │   │   │                   ├── FullScreenWebSocket.java
    │   │   │                   ├── InnerSocketClass.java
    │   │   │                   ├── KioskWebSocket.java
    │   │   │                   ├── MainWebSocket.java
    │   │   │                   ├── SingleModuleKioskWebSocket.java
    │   │   │                   └── SingleModuleWebSocket.java
    │   │   └── resources/
    │   │       ├── homedash.properties.default
    │   │       ├── log4j2.xml
    │   │       ├── templates/
    │   │       │   ├── add-module.jade
    │   │       │   ├── add-remote.jade
    │   │       │   ├── index.jade
    │   │       │   ├── layout-settings.jade
    │   │       │   ├── layout.jade
    │   │       │   ├── loader.jade
    │   │       │   ├── login.jade
    │   │       │   ├── module-full-screen.jade
    │   │       │   ├── module-kiosk.jade
    │   │       │   ├── module-layout.jade
    │   │       │   ├── module-settings.jade
    │   │       │   ├── settings.jade
    │   │       │   └── web-app-settings.jade
    │   │       └── web/
    │   │           ├── css/
    │   │           │   └── vendor/
    │   │           │       └── gridster/
    │   │           │           ├── gridster.css
    │   │           │           └── gridster.css.old
    │   │           ├── fonts/
    │   │           │   ├── FontAwesome/
    │   │           │   │   └── FontAwesome.otf
    │   │           │   └── Roboto/
    │   │           │       └── LICENSE.txt
    │   │           ├── images/
    │   │           │   └── ios/
    │   │           │       └── Contents.json
    │   │           └── js/
    │   │               └── vendor/
    │   │                   ├── bootstrap/
    │   │                   │   └── bootstrap.min.js.old
    │   │                   ├── grid/
    │   │                   │   ├── gridList.js
    │   │                   │   └── jquery.gridList.js
    │   │                   └── gridster/
    │   │                       ├── gridster.js
    │   │                       ├── gridster.js.old
    │   │                       └── jquery.gridster.js
    │   └── test/
    │       ├── java/
    │       │   └── com/
    │       │       └── ftpix/
    │       │           └── homedash/
    │       │               └── app/
    │       │                   └── ModuleTest.java
    │       └── resources/
    │           ├── conf/
    │           │   └── homedash.properties
    │           └── homedash.properties
    └── startup-scripts/
        ├── cpappend.bat
        ├── homedash.bat
        └── homedash.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .drone.yml
================================================
kind: pipeline
type: docker
name: default

steps:
  - name: restore-cache
    image: drillster/drone-volume-cache
    volumes:
      - name: cache
        path: /cache
    settings:
      restore: true
      mount:
        - ./m2

  - name: set version
    image: maven:3.8-openjdk-17
    commands:
      - mvn versions:set -DnewVersion=${DRONE_TAG} -Dmaven.repo.local=./m2
      - mvn versions:commit -Dmaven.repo.local=./m2
    when:
      event: tag

  - name: build
    image: maven:3.8-openjdk-17
    commands:
      - mvn clean install  -Dmaven.repo.local=./m2
      - mvn clean install -pl web -Dmaven.repo.local=./m2
      - cp web/target/Homedash-*.jar web/docker/
    #  - echo "latest,$(mvn help:evaluate -Dexpression=project.version -q  -DforceStdout -Dmaven.repo.local=./m2)" > .tags
    #  - cat .tags


  - name: rebuild-cache
    image: drillster/drone-volume-cache
    volumes:
      - name: cache
        path: /cache
    settings:
      rebuild: true
      mount:
        - ./m2

  - name: publish new version
    image: plugins/docker
    settings:
      username:
        from_secret: docker_username
      password:
        from_secret: docker_password
      tags:
        - '${DRONE_COMMIT}'
        - '${DRONE_TAG}'
        - latest
      repo: gonzague/homedash
      context: web/docker
      dockerfile: web/docker/Dockerfile
    when:
      event: tag

  - name: publish commit
    image: plugins/docker
    settings:
      username:
        from_secret: docker_username
      password:
        from_secret: docker_password
      tags:
        - '${DRONE_COMMIT}'
      repo: gonzague/homedash
      context: web/docker
      dockerfile: web/docker/Dockerfile
    when:
      event:
        exclude:
          - tag

  - name: git hub release
    image: plugins/github-release
    settings:
      api_key:
        from_secret: github_token
      files: web/target/Homedash-*.jar
    when:
      event: tag
trigger:
  event:
    - push
    - tag

volumes:
  - name: cache
    host:
      path: /home/core/cache


================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto

# Custom for Visual Studio
*.cs     diff=csharp

# Standard to msysgit
*.doc	 diff=astextplain
*.DOC	 diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot  diff=astextplain
*.DOT  diff=astextplain
*.pdf  diff=astextplain
*.PDF	 diff=astextplain
*.rtf	 diff=astextplain
*.RTF	 diff=astextplain

================================================
FILE: .gitignore
================================================
#maven

target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties


# =========================
# Operating System Files
# =========================

# OSX
# =========================

.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

# Windows
# =========================
# Windows image file caches
Thumbs.db
ehthumbs.db

# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp

# Windows shortcuts
*.lnk








.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders

# Eclipse Core
.project

# External tool builders
.externalToolBuilders/

# Locally stored "Eclipse launch configurations"
*.launch

# PyDev specific (Python IDE for Eclipse)
*.pydevproject

# CDT-specific (C/C++ Development Tooling)
.cproject

# JDT-specific (Eclipse Java Development Tools)
.classpath

# Java annotation processor (APT)
.factorypath

# PDT-specific (PHP Development Tools)
.buildpath

# sbteclipse plugin
.target

# Tern plugin
.tern-project

# TeXlipse plugin
.texlipse

# STS (Spring Tool Suite)
.springBeans

# Code Recommenders
.recommenders/


#intellij
.idea/
*.iml


#Netbeans
nbproject/private/
build/
nbbuild/
dist/
nbdist/
nbactions.xml
.nb-gradle/

#Homedash2
logs
cache/
homedash.mv.db
web/src/main/resources/native-libs
web/src/main/resources/web/web
web/src/main/resources/web/js/*.js
web/src/main/resources/web/css/*.css


web/src/main/resources/homedash.properties
plugins/*/src/main/resources/*

================================================
FILE: DevelopPlugin.md
================================================
# Developing a plugin for HomeDash

Developing a module for HomeDash is pretty simple, you just need to follow a specific file and folder structure

## POM file

Example file from the Hard disk plugin:

```xml
<?xml version="1.0"?>
<project
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
        xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.ftpix.homedash.plugins</groupId>
        <artifactId>plugins</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>harddisk</artifactId>
    <name>harddisk</name>

    <url>http://maven.apache.org</url>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <sigar.version>1.6.4</sigar.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.lesscss</groupId>
                <artifactId>lesscss-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>net.tqh.plugins</groupId>
                <artifactId>uglifyjs-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
```

Things to notice:
1. You need to set a parent POM
2. You need to have the build plugins to compile the less files, uglify the javascript and copy the your assets files where they're supposed to be
3. You can add any other library you might need

## Folder structure

it should be as follow:
```
src
    | main
        | java
            | assets
                | files
                | js
                | less
                | templates
            | com.yourpackage.yourplugin.asyou.wish
```

Things to notice:
1. The 4 folders under the assets package have to be there otherwise the build will fail


## The plugin class

To create a plugin, you need to create a new class file that will implement the Plugin class (com.models.plugins.Plugin)

Commented example:

```java
package com.ftpix.homedash.plugins.harddisk;



import com.ftpix.homedash.Utils.ByteUtils;
import com.ftpix.homedash.models.ModuleExposedData;
import com.ftpix.homedash.models.WebSocketMessage;
import com.ftpix.homedash.plugins.Plugin;

import java.io.File;
import java.util.Hashtable;
import java.util.Map;

/**
 * Created by gz on 06-Jun-16.
 */
public class HarddiskPlugin extends Plugin {
    private final String SETTING_PATH = "path";


     /**
     * Unique name for the plugin
     * Better if a simple string without any special characters
     *
     * @return
     */
    @Override
    public String getId() {
        return "harddisk";
    }


    /**
     * Nice to read name of your plugin
     *
     * @return
     */
    @Override
    public String getDisplayName() {
        return "Hard Disk";
    }
    
    
    /**
     * Description of what it's doing
     *
     * @return
     */
    @Override
    public String getDescription() {
        return "Help you monitor the space on a mount point";
    }

    /**
     * Provide an external link if available
     * for example if your plugin refers to an external service
     * returning the url of the service here is nice to have
     *
     * @return null if no link, otherwise an http url
     */
    @Override
    public String getExternalLink() {
        return null;
    }

    /**
     * Give chance to a plugin to run some stuff when creating it Settings can
     * be accessed via settings object
     */
    @Override
    protected void init() {

    }

     /**
      * Get the sizes available for this module
      * Each size should have the format "{width}x{height}" ex 2x4 or 1x1
      * If your module handles full screen view getSizes should contain ModuleLayout.FULL_SCREEN
      * @return an
      */
      @Override
    public String[] getSizes() {
        return new String[]{"1x1", "2x1"};
    }

    /**
     * How often (in second) this module should be refreshed in the background ,
     * 0 = never
     *
     * @return
     */
    @Override
    public int getBackgroundRefreshRate() {
        return 0;
    }

    /**
     * How often (in second) this module should be refreshed in the background ,
     * 0 = never
     * 
     * @return
     */
    @Override
    public WebSocketMessage processCommand(String command, String message, Object extra) {
        return null;
    }



    /**
     * Do background task if getBackgroundRefreshRate() > 0
     */
    @Override
    public void doInBackground() {
    }


    /**
     * Get data to send to clients via web socket
     *
     * @param size of the module
     * @return
     * @throws Exception
     */
    @Override
    protected Object refresh(String size) throws Exception {
        File root = new File(settings.get(SETTING_PATH));

        long usedSpace = root.getTotalSpace() - root.getFreeSpace();

        Map<String, String> diskSpace = new Hashtable<String, String>();


        Map<String, String> spaces = new Hashtable<>();
        //String[] space = new String[] { root.getTotalSpace(), true), humanReadableByteCount(root.getFreeSpace(), true), humanReadableByteCount(usedSpace, true) };
        spaces.put("path", root.getAbsolutePath());
        spaces.put("total", Long.toString(root.getTotalSpace()));
        spaces.put("free", Long.toString(root.getFreeSpace()));
        spaces.put("used", Long.toString(usedSpace));
        spaces.put("pretty", ByteUtils.humanReadableByteCount(usedSpace, root.getTotalSpace(), true));

        return spaces;
    }
    
    
    
    /**
     * Get refresh rate for main page display
     *
     * @return
     */
    @Override
    public int getRefreshRate() {
        return ONE_MINUTE*2;
    }

    /**
     * Validates a given set of settings when user adds the plugin
     *
     * @param settings
     * @return
     */
    @Override
    public Map<String, String> validateSettings(Map<String, String> settings) {
        Map<String, String> errors = new Hashtable<>();

        if(!new File(settings.get(SETTING_PATH)).exists()){
            errors.put("Path", "This mount point doesn't exist.");
        }

        return errors;
    }

    /**
     * Expose a chunk of selected data on request
     * This is not mandatory but nice to have. it's used when creating things like Pinned Site live tiles for windows
     *
     * @return
     */
    @Override
    public ModuleExposedData exposeData() {
        ModuleExposedData data = new ModuleExposedData();

        File root = new File(settings.get(SETTING_PATH));
        long usedSpace = root.getTotalSpace() - root.getFreeSpace();

        data.addText(root.getAbsolutePath());
        data.addText(ByteUtils.humanReadableByteCount(usedSpace, root.getTotalSpace(), true));
        return data;
    }

    /**
     * Expose a chunk of selected settings on request
     * Used when showing the available modules to a remote instance
     * DO NOT ADD SENSITIVE DATA HERE it's just to give some hints to user
     * @return
     */
    @Override
    public Map<String, String> exposeSettings() {
        Map<String, String> result = new Hashtable<>();
        result.put("Path", settings.get(SETTING_PATH));
        return result;
    }




}
```

Things to know:

1. Your plugin can save data using the function: setData(String name, Object object);
2. You can retrieved previously saved data via: getData(String name, Class classOfData); or get all with getAllData();
3. Remove data using removeData(String name);
4. You can access the settings via the settings variable

## Templates

You will need to create one template per size your module supports. HomeDash is using JADE template engine.

Templates need to be located in the assets.templates package and you need to follow a specific naming convention

### Settings
if your module has settings it needs to be named {plugin-name}-settings.jade
Example for the harddisk module it needs to be harddisk-settings.jade

Code example for settings template (still following the harddisk module)
```jade
.form-group
    label(for="path") Path
    if settings && settings.containsKey("path")
        input.form-control(type="text", id="path", name="path", placeholder="Absolute Path", value='#{settings.get("path")}')
    else
        input.form-control(type="text", id="path", name ="path", placeholder = "Absolute Path")

```

The name of each field will create a new settings accessible via settings.get("field-name") (check the plugin class example in the refresh() method)

### Plugin views

You need to create one template file per size you support. It needs to follow the following:
[plugin-name]-[size].jade
[plugin-name]-full-screen.jade (if your module has a full page view)

You can put pretty much anything you want in there. 
**Do not put element IDs in your template** it can conflict if your user has more than one instance of your plugin

Example from the Hard disk module on size 2x1 (harddisk-2x1.jade)

```jade
.storage-icon
h4.path
p.data
.hdd-container
```

## CSS

HomeDash is using less css. Style your module using the following rule:
```css
.[plugin-name]{

}

//If you want different CSS for specific size
.[plugin-name].size-[size]{ //example .harddisk.size-2x1{}

}
```

Try to make the content of your module not go over the limit of it's size.
Use overflow: hidden if necessary

## Javascript

As the rest of the files, the javascript need to follow a specific name and structure.

Your javascript files should be located in **assets.js** and follow the following structure:

```javascript
function [plugin-name](moduleId) {

    this.moduleId = moduleId;

    this.onConnect = function () {

    };

    this.documentReady = function (size) {

    };
    
    this.root = function(){
        return rootElement(this.moduleId);
    }

    /*
    * The on message functions are called when your module receives a command from the backend
    * or the normal timed refresh (command == 'refresh' in that case)
    * One function per size your modules handls
    */
    this.onMessage_2x1 = function (command, message, extra) {
      
    };
    this.onMessage_1x1 = function (command, message, extra) {
       
    };
    
    //full screen
    this.onMessage_fullScreen = function (command, message, extra) {
     
    };

}
```

Things to know:
1. You can use jQuery (v2.x)
2. If you need to find element in your DOM you can call the function this.root function. Usefull to add listeners to any buttons or put content in DOM element.

Check the harddisk or any other to get some examples of code.


## Native libraries

If your plugin need to use native libraries, put them in the folder [plugin-root]/lib/native

Your plugin needs to be compiled with the WEB module to be able to work with native libraries. You can't just copy/paste the packaged jar in the distribution plugin folder.


## How to use your plugin

Once HomeDash is started, it should automatically detect your plugin and it should be displayed when trying to add a new module


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 Paul Fauchon

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.


================================================
FILE: README.md
================================================
[![Build Status](https://drone.ftpix.com/api/badges/lamarios/Homedash2/status.svg)](https://drone.ftpix.com/lamarios/Homedash2)

Project status: **Abandonned**.

# HomeDash

## Requirements

### Build

1. Java SDK 11
2. Maven

### Run

1. Java 11



## Run from distribution

### From JAR file

Download the latest Homedash-version.jar from the [release page](https://github.com/lamarios/Homedash2/releases).

First time you run the application, you'll need to generate a config file.
```
java -jar Homedash-{version}.jar -create-config
```

This will create a config file that you can modify in your current working directory.

You can then run 
```
java -Dconfig.file=./homedash.properties -jar Homedash-{version}.jar
```

### From docker

You can run homedash using docker.

```
docker run -t --name homedash \
        -v "/etc/localtime:/etc/localtime:ro" \
        -v "/your/path/to/save/data:/data" \
        -e "SALT=somerandomstring" \
        -p "4567:4567" \
        gonzague/homedash
```
Environment Variables:

| Variable | Description | Required |
| ---------- | ------------- | ---------- |
| SALT | A random string used for authentication and other hashes | YES |
| UID | if you want to run the container under a different user id. Runs as root (0) by default | no |
| GID | The group id to run the container under. Runs as root (0) by default | no |
| JAVA_OPTS | configure the JVM | no |
| SECURE | "true" or "false" (default) to enable HTTPS  | no |
| KEY_STORE | if SECURE="true" you need to specified the path of your keystore (jks) file within the container after you mounted it | no |
| KEY_STORE_PASS | the password to your key store file | no | 
| DEBUG | Set to "true" to allow connection to the JMX to get visual vm debugging, if enabled you need to add the port map the port 4570 of the container as well. | no | 
| CONFIG | You can define all the plugins and boards using this variable. It is a JSON format. You can generate this JSON from an existing instance of homedash at the url http://yourhomedashinstall:port/export-config | no |


Note that running Homedash in docker will have reduced feature when it comes to system monitoring due to the nature of docker containers.
## How to build

```
mvn clean  install
```

The compiled application will be under web/target/Homedash-{version}.jar

## Run from source with Maven

```
mvn -pl web exec:java
```
## Develop plugin

If you're interested to develop a plugin, check DevelopPlugin.md


================================================
FILE: docs/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"
          integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">

    <link rel="stylesheet" href="style.css">
    <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
</head>
<body>
<div class="head">
    <a class="nav-link" href="https://github.com/lamarios/HomeDash2">
        <span class="hidden-xs-down"> View on GitHub </span>
        <svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-mark-github" aria-hidden="true"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path></svg>
    </a>
</div>

<div class="container">
    <div class="jumbotron">
        <h1 class="display-3">HomeDash</h1>
        <p class="lead">
            HomeDash is a simple dashboard that allows to monitor and interact with many different services in order to
            have a single entry point for one's dedicated server via a set of Plugins.
            It's easy to install, setup and use.
        </p>
    </div>

    <hr/>

    <div class="row">
        <div class="col-sm-6">
            <h2>Customisable</h2>
            <p>The dashboard plugins can be arranged and resized in any way. The mobile, tablet, desktop layouts and
                your custom ones have their own arrangement so each screen is really tailor-made.</p>
        </div>
        <div class="gif layout-image col-sm-6">
        </div>
    </div>
    <hr/>
    <div class="row">
        <div class="col-sm-6">
            <h2>Responsive</h2>
            <p>
                HomeDash is fully responsive, it will adapt to any device, from mobile to desktop.
                Custom sizes of layouts can be created so it can really fit any device devices.
                A kiosk mode is also available so it can be displayed full screen on TVs or Raspberry Pis with small screens.
            </p>
        </div>
        <div class="gif responsive-image col-sm-6">
        </div>
    </div>
    <hr/>
    <div class="row">
        <div class="col-sm-6">
            <h2>Organisable</h2>
            <p>
                HomeDash allows to have multiple boards so plugins can be categorized to avoid having a cluttered
                dashboard.
            </p>
        </div>
        <div class="gif boards-image col-sm-6">
        </div>
    </div>
    <hr />
    <div class="row">
        <div class="col-sm-6">
            <h2>All in one</h2>
            <p>
                Homedash can import plugins from another instance to keep everything at one place.
            </p>
        </div>
        <div class="gif remote-image col-sm-6">
        </div>
    </div>
    <hr />
    <div class="row">
        <div class="col-sm-12">
            <h2>Plugins</h2>
            <p>
                Homedash has many different plugins to connect and interact with many services.
            </p>
            <div id="myCarousel" class="carousel slide" data-ride="carousel">

                <ol class="carousel-indicators">
                    <li data-target="#myCarousel" data-slide-to="0" class="active"></li>
                    <li data-target="#myCarousel" data-slide-to="1"></li>
                    <li data-target="#myCarousel" data-slide-to="2"></li>
                    <li data-target="#myCarousel" data-slide-to="3"></li>
                    <li data-target="#myCarousel" data-slide-to="4"></li>
                    <li data-target="#myCarousel" data-slide-to="5"></li>
                    <li data-target="#myCarousel" data-slide-to="6"></li>
                    <li data-target="#myCarousel" data-slide-to="7"></li>
                    <li data-target="#myCarousel" data-slide-to="8"></li>
                    <li data-target="#myCarousel" data-slide-to="9"></li>
                    <li data-target="#myCarousel" data-slide-to="10"></li>
                    <li data-target="#myCarousel" data-slide-to="11"></li>
                    <li data-target="#myCarousel" data-slide-to="12"></li>
                    <li data-target="#myCarousel" data-slide-to="13"></li>
                </ol>

                <div class="carousel-inner">
                   <!-- System info -->
                    <div class="carousel-item active">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/systeminfo.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>System Info</h3>
                                <p>Monitor the CPU and Ram of the server hosting HomeDash.</p>
                            </div>
                        </div>
                    </div>

                    <!--Hard Drive-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/harddrive.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>Hard Drive</h3>
                                <p>Monitor a hard drive size.</p>
                                <p><strong>Full screen:</strong> Browse, create folders, upload, copy/cut paste files throughout the selected drive.</p>
                            </div>
                        </div>
                    </div>

                    <!-- Network monitor-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/network.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>Network Monitor</h3>
                                <p>Monitor a network interface of the server hosting HomeDash</p>
                            </div>
                        </div>
                    </div>
                    <!--Transmission-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/transmission.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>Transmission</h3>
                                <p>Monitor Download and Upload speed of a Transmission instance. Throttle speed and add torrents easily.</p>
                                <p><strong>Full screen:</strong> Manage all torrents of the instance.</p>
                            </div>
                        </div>
                    </div>

                    <!-- CouchPotato-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/couchpotato.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>CouchPotato / Radarr</h3>
                                <p>Easily add movies to a CouchPotato / Radarr instance wanted list.</p>
                            </div>
                        </div>
                    </div>

                    <!-- Pi Hole-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/pihole.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>PiHole</h3>
                                <p>Monitor the ad-blocking software PiHole</p>
                            </div>
                        </div>
                    </div>

                    <!--Docker-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/docker.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>Docker</h3>
                                <p>Shows the amount of running containers</p>
                                <p><strong>Full screen:</strong> Manage containers and images</p>
                            </div>
                        </div>
                    </div>

                    <!--Docker compose-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/docker-compose.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>Docker Compose</h3>
                                <p>Shows the amount of running containers of a docker compose stack</p>
                                <p><strong>Full screen:</strong> Show all the containers details, edit the docker-compose file and run docker-compose commands</p>
                            </div>
                        </div>
                    </div>

                    <!-- Dynamic DNS-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/ddns.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>Dynamic DNS</h3>
                                <p>Refreshes a dynamic DNS IP whenever the server's IP changes. Dynamic DNS providers supported: OVH, no-ip.org, dyn.com</p>
                            </div>
                        </div>
                    </div>
                    <!--logs-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/logs.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>Logs</h3>
                                <p>Shows the level of activity on a log file</p>
                                <p><strong>Full screen:</strong> Follows the log file content</p>
                            </div>
                        </div>
                    </div>

                    <!--Spotify-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/spotify.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>Spotify</h3>
                                <p>Shows the currently playing song on a Spotify Account</p>
                            </div>
                        </div>
                    </div>

                    <!--Plex-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/plex.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>Media server</h3>
                                <p>Displays currently playing Videos on a Plex or Jellyfin server</p>
                            </div>
                        </div>
                    </div>

                    <!-- Sonarr-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/sonarr.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>Sonarr</h3>
                                <p>Displays the upcoming TV shows from a Sonarr installation and add new TV show in full screen mode</p>
                            </div>
                        </div>
                    </div>

                    <!--mma-->
                    <div class="carousel-item">
                        <div class="row">
                            <div class="col-sm-4">
                                <img src="images/plugins/mma.png" />
                            </div>
                            <div class="col-sm-8">
                                <h3>MMA</h3>
                                <p>Get updated on the upcoming Mixed Martial Arts events and browse fighter records</p>
                            </div>
                        </div>
                    </div>

                    <a class="carousel-control-prev carousel-control" href="#myCarousel" role="button" data-slide="prev">
                        &lt;
                    </a>
                    <a class="carousel-control-next carousel-control" href="#myCarousel" role="button" data-slide="next">
                        &gt;
                    </a>
                </div>
            </div>
        </div>
    </div>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
        integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
        crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"
        integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4"
        crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js"
        integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1"
        crossorigin="anonymous"></script>
</body>
</html>

================================================
FILE: docs/style.css
================================================
body {
    font-family: 'Open Sans', sans-serif;
}

.head {
    width: 100%;
    height: 188px;
    background-color: #333;
    overflow: hidden;
    text-align: right;
    background-image: url('images/screen-tablet.png');

    background-size: 70% auto;

    background-repeat: no-repeat;
    background-position: center 50px;
    box-shadow: inset 0 -10px 10px -10px #000000;

    color:white;

}

.head a{
    color: #dddddd;
}

.head svg{
    fill: #dddddd;
}

@media (max-width: 425px) {
    .jumbotron h1 {
        font-size: 3.5rem;
    }
}

@media (min-width: 425px) {
    .head {
        height: 250px;
        /*background-image: url('images/screen-mobile.png');*/
    }
}

@media (min-width: 768px) {
    .head {
        height: 350px;
        /*background-image: url('images/screen-mobile.png');*/
    }
}
@media (min-width: 970px) {
    .head {
        height: 485px;
        background-size: auto;
        /*background-image: url('images/screen-mobile.png');*/
    }
}

.head img {
    display: block;
    margin: auto;
    box-shadow: black 0 0 50px;
}

.jumbotron {
    background-color: white;
}

.gif {
    height: 200px;
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
}

.responsive-image {
    background-image: url("images/responsive-optimize.gif");
}

.boards-image {
    background-image: url("images/boards-optimize.gif");
}

.remote-image {
    background-image: url("images/remote.png");
}
.layout-image {
    background-image: url("images/layout-optimize.gif");
}

.carousel-item img {
    max-width: 100%;
}

.carousel-item .col-sm-4 {
    text-align: right;
}

@media(max-width: 575px){
    .carousel-item .col-sm-4 {
        text-align: center;
    }

}

.container {
    margin: auto;
    padding: 0;
    max-width: 900px;
}

.row, .jumbotron {
    max-width: 100%;
    margin: 0;
    padding: 15px;
}

.carousel-inner {
    /*margin: 0 166px 0 166px;*/
}

.carousel-indicators li {
    background-color: #333333;
    cursor: pointer;
}

.carousel-indicators li.active {
    background-color: #999;
}

.carousel-indicators {
    position: relative;
    margin: 10px 0 10px 0;
    top: 0;
    bottom: 0;
}

.carousel-control {
    color: #333333 !important;
    font-size: 40px;
    font-weight: bold;
    display: block;
    margin-top: 50px;
}

hr{
    border-color: white;
}

.carousel-control:hover {
    color: black;
}

.carousel-control-prev {
    text-align: left;
}

.carousel-control-next {
    text-align: right;
}


================================================
FILE: models/.gitignore
================================================
/target/


================================================
FILE: models/pom.xml
================================================
<?xml version="1.0"?>
<project
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
        xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.ftpix.homedash</groupId>
        <artifactId>homedash</artifactId>
        <version>2021.1</version>
    </parent>

    <artifactId>models</artifactId>

    <name>models</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.mashape.unirest</groupId>
            <artifactId>unirest-java</artifactId>
            <version>1.4.9</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.191</version>
        </dependency>
        <dependency>
            <groupId>com.j256.ormlite</groupId>
            <artifactId>ormlite-jdbc</artifactId>
            <version>4.48</version>
        </dependency>
        <dependency>
            <groupId>de.neuland-bfi</groupId>
            <artifactId>jade4j</artifactId>
            <version>1.2.1</version>
        </dependency>

        <dependency>
            <groupId>io.gsonfire</groupId>
            <artifactId>gson-fire</artifactId>
            <version>1.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.16.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.0</version>
        </dependency>
        <dependency>
            <groupId>com.sparkjava</groupId>
            <artifactId>spark-core</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>com.sparkjava</groupId>
            <artifactId>spark-template-jade</artifactId>
            <version>${spark.template.version}</version>
        </dependency>

    </dependencies>
</project>


================================================
FILE: models/src/main/java/com/ftpix/homedash/Utils/ByteUtils.java
================================================
package com.ftpix.homedash.Utils;

/**
 * Created by gz on 07-Jun-16.
 */
public class ByteUtils {

    public static  String humanReadableByteCount(long maxBytes, boolean si) {
        int unit = si ? 1000 : 1024;
        if (maxBytes < unit)
            return maxBytes + " B";
        int exp = (int) (Math.log(maxBytes) / Math.log(unit));
        String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
        return String.format("%.2f %sB", maxBytes / Math.pow(unit, exp), pre);
    }

    public static  String humanReadableByteCount(long usedBytes, long maxBytes, boolean si) {
        int unit = si ? 1000 : 1024;
        if (maxBytes < unit)
            return maxBytes + " B";
        int exp = (int) (Math.log(maxBytes) / Math.log(unit));
        String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
        return String.format("%.2f / %.2f %sB", usedBytes / Math.pow(unit, exp), maxBytes / Math.pow(unit, exp), pre);
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/Utils/HomeDashClassPathTemplateLoader.java
================================================
package com.ftpix.homedash.Utils;

import de.neuland.jade4j.template.ClasspathTemplateLoader;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;

public class HomeDashClassPathTemplateLoader extends ClasspathTemplateLoader {
    private String templateRoot;

    public HomeDashClassPathTemplateLoader() {

    }

    public HomeDashClassPathTemplateLoader(String templateRoot) {
        if (!templateRoot.endsWith(File.separator)) {
            templateRoot = templateRoot + File.separator;
        }

        this.templateRoot = templateRoot;
    }

    @Override
    public Reader getReader(String name) throws IOException {
        if (this.templateRoot != null) {
            name = this.templateRoot + name;
        }
        name = name.replaceAll("\\\\", "/");
        return new InputStreamReader(Thread.currentThread().getContextClassLoader().getResourceAsStream(name), this.getEncoding());
    }
}



================================================
FILE: models/src/main/java/com/ftpix/homedash/models/ExposedModule.java
================================================
package com.ftpix.homedash.models;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by gz on 22-Jun-16.
 */
public class ExposedModule {

    private String name;
    private int id;
    private String description;
    private Map<String, String> settings;
    private String pluginClass;


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Map<String, String> getSettings() {
        return settings;
    }

    public void setSettings(Map<String, String> settings) {
        this.settings = settings;
    }

    public String getPluginClass() {
        return pluginClass;
    }

    public void setPluginClass(String pluginClass) {
        this.pluginClass = pluginClass;
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/ExternalEndPointDefinition.java
================================================
package com.ftpix.homedash.models;

import spark.ResponseTransformer;
import spark.Route;
import spark.TemplateViewRoute;

public class ExternalEndPointDefinition {
    public enum Method{
        GET,POST;
    }

    private Method method;
    private String url, acceptType;
    private Route route;
    private ResponseTransformer transformer;

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getAcceptType() {
        return acceptType;
    }

    public void setAcceptType(String acceptType) {
        this.acceptType = acceptType;
    }

    public Route getRoute() {
        return route;
    }

    public void setRoute(Route route) {
        this.route = route;
    }

    public ResponseTransformer getTransformer() {
        return transformer;
    }

    public void setTransformer(ResponseTransformer transformer) {
        this.transformer = transformer;
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/Layout.java
================================================
package com.ftpix.homedash.models;

import com.google.gson.annotations.Expose;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;

@DatabaseTable(tableName = "layouts")
public class Layout {

	// length of a grid unit, means a 2x2 module with 150 as widhth would be
	// 300x300px
	public static int GRID_UNIT_WIDTH = 100;
	@DatabaseField(generatedId = true, allowGeneratedIdInsert = true)
	@Expose
	private int id;

	@DatabaseField
	@Expose
	private String name;

	@DatabaseField(unique = true)
	@Expose
	private int maxGridWidth;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getMaxGridWidth() {
		return maxGridWidth;
	}

	public void setMaxGridWidth(int maxGridWidth) {
		this.maxGridWidth = maxGridWidth;
	}

	public int getActualSize() {
		return GRID_UNIT_WIDTH * maxGridWidth;
	}

}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/Module.java
================================================
package com.ftpix.homedash.models;

import com.j256.ormlite.dao.ForeignCollection;
import com.j256.ormlite.field.DataType;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.field.ForeignCollectionField;
import com.j256.ormlite.table.DatabaseTable;

@DatabaseTable(tableName = "modules")
public class Module {

    @DatabaseField(generatedId = true, allowGeneratedIdInsert = true)
    private int id;

    @DatabaseField
    private String pluginClass;

    @DatabaseField(unknownEnumName = "LOCAL")
    private ModuleLocation location = ModuleLocation.LOCAL;

    @DatabaseField(foreign = true, foreignAutoRefresh = true, maxForeignAutoRefreshLevel = 1)
    private Page page;

    @DatabaseField(dataType = DataType.BOOLEAN)
    private boolean onKiosk;

    @ForeignCollectionField(eager = false, maxEagerLevel = 0)
    public ForeignCollection<ModuleSettings> settings;

    @ForeignCollectionField(eager = false, maxEagerLevel = 0)
    public ForeignCollection<ModuleLayout> layouts;

    @ForeignCollectionField(eager = false, maxEagerLevel = 0)
    public ForeignCollection<ModuleData> data;

    public int getId() {
        return id;
    }

    public String getPluginClass() {
        return pluginClass;
    }

    public void setPluginClass(String pluginClass) {
        this.pluginClass = pluginClass;
    }

    public ModuleLocation getLocation() {
        return location;
    }

    public void setLocation(ModuleLocation location) {
        this.location = location;
    }

    public Page getPage() {
        return page;
    }

    public void setPage(Page page) {
        this.page = page;
    }

    public ForeignCollection<ModuleSettings> getSettings() {
        return settings;
    }

    public void setSettings(ForeignCollection<ModuleSettings> settings) {
        this.settings = settings;
    }

    public ForeignCollection<ModuleLayout> getLayouts() {
        return layouts;
    }

    public void setLayouts(ForeignCollection<ModuleLayout> layouts) {
        this.layouts = layouts;
    }

    public ForeignCollection<ModuleData> getData() {
        return data;
    }

    public void setData(ForeignCollection<ModuleData> data) {
        this.data = data;
    }

    public void setId(int id) {
        this.id = id;
    }

    public boolean isOnKiosk() {
        return onKiosk;
    }

    public void setOnKiosk(boolean onKiosk) {
        this.onKiosk = onKiosk;
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/ModuleData.java
================================================
package com.ftpix.homedash.models;

import com.j256.ormlite.field.DataType;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.field.types.LongStringType;
import com.j256.ormlite.table.DatabaseTable;

@DatabaseTable(tableName = "module_data")
public class ModuleData {
    @DatabaseField(generatedId = true, allowGeneratedIdInsert = true)
    private int id;

    @DatabaseField(foreign = true, foreignAutoRefresh = true, maxForeignAutoRefreshLevel = 1, uniqueCombo = true)
    private Module module;


    @DatabaseField(uniqueCombo = true)
    private String name;

    @DatabaseField(dataType = DataType.LONG_STRING)
    private String json;


    @DatabaseField
    private String dataClass;


    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Module getModule() {
        return module;
    }

    public void setModule(Module module) {
        this.module = module;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getJson() {
        return json;
    }

    public void setJson(String json) {
        this.json = json;
    }

    public String getDataClass() {
        return dataClass;
    }

    public void setDataClass(String dataClass) {
        this.dataClass = dataClass;
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/ModuleExposedData.java
================================================
package com.ftpix.homedash.models;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by gz on 06-Jun-16.
 */
public class ModuleExposedData {
    private List<String> images = new ArrayList<String>();
    private List<String> texts = new ArrayList<String>();
    private String moduleName = "";

    public List<String> getImages() {
        return images;
    }

    public void setImages(List<String> images) {
        this.images = images;
    }

    public List<String> getTexts() {
        return texts;
    }

    public void setTexts(List<String> texts) {
        this.texts = texts;
    }

    public String getModuleName() {
        return moduleName;
    }

    public void setModuleName(String moduleName) {
        this.moduleName = moduleName;
    }

    public void addText(String text) {
        texts.add(text);
    }

    public void addImage(String path) {
        images.add(path);
    }
}



================================================
FILE: models/src/main/java/com/ftpix/homedash/models/ModuleLayout.java
================================================
package com.ftpix.homedash.models;

import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;

@DatabaseTable(tableName = "module_layout")
public class ModuleLayout {

	public static final String SIZE_1x1 = "1x1", SIZE_2x1 = "2x1", SIZE_2x2 = "2x2", FULL_SCREEN = "full-screen", KIOSK = "kiosk";

	@DatabaseField(generatedId = true, allowGeneratedIdInsert = true)
	private int id;

	@DatabaseField(foreign = true, foreignAutoRefresh = true, maxForeignAutoRefreshLevel = 1)
	private Layout layout;

	@DatabaseField(foreign = true, foreignAutoRefresh = true, maxForeignAutoRefreshLevel = 1)
	private Module module;

	@DatabaseField
	private int x = 0;

	@DatabaseField
	private int y = 0;

	@DatabaseField
	private String size;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public Layout getLayout() {
		return layout;
	}

	public void setLayout(Layout layout) {
		this.layout = layout;
	}

	public Module getModule() {
		return module;
	}

	public void setModule(Module module) {
		this.module = module;
	}

	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getY() {
		return y;
	}

	public void setY(int y) {
		this.y = y;
	}

	public String getSize() {
		return size;
	}

	public String getWidth() {
		return size.split("x")[0];
	}

	public String getHeight() {
		return size.split("x")[1];
	}

	public void setSize(String size) {
		this.size = size;
	}
	
	@Override
	public boolean equals(Object obj) {
		try{
			ModuleLayout other = (ModuleLayout) obj;
			return id == other.getId();
		}catch(Exception e){
			return false;
		}
	}
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/ModuleLocation.java
================================================
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.ftpix.homedash.models;

/**
 *
 * @author User
 */
public enum ModuleLocation {
    REMOTE, LOCAL
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/ModuleSettings.java
================================================
package com.ftpix.homedash.models;

import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;

@DatabaseTable(tableName = "module_settings")
public class ModuleSettings {
	@DatabaseField(generatedId = true, allowGeneratedIdInsert = true)
	private int id;
	
	@DatabaseField(foreign = true, foreignAutoRefresh = true, maxForeignAutoRefreshLevel = 1)
	private Module module;
	
	
	@DatabaseField
	private String name;
	
	@DatabaseField
	private String value;

	public ModuleSettings(){}

	public ModuleSettings(String name, String value) {
		this.name = name;
		this.value = value;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public Module getModule() {
		return module;
	}

	public void setModule(Module module) {
		this.module = module;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getValue() {
		return value;
	}

	public void setValue(String value) {
		this.value = value;
	}
	
	
	
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/Page.java
================================================
package com.ftpix.homedash.models;

import com.google.gson.annotations.Expose;
import com.j256.ormlite.dao.ForeignCollection;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.field.ForeignCollectionField;
import com.j256.ormlite.table.DatabaseTable;

@DatabaseTable(tableName = "pages")
public class Page {
	@DatabaseField(generatedId = true, allowGeneratedIdInsert = true)
	@Expose
	private int id;

	@DatabaseField
	@Expose
	private String name;

	@ForeignCollectionField(eager = false, maxEagerLevel = 0)
	public ForeignCollection<Module> modules;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public ForeignCollection<Module> getModules() {
		return modules;
	}

	public void setModules(ForeignCollection<Module> modules) {
		this.modules = modules;
	}
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/RemoteFavorite.java
================================================
package com.ftpix.homedash.models;

import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;

/**
 * Created by gz on 22-Jun-16.
 */

@DatabaseTable(tableName = "remote_favorite")
public class RemoteFavorite {

    @DatabaseField(generatedId = true, allowGeneratedIdInsert = true)
    private int id;

    @DatabaseField
    private String name;

    @DatabaseField(unique = true)
    private String url;

    @DatabaseField
    private String apiKey;



    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getApiKey() {
        return apiKey;
    }

    public void setApiKey(String apiKey) {
        this.apiKey = apiKey;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }


    @Override
    public String toString() {
        return "RemoteFavorite{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", url='" + url + '\'' +
                ", apiKey='" + apiKey + '\'' +
                '}';
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/Schema.java
================================================
package com.ftpix.homedash.models;

import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;

/**
 * Created by gz on 7/22/17.
 */
@DatabaseTable(tableName = "schema")
public class Schema {


    @DatabaseField(id = true)
    private String version;

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/Settings.java
================================================
package com.ftpix.homedash.models;

import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;

@DatabaseTable(tableName = "settings")
public class Settings {
	
	public static final String USE_AUTH = "use_auth", USERNAME="username", PASSWORD="password", PUSHBULLET="use_pushbullet", PUSHBULLET_API_KEY = "pushbullet_api_key",
			PUSHALOT="use_pushalot", PUSHALOT_API_KEY = "pushalot_api_key", PUSHOVER = "use_pushover", PUSHOVER_API_KEY = "pushover_api_key", PUSHOVER_APP_TOKEN = "pushover_app_token",
	USE_REMOTE = "use_remote", REMOTE_API_KEY = "remote_api_key", REMOTE_NAME="remote_name";
	
	
	@DatabaseField(id = true)
	private String name;
	
	@DatabaseField
	private String value;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getValue() {
		return value;
	}

	public void setValue(String value) {
		this.value = value;
	}	
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/Version.java
================================================
package com.ftpix.homedash.models;

import java.util.Comparator;
import java.util.stream.Stream;

/**
 * Created by gz on 7/22/17.
 */
public class Version implements Comparable<Version>, Comparator<Version> {
    private int year = 0, month = 0, patch = 0;


    private final static String VERSION_PATTERN = "(\\d+)\\.(\\d+)\\.(\\d+)";


    public Version(String version) {
        if (version.matches(VERSION_PATTERN)) {

            int[] ints = Stream.of(version.split("\\.")).mapToInt(Integer::valueOf).toArray();

            year = ints[0];
            month = ints[1];
            patch = ints[2];
        }
    }

    public Version(int major, int minor, int patch) {
        this.year = major;
        this.month = minor;
        this.patch = patch;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public int getMonth() {
        return month;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    @Override
    public int compareTo(Version version) {

        int yearCompare = Integer.compare(year, version.getYear());
        int monthCompare = Integer.compare(month, version.getMonth());
        int patchCompare = Integer.compare(patch, version.getPatch());

        if (yearCompare == 0) {
            if (monthCompare == 0) {
                return patchCompare;
            } else {
                return monthCompare;
            }
        } else {
            return yearCompare;
        }
    }

    @Override
    public int compare(Version version, Version t1) {
        return version.compareTo(t1);
    }

    @Override
    public String toString() {
        return year + "." + month + "." + patch;
    }

    public int getPatch() {
        return patch;
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/WebSocketMessage.java
================================================
package com.ftpix.homedash.models;

import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import io.gsonfire.GsonFireBuilder;

import java.lang.reflect.Modifier;

public class WebSocketMessage {
	public final static String COMMAND_START = "start", COMMAND_ERROR = "error", COMMAND_SUCCESS = "success", COMMAND_REFRESH = "refresh", COMMAND_CHANGE_PAGE = "changePage",
			REMOTE_MODULE_NOT_FOUND = "remote404", RELOAD_OTHERS = "reloadOthers", COMMAND_CHANGE_LAYOUT = "changeLayout", COMMAND_SET_MODULE = "setModule";

	private String command;
	private Object message, extra;
	@SerializedName("id")
	private int moduleId;

	
	

	public String getCommand() {
		return command;
	}

	public void setCommand(String command) {
		this.command = command;
	}

	public Object getMessage() {
		return message;
	}

	public void setMessage(Object message) {
		this.message = message;
	}



	public int getModuleId() {
		return moduleId;
	}

	public void setModuleId(int moduleId) {
		this.moduleId = moduleId;
	}

	public Object getExtra() {
		return extra;
	}

	public void setExtra(Object extra) {
		this.extra = extra;
	}

	public String toJSon() {
		GsonBuilder builder = new GsonFireBuilder().enableExposeMethodResult().createGsonBuilder();
		builder.excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT, Modifier.VOLATILE);

		return builder.serializeSpecialFloatingPointValues().create().toJson(this);

	}
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/WebSocketSession.java
================================================
package com.ftpix.homedash.models;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;

public class WebSocketSession {
	private Session session;
	private Page page;
	private Layout layout;
	private  Logger logger = LogManager.getLogger();

	public Session getSession() {
		return session;
	}

	public void setSession(Session session) {
		this.session = session;
	}
	
	public Page getPage() {
		return page;
	}

	public void setPage(Page page) {
		this.page = page;
	}

	

	public Layout getLayout() {
		return layout;
	}

	public void setLayout(Layout layout) {
		this.layout = layout;
	}

	@Override
	public boolean equals(Object obj) {
		//logger.info("equals !!!");

		try{
			WebSocketSession wsSession = (WebSocketSession) obj;
			return wsSession.getSession().equals(session);
		}catch(Exception e){
			//logger.info("Can't convert");
		}
		
		//trying if it is just a session
		try{
			Session otherSession = (Session) obj;
			//logger.info("{} == {}", otherSession.hashCode(), session.hashCode());
			return otherSession.hashCode() == session.hashCode();
		}catch(Exception e){
			//logger.info("Can't convert to session either ?");

		}
		
		return false;
	}
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/export/Export.java
================================================
package com.ftpix.homedash.models.export;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Export {

    public List<PageExport> boards = new ArrayList<>();
    public List<LayoutExport> layouts = new ArrayList<>();
    public List<ModuleExport> modules = new ArrayList<>();
    public Map<String, String> settings = new HashMap<>();

}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/export/LayoutExport.java
================================================
package com.ftpix.homedash.models.export;

import com.ftpix.homedash.models.Layout;

public class LayoutExport {
    public int id, maxGridWidth;
    public String name;


    public static LayoutExport fromModel(Layout layout) {
        LayoutExport export = new LayoutExport();

        export.id = layout.getId();
        export.maxGridWidth = layout.getMaxGridWidth();
        export.name = layout.getName();

        return export;
    }

    public static Layout toModel(LayoutExport export) {
        Layout layout = new Layout();

        layout.setMaxGridWidth(export.maxGridWidth);
        layout.setName(export.name);
        layout.setId(export.id);

        return layout;
    }

}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/export/ModuleExport.java
================================================
package com.ftpix.homedash.models.export;

import com.ftpix.homedash.models.Module;
import com.ftpix.homedash.models.ModuleLocation;
import com.ftpix.homedash.models.Page;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ModuleExport {
    public int page;

    public String pluginClass;
    public boolean onKiosk;

    public List<ModuleLayoutExport> layouts = new ArrayList<>();

    public Map<String, String> settings = new HashMap<>();

    public ModuleLocation location;


    public static ModuleExport fromModel(Module module) {
        ModuleExport export = new ModuleExport();

        export.page = module.getPage().getId();
        export.pluginClass = module.getPluginClass();
        export.onKiosk = module.isOnKiosk();

        module.getLayouts().stream()
                .map(ModuleLayoutExport::fromModel)
                .forEach(export.layouts::add);


        module.getSettings()
                .forEach(s -> export.settings.put(s.getName(), s.getValue()));

        export.location = module.getLocation();

        return export;
    }

    public static Module toModel(ModuleExport export) {
        Module module = new Module();
        module.setPluginClass(export.pluginClass);
        module.setOnKiosk(export.onKiosk);
        module.setLocation(export.location);

        Page page = new Page();
        page.setId(export.page);

        module.setPage(page);


        return module;
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/export/ModuleLayoutExport.java
================================================
package com.ftpix.homedash.models.export;

import com.ftpix.homedash.models.ModuleLayout;

public class ModuleLayoutExport {
    public String size;
    public int layoutId, x, y;

    public static ModuleLayoutExport fromModel(ModuleLayout moduleLayout) {
        ModuleLayoutExport export = new ModuleLayoutExport();

        export.size = moduleLayout.getSize();
        export.x = moduleLayout.getX();
        export.y = moduleLayout.getY();

        export.layoutId = moduleLayout.getLayout().getId();

        return export;
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/models/export/PageExport.java
================================================
package com.ftpix.homedash.models.export;

import com.ftpix.homedash.models.Page;

public class PageExport {
    public int id;
    public String name;

    public static PageExport fromModel(Page page) {
        PageExport export = new PageExport();

        export.id = page.getId();
        export.name = page.getName();

        return export;
    }

    public static Page toModel(PageExport export) {
        Page page = new Page();
        page.setName(export.name);
        page.setId(export.id);


        return page;

    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/plugins/Plugin.java
================================================
package com.ftpix.homedash.plugins;

import com.ftpix.homedash.Utils.HomeDashClassPathTemplateLoader;
import com.ftpix.homedash.models.Module;
import com.ftpix.homedash.models.*;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import de.neuland.jade4j.JadeConfiguration;
import de.neuland.jade4j.exceptions.JadeException;
import de.neuland.jade4j.template.FileTemplateLoader;
import de.neuland.jade4j.template.JadeTemplate;
import de.neuland.jade4j.template.TemplateLoader;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;

import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.channels.NotYetBoundException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public abstract class Plugin {

    private final static String REMOTE_URL = "url", REMOTE_API_KEY = "key", REMOTE_MODULE_ID = "id";
    private final static boolean DEV_MODE = Boolean.parseBoolean(System.getProperty("dev", "false"));
    public static int NEVER = 0, ONE_SECOND = 1, ONE_MINUTE = 60, ONE_HOUR = 60 * ONE_MINUTE;
    protected Map<String, String> settings;
    protected Gson gson = new GsonBuilder().create();
    private Logger logger = LogManager.getLogger();
    private String cacheBase;
    private Module module;
    private List<PluginListener> listeners = new ArrayList<>();
    private AtomicInteger clients = new AtomicInteger(0);

    public Plugin() {
    }

    public Plugin(Module module) {
        this.module = module;

    }

    /**
     * Unique name for the plugin
     * Better if a simple string without any special characters
     */
    public abstract String getId();

    /**
     * Nice to read name of your plugin
     */
    public abstract String getDisplayName();

    /**
     * Description of what it's doing
     */
    public abstract String getDescription();

    /**
     * Provide an external link if available
     * for example if your plugin refers to an external service
     * returning the url of the service here is nice to have
     *
     * @return null if no link, otherwise an http url
     */
    public abstract String getExternalLink();

    /**
     * Give chance to a plugin to run some stuff when creating it Settings can
     * be accessed via settings object
     */
    protected abstract void init();

    /**
     * Get the sizes available for this module
     * Each size should have the format "{width}x{height}" ex 2x4 or 1x1
     * If your module handles full screen view getSizes should contain ModuleLayout.FULL_SCREEN
     *
     * @return an
     */
    public abstract String[] getSizes();

    /**
     * How often (in second) this module should be refreshed in the background ,
     * 0 = never
     */
    public abstract int getBackgroundRefreshRate();

    /**
     * Process a command sent by a client
     */
    protected abstract WebSocketMessage processCommand(String command, String message, Object extra);

    /**
     * Do background task if getBackgroundRefreshRate() > 0
     */
    public abstract void doInBackground();

    /**
     * Get data to send to clients via web socket
     *
     * @param size of the module
     */
    protected abstract Object refresh(String size) throws Exception;

    /**
     * Get refresh rate in seconds for main page display
     *
     * @param size size of the module being refreshed
     */
    public abstract int getRefreshRate(String size);

    /**
     * Validates a given set of settings when user adds the plugin
     */
    public abstract Map<String, String> validateSettings(Map<String, String> settings);

    /**
     * Expose a chunk of selected data on request This is not mandatory but nice to have. it's used
     * when creating things like Pinned Site live tiles for windows
     */
    public abstract ModuleExposedData exposeData();

    /**
     * Expose a chunk of selected settings on request
     * Used when showing the available modules to a remote instance
     * DO NOT ADD SENSITIVE DATA HERE it's just to give some hints to user
     */
    public abstract Map<String, String> exposeSettings();


    /**
     * Do something when the first websocket client connects
     */
    protected abstract void onFirstClientConnect();


    /**
     * Do something when the last websocket client disconnects
     */
    protected abstract void onLastClientDisconnect();


    /**
     * Any data that might be useful to the settings screen of the plugin.
     *
     * @param settings
     * @return anything useful, see Dynamic DNS plugin for example
     */
    protected Object getSettingsScreenData(Map<String, String> settings) {
        return null;
    }

    /**
     * Gets the html for a specific size
     */
    public final String getView(String size) throws JadeException, IOException {


        JadeConfiguration config = defaultTemplateConfig();
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("module", module);

        logger().info("id:[{}]", getId());
        logger().info("size:[{}]", size);
        JadeTemplate template = config.getTemplate("templates/" + getId() + "-" + size + ".jade");

        return config.renderTemplate(template, model);
    }


    /**
     * Gets the jadeconfiguration to get templates
     *
     * @return
     * @throws IOException
     */
    private JadeConfiguration defaultTemplateConfig() throws IOException {
        JadeConfiguration config = new JadeConfiguration();

        TemplateLoader loader = getTemplateLoader(null);

        config.setTemplateLoader(loader);

        return config;
    }

    /**
     * Refresh called from the websocket
     */
    public final WebSocketMessage refreshPlugin(String size) throws Exception {
        WebSocketMessage result = new WebSocketMessage();
        result.setCommand(WebSocketMessage.COMMAND_REFRESH);

        try {
            switch (module.getLocation()) {
                case LOCAL:
                    result.setMessage(refresh(size));
                    break;
                case REMOTE:
                    result.setMessage(refreshRemote(size));
                    break;
                default:
                    return null;
            }

        } catch (Exception e) {
            logger().error("Error while refreshing module", e);
            result.setCommand(WebSocketMessage.COMMAND_ERROR);
            result.setMessage("Can't refresh module:" + e.getMessage());
        }
        result.setModuleId(module.getId());

        return result;
    }

    /**
     * Processes an incoming command from the front end
     *
     * @param command the command name
     * @param message the content of the command
     * @param extra   any extra object that the front end might want to add
     * @return a {@link WebSocketMessage} to be sent to the front end
     */
    public final WebSocketMessage processIncomingCommand(String command, String message, Object extra) {
        switch (module.getLocation()) {
            case LOCAL:
                return processCommand(command, message, extra);
            case REMOTE:
                return processCommandRemote(command, message, extra);
            default:
                return null;
        }

    }


    /**
     * Gets the settings view if there's any
     */
    public final String getSettingsHtml() throws Exception {
        if (module != null) {
            return getSettingsHtml(getSettingsAsMap());
        } else {
            return getSettingsHtml(null);
        }
    }

    public final String getSettingsHtml(Map<String, String> settings) throws Exception {
        try {


            logger().info("Getting settings for [{}]", this.getId());
            JadeConfiguration config = defaultTemplateConfig();

            Map<String, Object> model = new HashMap<String, Object>();

            if (settings != null) {
                model.put("settings", settings);
            }


            Optional.ofNullable(getSettingsScreenData(settings))
                    .ifPresent(s -> model.put("data", s));

            Optional.ofNullable(getSettingsModel()).ifPresent(pluginSettingsModel -> model.put("model", pluginSettingsModel));

            String templateFile = "templates/" + getId() + "-settings.jade";
            logger().info("Looking for template: [{}]", templateFile);
            JadeTemplate template = config.getTemplate(templateFile);

            logger().info("Found setting template, returning it");
            return config.renderTemplate(template, model);
        } catch (Exception e) {
            logger().error("Error while getting settings template", e);
            throw e;
        }
    }

    /**
     * GEts the template loader based on the developer mode value
     *
     * @param loader
     * @return
     * @throws IOException
     */
    private final TemplateLoader getTemplateLoader(TemplateLoader loader) throws IOException {
        if (DEV_MODE) {
            //getting template
            Path plugin = Paths.get(".").resolve("plugins").resolve(getId()).toAbsolutePath();

            Optional<Path> optionalPath = Files.walk(plugin)
                    .filter(p -> Files.isDirectory(p) && p.toAbsolutePath().endsWith("assets/"))
                    .findFirst();


            if (optionalPath.isPresent()) {
                String basePath = optionalPath.get().toAbsolutePath().toString() + "/";
                loader = new FileTemplateLoader(basePath, StandardCharsets.UTF_8.name());
            }
        } else {
            loader = new HomeDashClassPathTemplateLoader();
        }
        return loader;
    }

    protected abstract Map<String, Object> getSettingsModel();

    /**
     * Get the module settings as a map
     */
    public final Map<String, String> getSettingsAsMap() {
        Map<String, String> settings = new HashMap<>();

        module.getSettings().forEach(ms -> {
            settings.put(ms.getName(), ms.getValue());
        });

        return settings;
    }

    /**
     * Get the module
     */
    public final Module getModule() {
        return module;
    }

    /**
     * Set the module, it will load the settings and the data from the module as
     * well
     */
    public final void setModule(Module module) {
        Map<String, String> oldSettings = null;
        if (this.module != null) {

            oldSettings = this.settings;
        }
        this.module = module;

        this.settings = getSettingsAsMap();

        if (module.getLocation() == ModuleLocation.LOCAL && (oldSettings == null || !this.settings.equals(oldSettings))) {
            init();
        }

    }

    public final void setCacheBase(String cacheBase) {
        this.cacheBase = cacheBase;
        if (!cacheBase.endsWith("/")) {
            this.cacheBase += "/";
        }
    }

    protected final Path getCacheFolder() throws NotYetBoundException, IOException {
        if (module != null) {

            Path p = Paths.get(cacheBase + module.getId() + "/");

            if (!Files.exists(p)) {
                Files.createDirectories(p);
            }
            return p;
        } else {
            throw new NotYetBoundException();
        }
    }

    /**
     * Gives the path to use for a given path file
     *
     * @param fileCachePath
     * @return
     */
    protected final String getCacheFileUrlPath(String fileCachePath) {
        final String relativeCacheLocation = fileCachePath.replaceAll(cacheBase, "");
        return "/cache" + (relativeCacheLocation.startsWith("/") ? "" : "/") + relativeCacheLocation;
    }

    /**
     * Boolean to check if a plugin has an external link
     */
    public final boolean hasExternalLink() {
        return getExternalLink() != null;
    }

    /**
     * Boolean to check if an array has full screen view
     */
    public final boolean hasFullScreen() {
        return Arrays.stream(getSizes()).anyMatch((s) -> s.equalsIgnoreCase(ModuleLayout.FULL_SCREEN));
    }

    /**
     * Adds a listener to the plugin listeners
     */
    public final void addListener(PluginListener listener) {
        if (!listeners.contains(listener)) {
            listeners.add(listener);
        }
    }

    public final void removeListener(PluginListener listener) {
        listeners.remove(listener);
    }

    /**
     * Sets data for a module
     */
    protected final void setData(String name, Object object) {
        logger().info("Saving data for module");
        ModuleData moduleData = new ModuleData();
        moduleData.setModule(module);
        moduleData.setName(name);
        moduleData.setDataClass(object.getClass().getCanonicalName());
        moduleData.setJson(gson.toJson(object));

        listeners.forEach(l -> l.saveModuleData(moduleData));
    }

    /**
     * Get module data
     *
     * @param type expected type
     */
    protected final Optional getData(String name, Type type) {
        try {
            Optional<ModuleData> filtered = module.getData().stream().filter(data -> data.getName().equalsIgnoreCase(name))
                    .findFirst();

            if (filtered.isPresent()) {

                ModuleData data = filtered.get();

                Class clazz = Class.forName(data.getDataClass());

                Object o = gson.fromJson(data.getJson(), type);

                return Optional.of(clazz.cast(o));
            } else {
                return Optional.empty();
            }
        } catch (Exception e) {
            logger().error("Error while getting the data", e);
            return Optional.empty();
        }
    }

    /**
     * Get all the data as a map
     */
    protected final Map<String, Object> getAllData() {
        Map<String, Object> data = new HashMap<>();

        module.getData().forEach(singleData -> {
            try {
                Class clazz = Class.forName(singleData.getDataClass());

                Object o = gson.fromJson(singleData.getJson(), clazz);
                data.put(singleData.getName(), o);
            } catch (Exception e) {
                logger().error("Error while getting module data [" + singleData.getName() + "]", e);
            }
        });

        return data;
    }

    /**
     * Remove a specific set of data
     */
    protected final void removeData(String name) {
        List<ModuleData> data = module.getData().stream().filter(d -> d.getName().equalsIgnoreCase(name))
                .collect(Collectors.toList());

        if (!data.isEmpty()) {
            listeners.forEach(l -> l.removeModuleData(data.get(0)));
        }
    }


    /**
     * Refresh a remote module
     */
    private final Object refreshRemote(String size) {

        if (module != null && module.getLocation() == ModuleLocation.REMOTE) {
            String url = settings.get(REMOTE_URL) + "api/refresh/" + settings.get(REMOTE_MODULE_ID) + "/size/" + size;
            String apiKey = settings.get(REMOTE_API_KEY);

            try {
                HttpResponse<JsonNode> response = Unirest.get(url).header("Authorization", apiKey).asJson();

                String jsonString = response.getBody().getObject().toString();

                logger().info("Refreshing remote module, calling [{}], responseL [{}]", url, jsonString);


                // replacing all the cache url calling by the remote one
                jsonString = jsonString.replaceAll("cache/", settings.get(REMOTE_URL) + "cache/");


                WebSocketMessage result = gson.fromJson(jsonString, WebSocketMessage.class);


                return result.getMessage();
            } catch (Exception e) {
                logger().error("Couldn't get remote module [" + settings.get(REMOTE_MODULE_ID) + "] from url: [" + url + "]", e);
                return null;
            }
        } else {
            return null;
        }
    }


    /**
     * Sends command to remote module
     */
    private WebSocketMessage processCommandRemote(String command, String message, Object extra) {
        if (module != null && module.getLocation() == ModuleLocation.REMOTE) {
            String url = settings.get(REMOTE_URL) + "api/process-command/" + settings.get(REMOTE_MODULE_ID);
            String apiKey = settings.get(REMOTE_API_KEY);

            try {
                HttpResponse<JsonNode> response = Unirest.post(url)
                        .header("Authorization", apiKey)
                        .field("command", command)
                        .field("message", message)
                        .field("extra", gson.toJson(extra))
                        .asJson();

                String jsonString = response.getBody().getObject().toString();

                logger().info("Refreshing remote module, calling [{}], responseL [{}]", url, jsonString);

                // replacing all the cache url calling by the remote one
                jsonString = jsonString.replaceAll("cache/", settings.get(REMOTE_URL) + "cache/");


                WebSocketMessage result = gson.fromJson(jsonString, WebSocketMessage.class);
                result.setModuleId(module.getId());


                return result;
            } catch (Exception e) {
                logger().error("Couldn't get remote module [" + settings.get(REMOTE_MODULE_ID) + "] from url: [" + url + "]", e);
                WebSocketMessage result = new WebSocketMessage();
                result.setCommand(WebSocketMessage.COMMAND_ERROR);
                result.setMessage("Can't refresh module:" + e.getMessage());
                result.setModuleId(module.getId());
                return result;
            }
        } else {
            return null;
        }
    }


    /**
     * Increase the number of clients, if it's the first one, do something
     */
    public void increaseClients() {
        if (clients.incrementAndGet() == 1) {
            logger().info("[{}] onFirstClientConnect()", getId());
            onFirstClientConnect();
        }

        logger().info("[{}] has now {} clients", getId(), clients.get());
    }

    /**
     * Decrease the number of clients, if it's the last one, do something
     */
    public void decreaseClients() {
        if (clients.decrementAndGet() == 0) {
            logger().info("[{}] onLastClientDisconnect()", getId());
            onLastClientDisconnect();
        }

        logger().info("[{}] has now {} clients", getId(), clients.get());
    }

    protected Logger logger() {
        ThreadContext.put("logFile", getId());
        return logger;
    }


    /**
     * Define endpoints that external parties can access.
     * The url will start by /external/{moduleid}/{your endpoint}
     * Follow sparkjava definition for parameters
     * <p>
     * YOUR URL MUST START WITH /
     *
     * @return
     */
    public List<ExternalEndPointDefinition> defineExternalEndPoints() {
        return null;
    }
}


================================================
FILE: models/src/main/java/com/ftpix/homedash/plugins/PluginListener.java
================================================
package com.ftpix.homedash.plugins;

import com.ftpix.homedash.models.Module;
import com.ftpix.homedash.models.ModuleData;

/**
 * Created by gz on 12-Jun-16.
 */
public interface PluginListener {
    void saveModuleData(ModuleData data);
    void removeModuleData(ModuleData data);
}


================================================
FILE: models/src/test/java/com/ftpix/homedash/plugins/AppTest.java
================================================
package com.ftpix.homedash.plugins;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

/**
 * Unit test for simple App.
 */
public class AppTest 
    extends TestCase
{
    /**
     * Create the test case
     *
     * @param testName name of the test case
     */
    public AppTest( String testName )
    {
        super( testName );
    }

    /**
     * @return the suite of tests being tested
     */
    public static Test suite()
    {
        return new TestSuite( AppTest.class );
    }

    /**
     * Rigourous Test :-)
     */
    public void testApp()
    {
        assertTrue( true );
    }
}


================================================
FILE: notifications/pom.xml
================================================
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>homedash</artifactId>
        <groupId>com.ftpix.homedash</groupId>
        <version>2021.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>notifications</artifactId>
    <packaging>jar</packaging>

    <name>notifications</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.mashape.unirest</groupId>
            <artifactId>unirest-java</artifactId>
            <version>1.4.9</version>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.16.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.0</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.21</version>
        </dependency>
    </dependencies>
</project>


================================================
FILE: notifications/src/main/java/com/ftpix/homedash/notifications/NotificationProvider.java
================================================
package com.ftpix.homedash.notifications;

import java.util.Map;

public interface NotificationProvider {
	String getName();
	void sendNotification(String title, String content) throws Exception;
	boolean setSettings(Map<String, String> settings);

}


================================================
FILE: notifications/src/main/java/com/ftpix/homedash/notifications/Notifications.java
================================================
package com.ftpix.homedash.notifications;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;


public class Notifications {
    private static final Logger logger = LogManager.getLogger();

    private static final Set<NotificationProvider> registered = new HashSet<>();

    public static void send(String title, String body) {
        send(title, body, false);
    }

    /**
     * Send a notification to all the registerd providers
     * @param title
     * @param body
     */
    public static void send(String title, String body, boolean test) {
        logger.info("Sending notification to [{}] providers: title:[{}] body:[{}]", registered.size(), title, body);
        registered.forEach(provider ->{
            try {
                logger.info("to provider [{}]", provider.getName());
                provider.sendNotification(title, body);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * Add a new provider (only if it doesn't already exists)
     * @param provider
     */
    public static void registerProvider(NotificationProvider provider){
        registered.add(provider);
        logger.info("Registering provider {}, Total providers: [{}]" ,provider.getName(), registered.size());
    }

    public static void resetRegisteredProvider(){
        logger.info("Clearing all the providers");
        registered.clear();
    }


}


================================================
FILE: notifications/src/main/java/com/ftpix/homedash/notifications/implementations/PushBullet.java
================================================
package com.ftpix.homedash.notifications.implementations;

import com.ftpix.homedash.notifications.NotificationProvider;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;


public class PushBullet implements NotificationProvider {
    public static final String API_KEY = "api-key", DEVICES = "devices";
    private Map<String, String> settings;

    private final Logger logger = LogManager.getLogger();

    @Override
    public String getName() {
        return "PushBullet";
    }

    @Override
    public void sendNotification(String title, String content) throws IOException {
        Authenticator.setDefault(new Authenticator() {
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(settings.get(API_KEY), "".toCharArray());
            }
        });

        String encodedTitle = URLEncoder.encode(title, "UTF-8");
        String encodedContent = URLEncoder.encode(content, "UTF-8");

        String url = "https://api.pushbullet.com/v2/pushes";
        URL obj = new URL(url);
        HttpURLConnection con = (HttpURLConnection) obj.openConnection();

        // add reuqest header
        con.setRequestMethod("POST");
        con.setRequestProperty("User-Agent", "Mozilla/5.0");
        con.setRequestProperty("Accept-Language", "en-US,en;q=0.5");

        String urlParameters = "type=note&title=" + encodedTitle + "&body=" + encodedContent;

        // Send post request
        con.setDoOutput(true);
        DataOutputStream wr = new DataOutputStream(con.getOutputStream());
        wr.writeBytes(urlParameters);
        wr.flush();
        wr.close();

        int responseCode = con.getResponseCode();
        logger.info("\nSending 'POST' request to URL : " + url);

        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String inputLine;
        StringBuffer response = new StringBuffer();

        while ((inputLine = in.readLine()) != null) {
            response.append(inputLine);
        }
        in.close();

    }

    @Override
    public boolean setSettings(Map<String, String> settings) {
        this.settings = settings;
        return settings.containsKey(API_KEY) && !settings.get(API_KEY).trim().equalsIgnoreCase("");
    }

}


================================================
FILE: notifications/src/main/java/com/ftpix/homedash/notifications/implementations/PushOver.java
================================================
package com.ftpix.homedash.notifications.implementations;

import com.ftpix.homedash.notifications.NotificationProvider;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;


public class PushOver implements NotificationProvider {
    public static final String USER_TOKEN = "user", APPLICATION_TOKEN = "token", DEVICES = "devices";
    private Map<String, String> settings;

    private final Logger logger = LogManager.getLogger();


    @Override
    public String getName() {
        return "PushOver";
    }

    @Override
    public void sendNotification(String title, String content) throws IOException {

        String encodedTitle = URLEncoder.encode(title, "UTF-8");
        String encodedContent = URLEncoder.encode(content, "UTF-8");
        String encodedAppToken = URLEncoder.encode(settings.get(APPLICATION_TOKEN), "UTF-8");
        String encodedUserToken = URLEncoder.encode(settings.get(USER_TOKEN), "UTF-8");

        String url = "https://api.pushover.net/1/messages.json";
        URL obj = new URL(url);
        HttpURLConnection con = (HttpURLConnection) obj.openConnection();

        // add reuqest header
        con.setRequestMethod("POST");
        con.setRequestProperty("User-Agent", "Mozilla/5.0");
        con.setRequestProperty("Accept-Language", "en-US,en;q=0.5");

        String urlParameters = "title=" + encodedTitle + "&message=" + encodedContent + "&" + APPLICATION_TOKEN + "=" + encodedAppToken + "&" + USER_TOKEN + "=" + encodedUserToken;

        // Send post request
        con.setDoOutput(true);
        DataOutputStream wr = new DataOutputStream(con.getOutputStream());
        wr.writeBytes(urlParameters);
        wr.flush();
        wr.close();

        int responseCode = con.getResponseCode();
        logger.info("\nSending 'POST' request to URL : " + url);
        logger.info("Post parameters : " + urlParameters);
        logger.info("Response Code : " + responseCode);

        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String inputLine;
        StringBuffer response = new StringBuffer();

        while ((inputLine = in.readLine()) != null) {
            response.append(inputLine);
        }
        in.close();

        // print result
        logger.info(response.toString());
    }

    @Override
    public boolean setSettings(Map<String, String> settings) {
        this.settings = settings;
        return settings.containsKey(APPLICATION_TOKEN) && !settings.get(APPLICATION_TOKEN).trim().equalsIgnoreCase("") && settings.containsKey(USER_TOKEN) && !settings.get(USER_TOKEN).trim().equalsIgnoreCase("");
    }

}

================================================
FILE: notifications/src/main/java/com/ftpix/homedash/notifications/implementations/Pushalot.java
================================================
package com.ftpix.homedash.notifications.implementations;

import com.ftpix.homedash.notifications.NotificationProvider;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


public class Pushalot implements NotificationProvider {
    public static final String API_KEY = "api-key", DEVICES = "devices";
    private Map<String, String> settings;

    private final Logger logger = LogManager.getLogger();


    @Override
    public String getName() {
        return "Pushalot";
    }

    @Override
    public void sendNotification(String title, String content) throws IOException {
        /*Authenticator.setDefault(new Authenticator() {
			protected PasswordAuthentication getPasswordAuthentication() {
				return new PasswordAuthentication(settings.get(API_KEY), "".toCharArray());
			}
		});*/

        String encodedTitle = URLEncoder.encode(title, "UTF-8");
        String encodedContent = URLEncoder.encode(content, "UTF-8");

        String url = "https://pushalot.com/api/sendmessage";
        URL obj = new URL(url);
        HttpURLConnection con = (HttpURLConnection) obj.openConnection();

        // add reuqest header
        con.setRequestMethod("POST");
        con.setRequestProperty("User-Agent", "Mozilla/5.0");
        con.setRequestProperty("Accept-Language", "en-US,en;q=0.5");

        String urlParameters = "AuthorizationToken=" + settings.get(API_KEY) + "&Title=" + encodedTitle + "&Body=" + encodedContent;

        // Send post request
        con.setDoOutput(true);
        DataOutputStream wr = new DataOutputStream(con.getOutputStream());
        wr.writeBytes(urlParameters);
        wr.flush();
        wr.close();

        int responseCode = con.getResponseCode();
        logger.info("\nSending 'POST' request to URL : " + url);
        logger.info("Post parameters : " + urlParameters);
        logger.info("Response Code : " + responseCode);

        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String inputLine;
        StringBuffer response = new StringBuffer();

        while ((inputLine = in.readLine()) != null) {
            response.append(inputLine);
        }
        in.close();

        // print result
        logger.info(response.toString());
    }

    @Override
    public boolean setSettings(Map<String, String> settings) {
        this.settings = settings;
        return settings.containsKey(API_KEY) && !settings.get(API_KEY).trim().equalsIgnoreCase("");
    }
}


================================================
FILE: plugins/couchpotato/pom.xml
================================================
<?xml version="1.0"?>
<project
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
        xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.ftpix.homedash.plugins</groupId>
        <artifactId>plugins</artifactId>
        <version>2021.1</version>
    </parent>

    <artifactId>couchpotato</artifactId>
    <name>couchpotato</name>
    <url>http://maven.apache.org</url>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <sigar.version>1.6.4</sigar.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.mashape.unirest</groupId>
            <artifactId>unirest-java</artifactId>
            <version>1.4.9</version>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>19.0</version>
        </dependency>
        <dependency>
            <groupId>org.imgscalr</groupId>
            <artifactId>imgscalr-lib</artifactId>
            <version>4.2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.lesscss</groupId>
                <artifactId>lesscss-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


================================================
FILE: plugins/couchpotato/src/main/java/assets/js/couchpotato.js
================================================
function couchpotato(moduleId) {

    this.moduleId = moduleId;
    this.movies = [];

    this.documentReady = function (size) {
        var self = this;
        var root = self.root();

        root.find('.modal').attr('data-module', this.moduleId);

        root.find(".show-search").click(function () {
            sendMessage(self.moduleId, 'qualities', '');
            sendMessage(self.moduleId, 'folders', '');
            self.modal().appendTo(".modal-dump").modal('show');
        });

        this.modal().find(".search-submit").click(function (event) {
            self.searchMovie();
        });

        this.modal().on('click', ".movie", function (event) {

            var movie = self.movies[$(this).attr("data-index")];
            self.addMovie(movie);

        });
    };

    this.modal = function () {
        return $(document).find('.modal[data-module="' + this.moduleId + '"]');
    };

    this.root = function () {
        return rootElement(this.moduleId);
    };

    this.onMessage = function (size, command, message, extra) {
        this.processData(command, message, extra);
    }


    this.processData = function (command, message, extra) {
        if (command === 'refresh') {
            this.refresh(message);
        } else if (command === 'movieList') {
            this.populateMovieList(message);
        } else if (command === 'error') {
            this.modal().modal('hide');
        } else if (command === 'folders') {
            this.populatesFolders(message);
        } else if (command === 'qualities') {
            this.populateQualities(message);
        }
    };

    this.populateQualities = function (qualities) {

        var qualitySelect = this.modal().find('.quality');
        var qualitiesStr = '';
        $.each(qualities, function (i, quality) {
            qualitiesStr += '<option value="' + quality.id + '">' + quality.name + '</option>';
        });
        qualitySelect.html(qualitiesStr);
    };

    this.populatesFolders = function (folders) {

        var folderSelect = this.modal().find('.folder');
        var foldersStr = '';
        $.each(folders, function (i, folder) {
            foldersStr += '<option value="' + folder.id + '">' + folder.name + '</option>';
        });
        folderSelect.html(foldersStr);
    };

    this.searchMovie = function () {

        this.modal().find('.couchpotato-movie-list').html('<div class="loader"></div>');
        //$("#cp"+this.moduleId+"-modal").appendTo("body").modal('show');
        sendMessage(this.moduleId, 'searchMovie', this.modal().find('.search-query').val());
    };

    this.refresh = function (message) {
        if (!message) {
            this.root.find(".overlay").html('Couch potato is not available at the moment');
            this.root.find(".overlay").show();
        } else {
            this.root().css('background-image', 'url(' + message.poster + ')');
            this.root().find('.name').html(message.name);
        }

    };

    this.addMovie = function (movie) {
        var qualitySelect = this.modal().find('.quality');
        var folderSelect = this.modal().find('.folder');

        var movieRequest = {
            movie: movie,
            quality: {
                id: qualitySelect.val(),
                name: qualitySelect.find('option:selected').text()
            },
            folder: {
                id: folderSelect.val(),
                name: folderSelect.find('option:selected').text()
            }
        }
        sendMessage(this.moduleId, 'addMovie', JSON.stringify(movieRequest));
        this.modal().modal('hide');
    };


    this.populateMovieList = function (message) {
        var parent = this;
        var movieList = parent.modal().find('.couchpotato-movie-list');
        movieList.html('');
        this.movies = message;
        if (message.length > 0) {
            $.each(message, function (index, value) {
                movieList.append(parent.movieToHtml(index, value));
                //$("#cp" + parent.moduleId + "-movieList").append('<hr style="border-color: black;
                // margin:0"/>');
            });
        } else {
            movieList.html('<p>No results</p>');
        }

    };

    this.movieToHtml = function (index, movie) {
        var html = [];
        html.push('<div class="movie" data-index="' + index + '">');
        html.push('<div class="movie-poster" style="background-image:url(', movie.poster,
            ');"></div>');
        html.push('<p class="movie-name"><strong>', movie.originalTitle, ' </strong>');

        if (movie.wanted) {
            html.push('<small>(already wanted)</small>');
        }

        if (movie.inLibrary) {
            html.push('<small>(already in library)</small>');
        }

        html.push(' - <span class="cp-movie-year">', movie.year, '</span></p>');

        html.push('</div>');
        return html.join('');

    }

}

================================================
FILE: plugins/couchpotato/src/main/java/assets/less/couchpotato.less
================================================
.couchpotato {
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;

  background-color: black;

  button {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    bottom: 0;
    color: white;
    font-size: 35px;
    background-color: rgba(0, 0, 0, 0.3);
    margin: 0;
  }

  p {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 2;
    color: white;
    text-align: center;
    margin: 0;
  }
}

.cp-modal {
  .search-form {
    text-align: center;

    input {
      width: 250px;
      height: 34px;
    }
  }

  .options{
    text-align: center;
  }

  .couchpotato-movie-list {
    margin-top: 20px;
    text-align: center;

    @poster-width: 150px;
    @poster-height: 225px;
    .movie {

      -webkit-transition: all 0.3s;
      transition: all 0.3s;

      animation-name: show-movie;
      animation-duration: 0.5s;

      display: inline-block;
      cursor: pointer;

      margin: 10px;
      vertical-align: top;

      .movie-poster {
        background-repeat: no-repeat;
        background-position: center;
        background-size: cover;
        background-color: black;

        border-radius: 4px;
        width: @poster-width;
        height: @poster-height;
      }

      .movie-name {
        width: @poster-width;
      }
    }

    .movie:hover {
      @scale: 1.1;
      -ms-transform: scale(@scale); /* IE 9 */
      -webkit-transform: scale(@scale); /* Safari */
      transform: scale(@scale);

    }

  }
}

.couchpotato.size-2x1, .couchpotato.size-2x2, .couchpotato.size-2x3, .couchpotato.size-3x2, .couchpotato.size-3x3 {
  button {
    font-size: 25px;
  }
}

@keyframes show-movie {
  0% {
    opacity: 0;
    @scale: 0.5;
    -ms-transform: scale(@scale); /* IE 9 */
    -webkit-transform: scale(@scale); /* Safari */
    transform: scale(@scale);
  }

  60% {
    opacity: 1;
    @scale: 1.05;
    -ms-transform: scale(@scale); /* IE 9 */
    -webkit-transform: scale(@scale); /* Safari */
    transform: scale(@scale);
  }

  100% {
    @scale: 1;
    -ms-transform: scale(@scale); /* IE 9 */
    -webkit-transform: scale(@scale); /* Safari */
    transform: scale(@scale);
  }
}

================================================
FILE: plugins/couchpotato/src/main/java/assets/templates/couchpotato-1x1.jade
================================================
button.btn.show-search
    i.fa.fa-search
p.name
include ./cp-modal.jade

================================================
FILE: plugins/couchpotato/src/main/java/assets/templates/couchpotato-1x3.jade
================================================
include ./couchpotato-1x1.jade

================================================
FILE: plugins/couchpotato/src/main/java/assets/templates/couchpotato-2x1.jade
================================================
button.btn.show-search
    i.fa.fa-search
    | &nbsp;Add movie
p.name
include ./cp-modal.jade

================================================
FILE: plugins/couchpotato/src/main/java/assets/templates/couchpotato-2x2.jade
================================================
include ./couchpotato-2x1.jade

================================================
FILE: plugins/couchpotato/src/main/java/assets/templates/couchpotato-2x3.jade
================================================
include ./couchpotato-2x1.jade

================================================
FILE: plugins/couchpotato/src/main/java/assets/templates/couchpotato-3x2.jade
================================================
include ./couchpotato-2x1.jade

================================================
FILE: plugins/couchpotato/src/main/java/assets/templates/couchpotato-3x3.jade
================================================
include ./couchpotato-2x1.jade

================================================
FILE: plugins/couchpotato/src/main/java/assets/templates/couchpotato-settings.jade
================================================
.form-group
    label(for="type") API Type
    select(name="type", id="type")
        each type, index in {'COUCHPOTATO':'CouchPotato', 'RADARR':'Radarr'}
            if settings && settings.containsKey("type") && settings.get("type").equalsIgnoreCase(index)
                option(value=index, selected=true)= type
            else
                option(value=index)= type

.form-group
    label(for="url") Couchpotato/Radarr URL address
    if settings && settings.containsKey("url")
        input.form-control(type="text", id="url", name="url", placeholder="Enter URL", value='#{settings.get("url")}')
    else
        input.form-control(type="text", id="url", name ="url", placeholder = "Enter URL")

.form-group
    label(for="apiKey") API key
    if settings && settings.containsKey("apiKey")
        input.form-control(type="text", id="apiKey", name="apiKey", placeholder="API Key", value='#{settings.get("apiKey")}')
    else
        input.form-control(type="text", id="apiKey", name ="apiKey", placeholder = "API Key")



================================================
FILE: plugins/couchpotato/src/main/java/assets/templates/cp-modal.jade
================================================
.modal.fade.cp-modal(tabindex="-1", role="dialog", aria-labelledby="mySmallModalLabel" ,aria-hidden="true")
    .modal-dialog.modal-lg
        .modal-content
            .modal-header
                button.close(type="button", data-dismiss="modal")
                    span &times;
                h4.modal-title Search movie

            .modal-body
                .search-form
                    input.search-query(type="text")
                    button.search-submit.btn.btn-primary
                        i.fa.fa-search
                .options
                    span Quality:
                    select.quality
                    span Folder:
                    select.folder
                .couchpotato-movie-list

================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/CouchPotatoPlugin.java
================================================
package com.ftpix.homedash.plugins.couchpotato;

import com.ftpix.homedash.models.ModuleExposedData;
import com.ftpix.homedash.models.WebSocketMessage;
import com.ftpix.homedash.plugins.Plugin;
import com.ftpix.homedash.plugins.couchpotato.apis.CouchPotatoApi;
import com.ftpix.homedash.plugins.couchpotato.apis.MovieProviderAPI;
import com.ftpix.homedash.plugins.couchpotato.apis.RadarrApi;
import com.ftpix.homedash.plugins.couchpotato.models.ImagePath;
import com.ftpix.homedash.plugins.couchpotato.models.MovieObject;
import com.ftpix.homedash.plugins.couchpotato.models.MovieRequest;
import com.ftpix.homedash.plugins.couchpotato.models.Type;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;

/**
 * Created by gz on 22-Jun-16.
 */
public class CouchPotatoPlugin extends Plugin {
    public static final String URL = "url", API_KEY = "apiKey", TYPE = "type";
    public static final int THUMB_SIZE = 500;

    public static final String METHOD_SEARCH_MOVIE = "searchMovie", METHOD_MOVIE_LIST = "movieList", METHOD_ADD_MOVIE = "addMovie", METHOD_GET_QUALITIES = "qualities", METHOD_GET_FOLDERS = "folders";

    private final String IMAGE_PATH = "images/";
    private final ImagePath imagePath = () -> {
        try {
            return getCacheFolder().resolve(IMAGE_PATH).toString();
        } catch (IOException e) {
            return "";
        }
    };
    private MovieProviderAPI movieAPI = null;


    @Override
    public String getId() {
        return "couchpotato";
    }

    @Override
    public String getDisplayName() {
        return "CouchPotato/Radarr";
    }

    @Override
    public String getDescription() {
        return "Add movies to your CouchPotato/Radarr wanted list";
    }

    @Override
    public String getExternalLink() {
        return movieAPI.getBaseUrl();
    }

    @Override
    protected void init() {
        logger().info("Initiating Couchpotato plugin.");

        movieAPI = createMovieProviderApiFromSettings(settings);

    }

    @Override
    public String[] getSizes() {
        return new String[]{"1x1", "1x3", "2x1", "2x2", "2x3", "3x2", "3x3",};
    }

    @Override
    public int getBackgroundRefreshRate() {
        return 0;
    }

    @Override
    protected WebSocketMessage processCommand(String command, String message, Object extra) {
        WebSocketMessage response = new WebSocketMessage();
        if (command.equalsIgnoreCase(METHOD_SEARCH_MOVIE)) {
            try {
                response.setMessage(searchMovie(message));
                response.setCommand(METHOD_MOVIE_LIST);
            } catch (Exception e) {
                logger().error("Error while searching movie", e);
                response.setCommand(WebSocketMessage.COMMAND_ERROR);
                response.setMessage("Error while searching movie.");
            }
        } else if (command.equalsIgnoreCase(METHOD_ADD_MOVIE)) {
            try {
                MovieRequest movieObject = gson.fromJson(message, MovieRequest.class);
                addMovie(movieObject);

                response.setCommand(WebSocketMessage.COMMAND_SUCCESS);
                response.setMessage("Movie added successfully !");
            } catch (Exception e) {
                logger().error("Error while searching movie", e);
                response.setCommand(WebSocketMessage.COMMAND_ERROR);
                response.setMessage("Error while Adding movie.");
            }
        } else if (command.equalsIgnoreCase(METHOD_GET_QUALITIES)) {
            try {
                response.setMessage(movieAPI.getQualityProfiles());
                response.setCommand(METHOD_GET_QUALITIES);
            } catch (Exception e) {
                logger().error("Error while getting qualities", e);
                response.setCommand(WebSocketMessage.COMMAND_ERROR);
                response.setMessage("Error while getting qualities.");
            }
        } else if (command.equalsIgnoreCase(METHOD_GET_FOLDERS)) {
            try {
                response.setMessage(movieAPI.getMoviesRootFolder());
                response.setCommand(METHOD_GET_FOLDERS);
            } catch (Exception e) {
                logger().error("Error while getting folders", e);
                response.setCommand(WebSocketMessage.COMMAND_ERROR);
                response.setMessage("Error while getting folders.");
            }
        }
        return response;
    }

    @Override
    public void doInBackground() {

    }

    @Override
    protected Object refresh(String size) throws Exception {
        Map<String, String> map = new HashMap<>();
        map.put("poster", movieAPI.getRandomWantedPoster());
        map.put("name", movieAPI.getName());
        return map;
    }

    @Override
    public int getRefreshRate(String size) {
        return ONE_MINUTE * 10;
    }

    @Override
    public Map<String, String> validateSettings(Map<String, String> settings) {
        //TODO create new movie API
        MovieProviderAPI movieAPI = createMovieProviderApiFromSettings(settings);
        return movieAPI.validateSettings();
    }

    @Override
    public ModuleExposedData exposeData() {
        try {
            ModuleExposedData data = new ModuleExposedData();

            File f = getImagePath().toFile();

            FilenameFilter filter = (dir, name) -> name.matches("([^\\s]+(\\.(?i)(jpg|png|gif|bmp))$)");

            List<File> filesArray = Arrays.asList(f.listFiles(filter));
            Collections.shuffle(filesArray);
            if (!filesArray.isEmpty()) {
                data.addImage(getImagePath() + filesArray.get(0).getName());
            }

            return data;
        } catch (Exception e) {
            logger().error("Couldn't get images", e);
            return null;
        }
    }

    @Override
    public Map<String, String> exposeSettings() {
        return null;
    }

    @Override
    protected void onFirstClientConnect() {

    }

    @Override
    protected void onLastClientDisconnect() {

    }


    @Override
    protected Map<String, Object> getSettingsModel() {
        return null;
    }

    ////////////
    /// Plugin methds
    ///

    private Path getImagePath() throws IOException {
        Path resolve = getCacheFolder().resolve(IMAGE_PATH);
        if (!java.nio.file.Files.exists(resolve)) {
            java.nio.file.Files.createDirectories(resolve);
        }
        return resolve;
    }


    private void addMovie(MovieRequest movieRequest) throws Exception {
        movieAPI.addMovie(movieRequest);
    }


    /**
     * Search a movie from couchpotato instance
     */
    private List<MovieObject> searchMovie(String query) throws Exception {
        return movieAPI.searchMovie(query);
    }


    /**
     * Generates an API client based on the settings
     *
     * @param settings
     * @return
     */
    private MovieProviderAPI createMovieProviderApiFromSettings(Map<String, String> settings) {

        Type type = Type.valueOf(settings.get(TYPE));

        String apiKey = settings.get(API_KEY);
        String url = settings.get(URL);

        if (!url.endsWith("/")) {
            url += "/";
        }

        if (!url.startsWith("http")) {
            url = "http://" + url;
        }

        return switch (type){
            case RADARR -> new RadarrApi(url,apiKey,imagePath, this::getCacheFileUrlPath);
            case COUCHPOTATO -> new CouchPotatoApi(url, apiKey, imagePath, this::getCacheFileUrlPath);
        };
    }
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/apis/CouchPotatoApi.java
================================================
package com.ftpix.homedash.plugins.couchpotato.apis;

import com.ftpix.homedash.plugins.couchpotato.models.*;
import com.google.common.io.Files;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.imgscalr.Scalr;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;

import static com.ftpix.homedash.plugins.couchpotato.CouchPotatoPlugin.THUMB_SIZE;

public class CouchPotatoApi extends MovieProviderAPI {

    private final String API_MOVIE_SEARCH = "/movie.search/?q=";
    private final String API_ADD_MOVIE = "/movie.add/?title=[TITLE]&identifier=[IMDB]";
    private final String API_AVAILABLE = "/app.available";
    private final String API_MOVIE_LIST = "/movie.list/?status=active";
    private final String baseUrl, url, apiKey;
    private final ImagePath imagePath;
    private Logger logger = LogManager.getLogger();

    public CouchPotatoApi(String baseUrl, String apiKey, ImagePath imagePath, Function<String, String> cachePathToUrlPath) {
        super(cachePathToUrlPath);
        this.baseUrl = baseUrl;
        this.imagePath = imagePath;

        url = baseUrl + "api/" + apiKey;
        this.apiKey = apiKey;
    }

    @Override
    public String getRandomWantedPoster() throws Exception {
        Unirest.get(url + API_AVAILABLE).asString();

        JSONObject movieList = new JSONObject(Unirest.get(url + API_MOVIE_LIST).asString().getBody());
        String poster = null;
        if (movieList.getBoolean("success")) {
            JSONArray movies = movieList.getJSONArray("movies");
            for (int i = 0; i < movies.length() && poster == null; i++) {
                JSONObject movieInfo = movies.getJSONObject(new Random().nextInt(movies.length())).getJSONObject("info");
                JSONArray images = movieInfo.getJSONObject("images").getJSONArray("poster_original");
                if (images.length() != 0) {

                    // poster = images.getString(new
                    // Random().nextInt(images.length()));
                    File f = new File(imagePath.get() + movieInfo.getString("imdb") + ".jpg");
                    if (!f.exists()) {
                        FileUtils.copyURLToFile(new java.net.URL(images.getString(new Random().nextInt(images.length()))), f);
                        BufferedImage image = ImageIO.read(f);
                        BufferedImage resized = Scalr.resize(image, THUMB_SIZE);
                        ImageIO.write(resized, Files.getFileExtension(f.getName()), f);
                    }
                    poster = imagePath.get() + movieInfo.getString("imdb") + ".jpg";

                }
            }
        }

        return poster;
    }

    @Override
    public Map<String, String> validateSettings() {
        Map<String, String> errors = new Hashtable<String, String>();
        String url = baseUrl + API_AVAILABLE;
        try {
            Unirest.get(url).asString().getBody();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            logger.info("Can't access Couchpotato at URL [{}]", url);
            errors.put("Unavailable", "Couch potato is not available at this URL: " + url);
        }

        return errors;
    }

    @Override
    public void addMovie(MovieRequest request) throws UnsupportedEncodingException, UnirestException {
        MovieObject movie = request.getMovie();
        String queryUrl = url + API_ADD_MOVIE.replace("[TITLE]", URLEncoder.encode(movie.originalTitle, "UTF-8")).replace("[IMDB]", movie.imdbId);
        Unirest.get(queryUrl).asString().getBody();
    }

    @Override
    public List<MovieObject> searchMovie(String query) throws Exception {
        List<MovieObject> result = new ArrayList<MovieObject>();
        String queryUrl = url + API_MOVIE_SEARCH + URLEncoder.encode(query, "UTF-8");

        String response = Unirest.get(queryUrl).asString().getBody();
        logger.info("Search query:[{}] response:{}", queryUrl, response);

        List<Callable<Void>> pictureDownload = new ArrayList<>();

        try {
            JSONObject json = new JSONObject(response);

            JSONArray jsonarray = json.getJSONArray("movies");

            for (int i = 0; i < jsonarray.length(); i++) {

                JSONObject movie = jsonarray.getJSONObject(i);

                MovieObject movieObject = new MovieObject();
                try {
                    movieObject.imdbId = movie.getString("imdb");

                    JSONArray images = movie.getJSONObject("images").getJSONArray("poster_original");
                    if (images.length() != 0) {
                        File f = new File(imagePath.get() + movieObject.imdbId + ".jpg");
                        if (!f.exists()) {

                            pictureDownload.add(() -> {
                                FileUtils.copyURLToFile(new java.net.URL(images.getString(0)), f);

                                BufferedImage image = ImageIO.read(f);
                                BufferedImage resized = Scalr.resize(image, THUMB_SIZE);
                                ImageIO.write(resized, Files.getFileExtension(f.getName()), f);

                                return null;
                            });

                        }
                        movieObject.poster = imagePath.get() + movieObject.imdbId + ".jpg";
                    }
                } catch (Exception e) {
                    logger.error("Error while parsing JSON");
                    //skipping for this item
                    continue;
                }

                try {
                    movieObject.inLibrary = movie.getBoolean("in_library");
                } catch (JSONException e) {
                    movieObject.inLibrary = true;
                }

                movieObject.originalTitle = movie.getString("original_title");
                try {
                    movieObject.wanted = movie.getBoolean("in_wanted");
                } catch (JSONException e) {
                    movieObject.wanted = true;
                }
                movieObject.year = movie.getInt("year");

                result.add(movieObject);

            }
        } catch (Exception e) {
            logger.info("No movies found");
        }


        //downloading thumbnails
        ExecutorService exec = Executors.newFixedThreadPool(result.size());
        try {
            exec.invokeAll(pictureDownload);
        } finally {
            exec.shutdown();
        }
        return result;
    }

    @Override
    public String getBaseUrl() {
        return baseUrl;
    }

    @Override
    public List<QualityProfile> getQualityProfiles() {
        return null;
    }

    @Override
    public List<MoviesRootFolder> getMoviesRootFolder() throws Exception {
        return null;
    }

    @Override
    public String getName() {
        return "CouchPotato";
    }
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/apis/MovieProviderAPI.java
================================================
package com.ftpix.homedash.plugins.couchpotato.apis;

import com.ftpix.homedash.plugins.couchpotato.models.MovieObject;
import com.ftpix.homedash.plugins.couchpotato.models.MovieRequest;
import com.ftpix.homedash.plugins.couchpotato.models.MoviesRootFolder;
import com.ftpix.homedash.plugins.couchpotato.models.QualityProfile;
import com.mashape.unirest.http.exceptions.UnirestException;

import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public abstract class MovieProviderAPI {


    private final Function<String, String> cachePathToUrlPath;

    public MovieProviderAPI(Function<String, String> cachePathToUrlPath) {

        this.cachePathToUrlPath = cachePathToUrlPath;
    }


    public String getCachePathUrl(String path) {
        return cachePathToUrlPath.apply(path);
    }

    /**
     * Gets a ramdom poster path to display on the background of the module
     *
     * @return the relative URL to the file
     * @throws Exception
     */
    public abstract String getRandomWantedPoster() throws Exception;

    /**
     * Validates the settings of the instance.
     *
     * @return an empty map or null if everything is fine, a Map<String, String> with error titles and descriptions in case
     */
    public abstract Map<String, String> validateSettings();

    /**
     * Add a movie to the wanted list
     *
     * @param request
     * @throws UnsupportedEncodingException
     * @throws UnirestException
     */
    public abstract void addMovie(MovieRequest request) throws UnsupportedEncodingException, UnirestException;

    /**
     * Search for a movie
     *
     * @param query
     * @return a list of movies
     * @throws Exception
     */
    public abstract List<MovieObject> searchMovie(String query) throws Exception;

    /**
     * Get the base url of the installation
     *
     * @return
     */
    public abstract String getBaseUrl();


    /**
     * List down all the qualities available on the server
     *
     * @return
     * @throws Exception
     */
    public abstract List<QualityProfile> getQualityProfiles() throws Exception;


    /**
     * List down all the possible root folders for the movies
     *
     * @return
     * @throws Exception
     */
    public abstract List<MoviesRootFolder> getMoviesRootFolder() throws Exception;


    /**
     * Gets the name of the API
     *
     * @return
     */
    public abstract String getName();
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/apis/RadarrApi.java
================================================
package com.ftpix.homedash.plugins.couchpotato.apis;

import com.ftpix.homedash.plugins.couchpotato.apis.radarr.RadarrMovieRequest;
import com.ftpix.homedash.plugins.couchpotato.models.*;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.imgscalr.Scalr;
import org.json.JSONArray;
import org.json.JSONObject;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;

import static com.ftpix.homedash.plugins.couchpotato.CouchPotatoPlugin.THUMB_SIZE;

public class RadarrApi extends MovieProviderAPI {
    private final String baseUrl, url, apiKey;
    private final ImagePath imagePath;
    private final String API_MOVIE_SEARCH = "movie/lookup";
    private final String API_ADD_MOVIE = "movie.add/?title=[TITLE]&identifier=[IMDB]";
    private final String API_AVAILABLE = "system/status";
    private final String API_PROFILES = "profile";
    private final String API_ROOT_FOLDERS = "rootfolder";
    private final String API_MOVIE_LIST = "movie";
    private Logger logger = LogManager.getLogger();

    public RadarrApi(String baseUrl, String apiKey, ImagePath imagePath, Function<String, String> cachePathToUrlPath) {
        super(cachePathToUrlPath);
        this.baseUrl = baseUrl;
        this.imagePath = imagePath;

        url = baseUrl + "api/%s?apikey=" + apiKey;
        this.apiKey = apiKey;
    }

    @Override
    public String getRandomWantedPoster() throws Exception {

        final String apiUrl = String.format(url, API_MOVIE_LIST);
        final String body = Unirest.get(apiUrl).asString().getBody();
        JSONArray movies = new JSONArray(body);
        String poster = null;
        List<String> posters = new ArrayList<>();
        for (int i = 0; i < movies.length(); i++) {

            JSONObject movieInfo = movies.getJSONObject(i);
            JSONArray images = movieInfo.getJSONArray("images");
            if (images.length() != 0) {
                String imageUrl = null;
                for (int j = 0; j < images.length(); j++) {
                    JSONObject imageObject = images.getJSONObject(j);
                    if (imageObject.getString("coverType").equalsIgnoreCase("poster")) {
                        posters.add(imageObject.getString("url"));
                    }
                }

                // poster = images.getString(new

            }
        }

        Collections.shuffle(posters);
        if (posters.size() > 0) {
            String selected = posters.get(0);

            final int uselessParam = selected.indexOf("?");
            if (uselessParam != -1) {
                selected = selected.substring(0, uselessParam);
            }

            String fileName = imagePath.get() + selected.replaceAll("[^a-zA-Z0-9]", "") + ".jpg";
            File f = new File(fileName);
            if (!f.exists()) {
                String finalUrl = String.format(url, selected);
                FileUtils.copyURLToFile(new java.net.URL(finalUrl), f);
                BufferedImage image = ImageIO.read(f);
                BufferedImage resized = Scalr.resize(image, THUMB_SIZE);
                ImageIO.write(resized, Files.getFileExtension(f.getName()), f);
            }
            poster = getCachePathUrl(fileName);

        }

        // Random().nextInt(images.length()));

        return poster;
    }

    @Override
    public Map<String, String> validateSettings() {
        Map<String, String> errors = new Hashtable<String, String>();
        String url = String.format(this.url, API_AVAILABLE);
        try {
            Unirest.get(url).asString().getBody();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            logger.info("Can't access Radarr at URL [{}]", url);
            errors.put("Unavailable", "Radarr is not available at this URL: " + url);
        }

        return errors;
    }

    @Override
    public void addMovie(MovieRequest request) throws UnsupportedEncodingException, UnirestException {
        Gson gson = new Gson();
        RadarrMovieRequest radarrMovieRequest = gson.fromJson(request.getMovie().rawJson, RadarrMovieRequest.class);
        radarrMovieRequest.setMonitored(true);
        radarrMovieRequest.setQualityProfileId(request.getQuality().getId());
        radarrMovieRequest.setRootFolderPath(request.getFolder().getName());
        System.out.println(request);

        Unirest.post(String.format(url, API_MOVIE_LIST))
                .header("Content-Type", "application/json")
                .body(gson.toJson(radarrMovieRequest))
                .asString().getBody();
    }

    @Override
    public List<MovieObject> searchMovie(String query) throws Exception {
        String fullUrl = String.format(url, API_MOVIE_SEARCH) + "&term=" + URLEncoder.encode(query, StandardCharsets.UTF_8.name());
        String body = Unirest.get(fullUrl).asString().getBody();
        JSONArray movies = new JSONArray(body);
        List<MovieObject> moviesResults = new ArrayList<>();
        List<String> postersToDownload = new ArrayList<>();

        Function<JSONObject, MovieObject> toMovieObject = (movie) -> {
            MovieObject movieObject = new MovieObject();
            movieObject.rawJson = movie.toString();
            Optional.ofNullable(movie.getString("title")).ifPresent(s -> movieObject.originalTitle = s);
            Optional.ofNullable(movie.getInt("year")).ifPresent(s -> movieObject.year = s);
            Optional.ofNullable(movie.getInt("tmdbId")).ifPresent(s -> movieObject.imdbId = Integer.toString(s));
            Optional.ofNullable(movie.getBoolean("hasFile")).ifPresent(s -> movieObject.inLibrary = s);
            Optional.ofNullable(movie.getBoolean("monitored")).ifPresent(s -> movieObject.wanted = s);

            JSONArray images = movie.getJSONArray("images");
            for (int j = 0; j < images.length(); j++) {

                JSONObject imageObject = images.getJSONObject(j);
                if (imageObject.getString("coverType").equalsIgnoreCase("poster")) {
                    movieObject.poster = imageObject.getString("url");
                    break;
                }
            }
            return movieObject;
        };


        for (int i = 0; i < movies.length(); i++) {
            JSONObject movie = movies.getJSONObject(i);
            moviesResults.add(toMovieObject.apply(movie));
        }

        //Getting all movies to compare and see if it is already in library
        body = Unirest.get(String.format(url, API_MOVIE_LIST)).asString().getBody();
        JSONArray libraryMovies = new JSONArray(body);
        List<MovieObject> library = new ArrayList<>();

        for (int i = 0; i < libraryMovies.length(); i++) {
            JSONObject movie = libraryMovies.getJSONObject(i);
            library.add(toMovieObject.apply(movie));
        }


        List<Callable<Void>> downloads = new ArrayList<>();
        moviesResults.forEach(m -> downloads.add(() -> {
            //setting movie state from library
            library.stream()
                    .filter(l -> {
                        return l.imdbId.equalsIgnoreCase(m.imdbId);
                    })
                    .findFirst()
                    .ifPresent(l -> {
                        m.inLibrary = l.inLibrary;
                        m.wanted = l.wanted;
                    });

            //If we already have the movie, let's mark it has not wanted, even if in monitored state in Radarr
            if (m.inLibrary) m.wanted = false;

            try {
                String fileName = imagePath.get() + m.poster.replaceAll("[^a-zA-Z0-9]", "") + ".jpg";
                File f = new File(fileName);
                if (!f.exists()) {
                    FileUtils.copyURLToFile(new java.net.URL(m.poster), f);
                    BufferedImage image = ImageIO.read(f);
                    BufferedImage resized = Scalr.resize(image, THUMB_SIZE);
                    ImageIO.write(resized, Files.getFileExtension(f.getName()), f);
                }

                m.poster = getCachePathUrl(f.getPath());

            } catch (Exception e) {
                logger.info("Couldn't download movie poster at url: [{}]", m.poster);
            }
            return null;
        }));

        ExecutorService exec = Executors.newFixedThreadPool(moviesResults.size());
        try {
            exec.invokeAll(downloads);
        } finally {
            exec.shutdown();
        }

        return moviesResults;
    }

    @Override
    public String getBaseUrl() {
        return baseUrl;
    }

    @Override
    public List<QualityProfile> getQualityProfiles() throws Exception {
        List<QualityProfile> profiles = new ArrayList<>();
        String body = Unirest.get(String.format(url, API_PROFILES)).asString().getBody();
        JSONArray profilesJson = new JSONArray(body);
        for (int i = 0; i < profilesJson.length(); i++) {
            JSONObject profileJson = profilesJson.getJSONObject(i);
            QualityProfile quality = new QualityProfile();
            quality.setName(profileJson.getString("name"));
            quality.setId(profileJson.getInt("id"));

            profiles.add(quality);
        }

        return profiles;
    }

    @Override
    public List<MoviesRootFolder> getMoviesRootFolder() throws Exception {
        List<MoviesRootFolder> folders = new ArrayList<>();
        JSONArray foldersJson = new JSONArray(Unirest.get(String.format(url, API_ROOT_FOLDERS)).asString().getBody());
        for (int i = 0; i < foldersJson.length(); i++) {
            JSONObject folderJson = foldersJson.getJSONObject(i);
            MoviesRootFolder folder = new MoviesRootFolder();
            folder.setName(folderJson.getString("path"));
            folder.setId(folderJson.getInt("id"));

            folders.add(folder);
        }

        return folders;
    }

    @Override
    public String getName() {
        return "Radarr";
    }
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/apis/radarr/RadarrImage.java
================================================
package com.ftpix.homedash.plugins.couchpotato.apis.radarr;

public class RadarrImage {
    private String coverType, url;

    public String getCoverType() {
        return coverType;
    }

    public void setCoverType(String coverType) {
        this.coverType = coverType;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/apis/radarr/RadarrMovieRequest.java
================================================
package com.ftpix.homedash.plugins.couchpotato.apis.radarr;

import java.util.List;

public class RadarrMovieRequest {
    private String title, titleSlug, rootFolderPath, year;
    private int tmdbId, qualityProfileId;
    private boolean monitored;
    private List<RadarrImage> images;


    public List<RadarrImage> getImages() {
        return images;
    }

    public void setImages(List<RadarrImage> images) {
        this.images = images;
    }

    public boolean isMonitored() {
        return monitored;
    }

    public void setMonitored(boolean monitored) {
        this.monitored = monitored;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getTitleSlug() {
        return titleSlug;
    }

    public void setTitleSlug(String titleSlug) {
        this.titleSlug = titleSlug;
    }

    public String getRootFolderPath() {
        return rootFolderPath;
    }

    public void setRootFolderPath(String rootFolderPath) {
        this.rootFolderPath = rootFolderPath;
    }

    public int getTmdbId() {
        return tmdbId;
    }

    public void setTmdbId(int tmdbId) {
        this.tmdbId = tmdbId;
    }

    public int getQualityProfileId() {
        return qualityProfileId;
    }

    public void setQualityProfileId(int qualityProfileId) {
        this.qualityProfileId = qualityProfileId;
    }

    public String getYear() {
        return year;
    }

    public void setYear(String year) {
        this.year = year;
    }
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/ImagePath.java
================================================
package com.ftpix.homedash.plugins.couchpotato.models;

@FunctionalInterface
public interface ImagePath {
    String get();
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/MovieObject.java
================================================
package com.ftpix.homedash.plugins.couchpotato.models;

/**
 * Created by gz on 22-Jun-16.
 */
public class MovieObject {
    public boolean inLibrary;
    public String originalTitle;
    public int year;
    public boolean wanted;
    public String imdbId;
    public String poster;
    public String rawJson;
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/MovieProviderBuilder.java
================================================
package com.ftpix.homedash.plugins.couchpotato.models;

import com.ftpix.homedash.plugins.couchpotato.apis.MovieProviderAPI;

import java.util.function.Function;

@FunctionalInterface
public interface MovieProviderBuilder {
    MovieProviderAPI build(String url, String apiKey, ImagePath imagePath, Function<String, String> cacheToPath);
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/MovieRequest.java
================================================
package com.ftpix.homedash.plugins.couchpotato.models;

public class MovieRequest {
    private MovieObject movie;
    private MoviesRootFolder folder;
    private QualityProfile quality;


    public MovieObject getMovie() {
        return movie;
    }

    public void setMovie(MovieObject movie) {
        this.movie = movie;
    }

    public MoviesRootFolder getFolder() {
        return folder;
    }

    public void setFolder(MoviesRootFolder folder) {
        this.folder = folder;
    }

    public QualityProfile getQuality() {
        return quality;
    }

    public void setQuality(QualityProfile quality) {
        this.quality = quality;
    }
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/MoviesRootFolder.java
================================================
package com.ftpix.homedash.plugins.couchpotato.models;

public class MoviesRootFolder {
    private String name;
    private int id;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/QualityProfile.java
================================================
package com.ftpix.homedash.plugins.couchpotato.models;

public class QualityProfile {
    private String name;
    private int id;


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}


================================================
FILE: plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/Type.java
================================================
package com.ftpix.homedash.plugins.couchpotato.models;

public enum Type {
    COUCHPOTATO,
    RADARR;

}


================================================
FILE: plugins/docker/.gitignore
================================================
/target/


================================================
FILE: plugins/docker/pom.xml
================================================
<?xml version="1.0"?>
<project
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
        xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.ftpix.homedash.plugins</groupId>
        <artifactId>plugins</artifactId>
        <version>2021.1</version>
    </parent>

    <artifactId>docker</artifactId>
    <name>docker</name>
    <url>http://maven.apache.org</url>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.spotify</groupId>
            <artifactId>docker-client</artifactId>
            <version>8.11.7</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.lesscss</groupId>
                <artifactId>lesscss-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>


================================================
FILE: plugins/docker/src/main/java/assets/js/docker.js
================================================
function docker(moduleId) {
    this.moduleId = moduleId;
    this.selectedContainer;
    this.selectedImage;
    this.lastLine = 0;

    this.documentReady = function (size) {
        var root = rootElement(this.moduleId);
        var self = this;
        if (size == 'full-screen') {

            root.on('click', '.container-modal', function () {
                self.showContainerModal($(this));
            });

            root.on('click', '.container-logs', function () {
                self.showContainerLogs($(this));
            });

            root.on('click', '.image-modal', function () {
                self.showImageModal($(this));
            });

            root.on('click', '.action', function (event) {
                if (self.selectedContainer !== undefined) {
                    sendMessage(self.moduleId, $(this).attr('data-action'), self.selectedContainer);
                    root.find('.modal').modal('hide');
                }
            });


            root.on('click', '.image-action', function (event) {
                if (self.selectedImage !== undefined) {
                    sendMessage(self.moduleId, $(this).attr('data-action'), self.selectedImage);
                    root.find('.modal').modal('hide');
                }
            });

        }
    };

    this.onConnect = function () {

    };

    this.onMessage = function (size, command, message, extra) {
        switch (size) {
            case '1x1':
                this.onMessage_1x1(command, message, extra);
                break;
            case 'full-screen':
                this.onMessage_fullScreen(command, message, extra);
                break;
        }
    }

    this.onMessage_1x1 = function (command, message, extra) {
        var root = rootElement(this.moduleId);

        root.find('.count').html(message);
    };

    this.onMessage_fullScreen = function (command, message, extra) {
        if (command === 'refresh') {
            this.refreshContainers(message.containers);
            this.refreshImages(message.images);
        } else if (command === 'success') {
            this.refreshContainers(extra.containers);
            this.refreshImages(extra.images);
        } else if (command === 'details') {
            this.showContainerDetails(message);
        } else if (command === 'logs') {
            this.displayLogs(message);
        }
    };


    this.showImageModal = function (source) {
        var root = rootElement(this.moduleId);

        var modal = root.find('#image-modal');
        this.selectedImage = source.attr('data-id');
        modal.find('.modal-title').html(source.attr('data-name'));
        modal.find('.id').html(this.selectedImage);

        modal.modal('show');

    };


    this.displayLogs = function (logLines) {
        var root = rootElement(this.moduleId);

        var logs = $('#container-logs-modal .logs');
        var modal = root.find('#container-logs-modal');
        this.lastLine = logLines.total;

        if (modal.hasClass('in')) {
            this.requestForLogs(modal.attr('data-id'));
        }

        if (logLines.lines.length > 0) {
            logs.val(logs.val() + '\n' + logLines.lines.join('\n'));
            logs.scrollTop(logs[0].scrollHeight - logs.height());
        }
    };

    this.showContainerLogs = function (source) {
        this.lastLine = 0;
        var root = rootElement(this.moduleId);

        var logs = $('#container-logs-modal .logs');
        logs.val('');

        var modal = root.find('#container-logs-modal');
        modal.attr('data-id', source.attr('data-id'));
        modal.find('.modal-title').html(source.attr('data-name') + ' logs');
        modal.modal('show');

        this.requestForLogs(source.attr('data-id'));
    };


    this.requestForLogs = function (containerId) {
        var self = this;
        setTimeout(function () {
            console.log('refreshing logs');
            sendMessage(self.moduleId, 'logs', containerId, self.lastLine);
        }, 1000);
    };

    this.showContainerModal = function (source) {
        var root = rootElement(this.moduleId);

        var modal = root.find('#container-modal');
        this.selectedContainer = source.attr('data-id');
        var status = root.find(
            '.containers tr[data-id="' + this.selectedContainer + '"] .status').html();

        modal.find('.modal-header h4').html(source.attr('data-name'));

        modal.find('.id').html(this.selectedContainer);

        modal.find('.action').hide();
        if (status.startsWith('Up')) {
            modal.find('.action.running').show();
        } else {
            modal.find('.action.not-running').show();
        }

        modal.find('.container-info').html('<div class="loader"></div>');
        sendMessage(this.moduleId, 'details', this.selectedContainer);

        modal.modal('show');
    };

    this.showContainerDetails = function (data) {
        var root = rootElement(this.moduleId);

        var html = [];

        if (data.config.image.length > 0) {
            html.push(this.infoPanel('Image', data.config.image));
        }

        if (data.hostConfig.restartPolicy.name !== undefined) {
            html.push(this.infoPanel('Restart policy', data.hostConfig.restartPolicy.name));
        }

        if (data.networkSettings.networks !== undefined) {
            var network = [];
            $.each(data.networkSettings.networks, function (index, value) {
                network.push('<h4>', index, '</h4>');

                if (value.aliases !== undefined) {
                    network.push('<p><strong>Aliases: </strong>', value.aliases.join(', '), '</p>');
                }

                if (value.gateway.length > 0) {
                    network.push('<p><strong>Gateway: </strong>', value.gateway, '</p>');
                }

                if (value.ipAddress.length > 0) {
                    network.push('<p><strong>IP Address: </strong>', value.ipAddress, '</p>');
                }

                if (value.macAddress.length > 0) {
                    network.push('<p><strong>MAC Adress: </strong>', value.macAddress, '</p>');
                }
                network.push('<p>', '</p>');
                network.push('<hr />')
            });

            html.push(this.infoPanel('Networks', network.join('')));
        }

        if (data.networkSettings.ports !== undefined) {
            var network = [];
            $.each(data.networkSettings.ports, function (index, value) {
                if (value.length > 0) {
                    console.log('Port:', value);
                    network.push('<p>');
                    network.push(value[0].hostPort,
                        ' <i class="fa fa-arrow-right" aria-hidden="true"></i> ', index);
                    network.push('</p>');
                }
            });

            if (network.length > 0) {
                html.push(this.infoPanel('Ports', network.join('')));
            }
        }


        var mounts = [];
        $.each(data.mounts, function (index, value) {
            mounts.push('<p>');
            mounts.push(value.source, ' <i class="fa fa-arrow-right" aria-hidden="true"></i> ',
                value.destination, ':', value.mode);
            mounts.push('</p>');
        });

        html.push(this.infoPanel("Mounts", mounts.join('')));

        console.log('ENV', data.config.env)
        if (data.config.env !== undefined && data.config.env.length > 0) {
            var env = [];
            $.each(data.config.env, function (index, value) {
                var split = value.split('\u003d');
                env.push('<p><strong>', split[0], ': </strong>', split[1], '</p>');
            });

            html.push(this.infoPanel('Environment variables', env.join('')));
        }

        root.find('.modal .container-info').html(html.join(''));
    };

    this.infoPanel = function (title, content) {
        var html = [];

        //
        html.push('<div class="panel panel-default">');
        html.push('<div class="panel-heading">', title, '</div>');
        html.push('<div class="panel-body">');
        html.push(content);
        html.push('</div>');
        html.push('</div>');

        return html.join('');
    };

    this.refreshImages = function (images) {

        var root = rootElement(this.moduleId);

        var html = [];
        $.each(images, function (index, image) {
            html.push('<tr data-id="', image.id, '">');

            html.push('<td>', image.tag, '</td>');
            html.push('<td>', image.usedBy, '</td>');
            html.push('<td>', image.size, '</td>');
            html.push('<td>', image.created, '</td>');
            html.push('<td class="image-modal" data-id="', image.id, '", data-name="',
                image.tag, '">',
                '<i class="fa fa-info-circle" aria-hidden="true"></i>',
                '</td>');

            html.push('</tr>');
        });

        root.find('.images tbody').html(html.join(''));
    }


    this.refreshContainers = function (message) {

        var root = rootElement(this.moduleId);

        var html = [];
        $.each(message, function (index, container) {
            html.push('<tr data-id="', container.id, '">');

            html.push('<td>', container.names.join(','), '</td>');
            html.push('<td>', container.image, '</td>');
            html.push('<td class="status">', container.status, '</td>');
            html.push('<td>', container.memoryUsagePretty, '</td>');
            html.push('<td>', container.bytesReceivedPretty, '</td>');
            html.push('<td>', container.bytesSentPretty, '</td>');
            html.push('<td class="container-modal" data-id="', container.id, '", data-name="',
                container.names.join(','), '">',
                '<i class="fa fa-info-circle" aria-hidden="true"></i>',
                '</td>');
            html.push('<td class="container-logs" data-id="', container.id, '", data-name="',
                container.names.join(','), '">',
                '<button class="btn btn-sm btn-default"><i class="fa fa-info-circle" aria-hidden="true"></i> logs</button>',
                '</td>');

            html.push('</tr>');
        });

        root.find('.containers tbody').html(html.join(''));
    }

}

================================================
FILE: plugins/docker/src/main/java/assets/less/docker.less
================================================
.docker.size-1x1 {
  background-color: #0087C9;
  color: white;
  background-image: url("../files/images/docker.svg");
  background-repeat: no-repeat;
  background-size: cover;

  h1 {
    text-align: center;
    margin-top: 10px;
  }

  p.header {
    margin: 0 2px;
  }

  p.footer {
    text-align: center;
    font-size: 11px;
  }
}

.docker.size-full-screen {
  .image-modal, .container-modal {
    cursor: pointer;
  }

  #container-logs-modal {
    .modal-dialog {
      height: 100%;
      width: 100%;
      margin: 0;
      padding: 30px;
      box-sizing: border-box;

      .modal-content {
        height: 100%;

        .modal-body {
          padding-bottom: 70px;
          height: 100%;

          .logs {
            width: 100%;
            height: 100%;
          }
        }
      }
    }
  }
}

================================================
FILE: plugins/docker/src/main/java/assets/templates/docker-1x1.jade
================================================
p.header Docker
h1.count ...
p.footer running containers

================================================
FILE: plugins/docker/src/main/java/assets/templates/docker-full-screen.jade
================================================

div
    ul.nav.nav-tabs(role="tablist")
        li.active(role="presentation")
            a(href="#containers", aria-controls="containers", role="tab", data-toggle="tab") Containers
        li(role="presentation")
            a(href="#images", aria-controls="images", role="tab", data-toggle="tab") Images

    .tab-content
        #containers.tab-pane.active(role="tabpanel")
            .table-responsive
                table.table.containers.table-striped.table-hover
                    thead
                        tr
                            th Names
                            th Image
                            th Status
                            th Memory
                            th Bytes received
                            th Bytes sent
                            th
                            th
                    tbody
                        tr
                            td(colspan="8") Loading...
        #images.tab-pane(role="tabpanel")
            .table-responsive
                table.table.images.table-striped.table-hover
                    thead
                        tr
                            th Labels
                            th Used by
                            th Size
                            th Created
                            th
                    tbody
                        tr
                            td(colspan="5") Loading...


include ./docker-modal
include ./docker-image-modal
include ./docker-logs-modal


================================================
FILE: plugins/docker/src/main/java/assets/templates/docker-image-modal.jade
================================================
#image-modal.modal.fade(tabindex="-1", role="dialog")
    .modal-dialog(role="document")
        .modal-content
            .modal-header
                button.close(type="button", data-dismiss="modal", aria-label="Close")
                    span(aria=hidden="true") &times;
                h4.modal-title
            .modal-body
                p
                    strong ID:&nbsp;
                    span.id
                button.btn.btn-warning.image-action.not-running(data-
gitextract_7__wbq9_/

├── .drone.yml
├── .gitattributes
├── .gitignore
├── DevelopPlugin.md
├── LICENSE
├── README.md
├── docs/
│   ├── index.html
│   └── style.css
├── models/
│   ├── .gitignore
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   └── java/
│       │       └── com/
│       │           └── ftpix/
│       │               └── homedash/
│       │                   ├── Utils/
│       │                   │   ├── ByteUtils.java
│       │                   │   └── HomeDashClassPathTemplateLoader.java
│       │                   ├── models/
│       │                   │   ├── ExposedModule.java
│       │                   │   ├── ExternalEndPointDefinition.java
│       │                   │   ├── Layout.java
│       │                   │   ├── Module.java
│       │                   │   ├── ModuleData.java
│       │                   │   ├── ModuleExposedData.java
│       │                   │   ├── ModuleLayout.java
│       │                   │   ├── ModuleLocation.java
│       │                   │   ├── ModuleSettings.java
│       │                   │   ├── Page.java
│       │                   │   ├── RemoteFavorite.java
│       │                   │   ├── Schema.java
│       │                   │   ├── Settings.java
│       │                   │   ├── Version.java
│       │                   │   ├── WebSocketMessage.java
│       │                   │   ├── WebSocketSession.java
│       │                   │   └── export/
│       │                   │       ├── Export.java
│       │                   │       ├── LayoutExport.java
│       │                   │       ├── ModuleExport.java
│       │                   │       ├── ModuleLayoutExport.java
│       │                   │       └── PageExport.java
│       │                   └── plugins/
│       │                       ├── Plugin.java
│       │                       └── PluginListener.java
│       └── test/
│           └── java/
│               └── com/
│                   └── ftpix/
│                       └── homedash/
│                           └── plugins/
│                               └── AppTest.java
├── notifications/
│   ├── pom.xml
│   └── src/
│       └── main/
│           └── java/
│               └── com/
│                   └── ftpix/
│                       └── homedash/
│                           └── notifications/
│                               ├── NotificationProvider.java
│                               ├── Notifications.java
│                               └── implementations/
│                                   ├── PushBullet.java
│                                   ├── PushOver.java
│                                   └── Pushalot.java
├── plugins/
│   ├── couchpotato/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── couchpotato.js
│   │               │   ├── less/
│   │               │   │   └── couchpotato.less
│   │               │   └── templates/
│   │               │       ├── couchpotato-1x1.jade
│   │               │       ├── couchpotato-1x3.jade
│   │               │       ├── couchpotato-2x1.jade
│   │               │       ├── couchpotato-2x2.jade
│   │               │       ├── couchpotato-2x3.jade
│   │               │       ├── couchpotato-3x2.jade
│   │               │       ├── couchpotato-3x3.jade
│   │               │       ├── couchpotato-settings.jade
│   │               │       └── cp-modal.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── couchpotato/
│   │                                   ├── CouchPotatoPlugin.java
│   │                                   ├── apis/
│   │                                   │   ├── CouchPotatoApi.java
│   │                                   │   ├── MovieProviderAPI.java
│   │                                   │   ├── RadarrApi.java
│   │                                   │   └── radarr/
│   │                                   │       ├── RadarrImage.java
│   │                                   │       └── RadarrMovieRequest.java
│   │                                   └── models/
│   │                                       ├── ImagePath.java
│   │                                       ├── MovieObject.java
│   │                                       ├── MovieProviderBuilder.java
│   │                                       ├── MovieRequest.java
│   │                                       ├── MoviesRootFolder.java
│   │                                       ├── QualityProfile.java
│   │                                       └── Type.java
│   ├── docker/
│   │   ├── .gitignore
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── docker.js
│   │               │   ├── less/
│   │               │   │   └── docker.less
│   │               │   └── templates/
│   │               │       ├── docker-1x1.jade
│   │               │       ├── docker-full-screen.jade
│   │               │       ├── docker-image-modal.jade
│   │               │       ├── docker-logs-modal.jade
│   │               │       ├── docker-modal.jade
│   │               │       └── docker-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── docker/
│   │                                   ├── DockerPlugin.java
│   │                                   └── models/
│   │                                       ├── DockerExtendedInfo.java
│   │                                       ├── DockerImageInfo.java
│   │                                       └── DockerInfo.java
│   ├── dockercompose/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── dockercompose.js
│   │               │   ├── less/
│   │               │   │   └── dockercompose.less
│   │               │   └── templates/
│   │               │       ├── dockercompose-1x1.jade
│   │               │       ├── dockercompose-2x1.jade
│   │               │       ├── dockercompose-full-screen.jade
│   │               │       ├── dockercompose-modal.jade
│   │               │       └── dockercompose-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── dockercompose/
│   │                                   ├── DockerComposePlugin.java
│   │                                   ├── exceptions/
│   │                                   │   └── CommandException.java
│   │                                   └── models/
│   │                                       ├── CommandOutput.java
│   │                                       └── DockerContainer.java
│   ├── dynamicdns/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── dynamicdns.js
│   │               │   ├── less/
│   │               │   │   └── dynamicdns.less
│   │               │   └── templates/
│   │               │       ├── dynamicdns-2x1.jade
│   │               │       └── dynamicdns-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── dynamicdns/
│   │                                   ├── DynamicDnsPlugin.java
│   │                                   ├── inputs/
│   │                                   │   ├── FormInput.java
│   │                                   │   └── FormType.java
│   │                                   ├── models/
│   │                                   │   ├── Ip.java
│   │                                   │   └── IpFromWeb.java
│   │                                   └── providers/
│   │                                       ├── DynDNSProvider.java
│   │                                       └── implementations/
│   │                                           ├── DynDNS.java
│   │                                           ├── NoIP.java
│   │                                           ├── OVH.java
│   │                                           └── StandardProvider.java
│   ├── googlepubliccalendar/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── googlepubliccalendar.js
│   │               │   ├── less/
│   │               │   │   └── googlepubliccalendar.less
│   │               │   └── templates/
│   │               │       ├── googlepubliccalendar-3x1.jade
│   │               │       ├── googlepubliccalendar-3x4.jade
│   │               │       ├── googlepubliccalendar-4x4.jade
│   │               │       ├── googlepubliccalendar-full-screen.jade
│   │               │       ├── googlepubliccalendar-modal.jade
│   │               │       └── googlepubliccalendar-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── googlepubliccalendar/
│   │                                   └── GooglePublicCalendarPlugin.java
│   ├── harddisk/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── harddisk.js
│   │               │   ├── less/
│   │               │   │   └── harddisk.less
│   │               │   └── templates/
│   │               │       ├── harddisk-1x1.jade
│   │               │       ├── harddisk-2x1.jade
│   │               │       ├── harddisk-full-screen.jade
│   │               │       ├── harddisk-kiosk.jade
│   │               │       └── harddisk-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── harddisk/
│   │                                   ├── DiskFile.java
│   │                                   ├── FileOperation.java
│   │                                   ├── HarddiskPlugin.java
│   │                                   └── OperationException.java
│   ├── kvm/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── kvm.js
│   │               │   ├── less/
│   │               │   │   └── kvm.less
│   │               │   └── templates/
│   │               │       ├── kvm-1x1.jade
│   │               │       ├── kvm-full-screen.jade
│   │               │       └── kvm-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── kvm/
│   │                                   ├── KvmPlugin.java
│   │                                   ├── VMAction.java
│   │                                   ├── VMActionResponse.java
│   │                                   └── VMInfo.java
│   ├── logreader/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── logreader.js
│   │               │   ├── less/
│   │               │   │   └── logreader.less
│   │               │   └── templates/
│   │               │       ├── logreader-1x1.jade
│   │               │       ├── logreader-full-screen.jade
│   │               │       └── logreader-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── logreader/
│   │                           └── LogReaderPlugin.java
│   ├── mma/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── mma.js
│   │               │   ├── less/
│   │               │   │   └── mma.less
│   │               │   └── templates/
│   │               │       ├── mma-3x2.jade
│   │               │       ├── mma-3x4.jade
│   │               │       ├── mma-4x4.jade
│   │               │       ├── mma-full-screen.jade
│   │               │       ├── mma-kiosk.jade
│   │               │       ├── mma-modal.jade
│   │               │       └── mma-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── mma/
│   │                                   ├── MmaPlugin.java
│   │                                   └── model/
│   │                                       ├── HomeDashEvent.java
│   │                                       └── HomeDashOrganization.java
│   ├── networkmonitor/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── networkmonitor.js
│   │               │   ├── less/
│   │               │   │   └── networkmonitor.less
│   │               │   └── templates/
│   │               │       ├── graph.jade
│   │               │       ├── networkmonitor-2x1.jade
│   │               │       ├── networkmonitor-3x2.jade
│   │               │       ├── networkmonitor-kiosk.jade
│   │               │       └── networkmonitor-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── networkmonitor/
│   │                                   ├── NetworkMonitorPlugin.java
│   │                                   └── models/
│   │                                       └── NetworkInfo.java
│   ├── pihole/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── pihole.js
│   │               │   ├── less/
│   │               │   │   └── pihole.less
│   │               │   └── templates/
│   │               │       ├── pihole-1x1.jade
│   │               │       ├── pihole-2x2.jade
│   │               │       ├── pihole-3x2.jade
│   │               │       ├── pihole-4x2.jade
│   │               │       ├── pihole-full-screen.jade
│   │               │       ├── pihole-kiosk.jade
│   │               │       └── pihole-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── pihole/
│   │                                   ├── PiHoleClient.java
│   │                                   ├── PiHolePlugin.java
│   │                                   ├── UnauthorizedException.java
│   │                                   └── models/
│   │                                       ├── AnswerType.java
│   │                                       ├── PiHoleQuery.java
│   │                                       └── PiHoleStats.java
│   ├── plex/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── plex.js
│   │               │   ├── less/
│   │               │   │   └── plex.less
│   │               │   └── templates/
│   │               │       ├── plex-1x1.jade
│   │               │       ├── plex-2x1.jade
│   │               │       ├── plex-3x2.jade
│   │               │       ├── plex-3x3.jade
│   │               │       ├── plex-4x4.jade
│   │               │       ├── plex-kiosk.jade
│   │               │       └── plex-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugin/
│   │                               └── plex/
│   │                                   ├── PlexPlugin.java
│   │                                   ├── PlexResultParser.java
│   │                                   ├── api/
│   │                                   │   ├── ApiType.java
│   │                                   │   ├── MediaServerApi.java
│   │                                   │   └── impl/
│   │                                   │       ├── JellyFinApi.java
│   │                                   │       └── PlexApi.java
│   │                                   └── model/
│   │                                       ├── JellyfinNowPlaying.java
│   │                                       ├── JellyfinPlayState.java
│   │                                       ├── JellyfinSession.java
│   │                                       ├── MediaContainer.java
│   │                                       ├── NowPlaying.java
│   │                                       ├── Player.java
│   │                                       ├── PlexSession.java
│   │                                       └── Video.java
│   ├── pom.xml
│   ├── portmapper/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── portmapper.js
│   │               │   ├── less/
│   │               │   │   └── portmapper.less
│   │               │   └── templates/
│   │               │       ├── pm-modal.jade
│   │               │       ├── portmapper-2x1.jade
│   │               │       ├── portmapper-5x5.jade
│   │               │       ├── portmapper-6x5.jade
│   │               │       └── portmapper-full-screen.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── portmapper/
│   │                                   ├── MappingObject.java
│   │                                   ├── PortMapperPlugin.java
│   │                                   └── RouterObject.java
│   ├── sonarrtv/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── sonarrtv.js
│   │               │   ├── less/
│   │               │   │   └── sonarrtv.less
│   │               │   └── templates/
│   │               │       ├── sonarrtv-2x2.jade
│   │               │       ├── sonarrtv-3x1.jade
│   │               │       ├── sonarrtv-3x3.jade
│   │               │       ├── sonarrtv-4x4.jade
│   │               │       ├── sonarrtv-full-screen.jade
│   │               │       ├── sonarrtv-kiosk.jade
│   │               │       └── sonarrtv-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               ├── SonarrTvPlugin.java
│   │                               └── api/
│   │                                   ├── SonarrApi.java
│   │                                   ├── SonarrUnauthorizedException.java
│   │                                   └── models/
│   │                                       ├── SearchResults.java
│   │                                       └── SonarrCalendar.java
│   ├── spotify/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── spotify.js
│   │               │   ├── less/
│   │               │   │   └── spotify.less
│   │               │   └── templates/
│   │               │       ├── spotify-1x1.jade
│   │               │       ├── spotify-2x1.jade
│   │               │       ├── spotify-2x2.jade
│   │               │       ├── spotify-3x2.jade
│   │               │       ├── spotify-3x3.jade
│   │               │       ├── spotify-4x4.jade
│   │               │       ├── spotify-kiosk.jade
│   │               │       └── spotify-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               └── spotify/
│   │                                   ├── SpotifyPlugin.java
│   │                                   ├── SpotifyToken.java
│   │                                   └── models/
│   │                                       ├── Album.java
│   │                                       ├── Artist.java
│   │                                       ├── Error.java
│   │                                       ├── Image.java
│   │                                       ├── Item.java
│   │                                       └── SpotifyNowPlaying.java
│   ├── systeminfo/
│   │   ├── .gitignore
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── systeminfo.js
│   │               │   ├── less/
│   │               │   │   └── systeminfo.less
│   │               │   └── templates/
│   │               │       ├── systeminfo-1x1.jade
│   │               │       ├── systeminfo-2x1.jade
│   │               │       ├── systeminfo-full-screen.jade
│   │               │       ├── systeminfo-kiosk.jade
│   │               │       └── systeminfo-settings.jade
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               ├── SystemInfoPlugin.java
│   │                               └── models/
│   │                                   ├── CpuInfo.java
│   │                                   ├── HardwareInfo.java
│   │                                   ├── OsInfo.java
│   │                                   ├── Process.java
│   │                                   ├── RamInfo.java
│   │                                   └── SystemInfoData.java
│   ├── transmission/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               ├── assets/
│   │               │   ├── js/
│   │               │   │   └── transmission.js
│   │               │   ├── less/
│   │               │   │   └── transmission.less
│   │               │   └── templates/
│   │               │       ├── transmission-2x1.jade
│   │               │       ├── transmission-2x2.jade
│   │               │       ├── transmission-3x2.jade
│   │               │       ├── transmission-full-screen.jade
│   │               │       ├── transmission-kiosk.jade
│   │               │       └── transmission-settings.jade
│   │               ├── ca/
│   │               │   └── benow/
│   │               │       └── transmission/
│   │               │           ├── AddTorrentParameters.java
│   │               │           ├── Base64.java
│   │               │           ├── SetTorrentParameters.java
│   │               │           ├── TorrentParameters.java
│   │               │           ├── TransmissionClient.java
│   │               │           ├── TransmissionException.java
│   │               │           ├── model/
│   │               │           │   ├── AddedTorrentInfo.java
│   │               │           │   ├── JSONAccessor.java
│   │               │           │   ├── SessionStatus.java
│   │               │           │   ├── TorrentStatus.java
│   │               │           │   ├── TrackerPair.java
│   │               │           │   ├── TransmissionSession.java
│   │               │           │   └── package.html
│   │               │           └── package.html
│   │               └── com/
│   │                   └── ftpix/
│   │                       └── homedash/
│   │                           └── plugins/
│   │                               ├── TransmissionPlugin.java
│   │                               └── models/
│   │                                   ├── TorrentObject.java
│   │                                   └── TorrentSession.java
│   └── unifi/
│       ├── pom.xml
│       └── src/
│           └── main/
│               └── java/
│                   ├── assets/
│                   │   ├── js/
│                   │   │   └── unifi.js
│                   │   ├── less/
│                   │   │   └── unifi.less
│                   │   └── templates/
│                   │       ├── unifi-1x1.jade
│                   │       └── unifi-settings.jade
│                   └── com/
│                       └── ftpix/
│                           └── homedash/
│                               └── plugins/
│                                   └── unifi/
│                                       ├── UnifiApi.java
│                                       ├── UnifiPlugin.java
│                                       ├── UnifiResponse.java
│                                       └── UnifiThroughPut.java
├── pom.xml
├── updater/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── ftpix/
│       │   │           └── homedash/
│       │   │               └── updater/
│       │   │                   ├── Updater.java
│       │   │                   └── exceptions/
│       │   │                       ├── WrongInstallPathStructure.java
│       │   │                       └── WrongVersionPatternException.java
│       │   └── resources/
│       │       ├── log4j2.xml
│       │       └── version.properties
│       └── test/
│           └── java/
│               └── com/
│                   └── ftpix/
│                       └── homedash/
│                           └── updater/
│                               └── UpdaterTest.java
└── web/
    ├── .gitignore
    ├── assembly.xml
    ├── docker/
    │   ├── Dockerfile
    │   └── run.sh
    ├── pom.xml
    ├── src/
    │   ├── main/
    │   │   ├── java/
    │   │   │   ├── assets/
    │   │   │   │   ├── js/
    │   │   │   │   │   ├── add-remote.js
    │   │   │   │   │   ├── full-screen.js
    │   │   │   │   │   ├── index.js
    │   │   │   │   │   ├── ios-script.js
    │   │   │   │   │   ├── kiosk.js
    │   │   │   │   │   ├── layout-settings.js
    │   │   │   │   │   ├── pages.js
    │   │   │   │   │   ├── settings.js
    │   │   │   │   │   └── websocket.js
    │   │   │   │   └── less/
    │   │   │   │       ├── dropdown-multilevel.less
    │   │   │   │       ├── full-screen.less
    │   │   │   │       ├── index.less
    │   │   │   │       ├── kiosk.less
    │   │   │   │       ├── layout-settings.less
    │   │   │   │       ├── loader.less
    │   │   │   │       ├── login.less
    │   │   │   │       ├── main.less
    │   │   │   │       ├── module-modal.less
    │   │   │   │       ├── pages.less
    │   │   │   │       ├── remotes.less
    │   │   │   │       ├── settings.less
    │   │   │   │       ├── variables.less
    │   │   │   │       └── vendor/
    │   │   │   │           ├── bootstrap/
    │   │   │   │           │   ├── bootstrap.less
    │   │   │   │           │   └── config.json
    │   │   │   │           ├── font.less
    │   │   │   │           └── font_awesome/
    │   │   │   │               ├── animated.less
    │   │   │   │               ├── bordered-pulled.less
    │   │   │   │               ├── core.less
    │   │   │   │               ├── fixed-width.less
    │   │   │   │               ├── font-awesome.less
    │   │   │   │               ├── icons.less
    │   │   │   │               ├── larger.less
    │   │   │   │               ├── list.less
    │   │   │   │               ├── mixins.less
    │   │   │   │               ├── path.less
    │   │   │   │               ├── rotated-flipped.less
    │   │   │   │               ├── screen-reader.less
    │   │   │   │               ├── stacked.less
    │   │   │   │               └── variables.less
    │   │   │   └── com/
    │   │   │       └── ftpix/
    │   │   │           └── homedash/
    │   │   │               ├── app/
    │   │   │               │   ├── App.java
    │   │   │               │   ├── Constants.java
    │   │   │               │   ├── Endpoints.java
    │   │   │               │   ├── PluginModuleMaintainer.java
    │   │   │               │   └── controllers/
    │   │   │               │       ├── APIController.java
    │   │   │               │       ├── Controller.java
    │   │   │               │       ├── KioskController.java
    │   │   │               │       ├── LayoutController.java
    │   │   │               │       ├── ModuleController.java
    │   │   │               │       ├── ModuleLayoutController.java
    │   │   │               │       ├── ModuleSettingsController.java
    │   │   │               │       ├── PageController.java
    │   │   │               │       ├── PluginController.java
    │   │   │               │       ├── PluginUrlController.java
    │   │   │               │       ├── RemoteController.java
    │   │   │               │       ├── SettingsController.java
    │   │   │               │       └── UpdateController.java
    │   │   │               ├── db/
    │   │   │               │   ├── DB.java
    │   │   │               │   └── schemaManagement/
    │   │   │               │       ├── UpdateStep.java
    │   │   │               │       └── updates/
    │   │   │               │           └── Update20170722.java
    │   │   │               ├── jobs/
    │   │   │               │   └── BackgroundRefresh.java
    │   │   │               ├── utils/
    │   │   │               │   ├── HomeDashTemplateEngine.java
    │   │   │               │   └── Predicates.java
    │   │   │               └── websocket/
    │   │   │                   ├── FullScreenWebSocket.java
    │   │   │                   ├── InnerSocketClass.java
    │   │   │                   ├── KioskWebSocket.java
    │   │   │                   ├── MainWebSocket.java
    │   │   │                   ├── SingleModuleKioskWebSocket.java
    │   │   │                   └── SingleModuleWebSocket.java
    │   │   └── resources/
    │   │       ├── homedash.properties.default
    │   │       ├── log4j2.xml
    │   │       ├── templates/
    │   │       │   ├── add-module.jade
    │   │       │   ├── add-remote.jade
    │   │       │   ├── index.jade
    │   │       │   ├── layout-settings.jade
    │   │       │   ├── layout.jade
    │   │       │   ├── loader.jade
    │   │       │   ├── login.jade
    │   │       │   ├── module-full-screen.jade
    │   │       │   ├── module-kiosk.jade
    │   │       │   ├── module-layout.jade
    │   │       │   ├── module-settings.jade
    │   │       │   ├── settings.jade
    │   │       │   └── web-app-settings.jade
    │   │       └── web/
    │   │           ├── css/
    │   │           │   └── vendor/
    │   │           │       └── gridster/
    │   │           │           ├── gridster.css
    │   │           │           └── gridster.css.old
    │   │           ├── fonts/
    │   │           │   ├── FontAwesome/
    │   │           │   │   └── FontAwesome.otf
    │   │           │   └── Roboto/
    │   │           │       └── LICENSE.txt
    │   │           ├── images/
    │   │           │   └── ios/
    │   │           │       └── Contents.json
    │   │           └── js/
    │   │               └── vendor/
    │   │                   ├── bootstrap/
    │   │                   │   └── bootstrap.min.js.old
    │   │                   ├── grid/
    │   │                   │   ├── gridList.js
    │   │                   │   └── jquery.gridList.js
    │   │                   └── gridster/
    │   │                       ├── gridster.js
    │   │                       ├── gridster.js.old
    │   │                       └── jquery.gridster.js
    │   └── test/
    │       ├── java/
    │       │   └── com/
    │       │       └── ftpix/
    │       │           └── homedash/
    │       │               └── app/
    │       │                   └── ModuleTest.java
    │       └── resources/
    │           ├── conf/
    │           │   └── homedash.properties
    │           └── homedash.properties
    └── startup-scripts/
        ├── cpappend.bat
        ├── homedash.bat
        └── homedash.sh
Condensed preview — 419 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,632K chars).
[
  {
    "path": ".drone.yml",
    "chars": 2038,
    "preview": "kind: pipeline\ntype: docker\nname: default\n\nsteps:\n  - name: restore-cache\n    image: drillster/drone-volume-cache\n    vo..."
  },
  {
    "path": ".gitattributes",
    "chars": 377,
    "preview": "# Auto detect text files and perform LF normalization\n* text=auto\n\n# Custom for Visual Studio\n*.cs     diff=csharp\n\n# St..."
  },
  {
    "path": ".gitignore",
    "chars": 1898,
    "preview": "#maven\n\ntarget/\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\npom.xml.next\nrelease.properties\ndependency-redu..."
  },
  {
    "path": "DevelopPlugin.md",
    "chars": 11598,
    "preview": "# Developing a plugin for HomeDash\n\nDeveloping a module for HomeDash is pretty simple, you just need to follow a specifi..."
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2023 Paul Fauchon\n\nPermission is hereby granted, free of charge, to any person obtaining a co..."
  },
  {
    "path": "README.md",
    "chars": 2469,
    "preview": "[![Build Status](https://drone.ftpix.com/api/badges/lamarios/Homedash2/status.svg)](https://drone.ftpix.com/lamarios/Hom..."
  },
  {
    "path": "docs/index.html",
    "chars": 15011,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <!-- Required meta tags -->\n    <meta charset=\"utf-8\">\n    <meta name=\"viewp..."
  },
  {
    "path": "docs/style.css",
    "chars": 2507,
    "preview": "body {\n    font-family: 'Open Sans', sans-serif;\n}\n\n.head {\n    width: 100%;\n    height: 188px;\n    background-color: #3..."
  },
  {
    "path": "models/.gitignore",
    "chars": 9,
    "preview": "/target/\n"
  },
  {
    "path": "models/pom.xml",
    "chars": 2630,
    "preview": "<?xml version=\"1.0\"?>\n<project\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/Utils/ByteUtils.java",
    "chars": 981,
    "preview": "package com.ftpix.homedash.Utils;\n\n/**\n * Created by gz on 07-Jun-16.\n */\npublic class ByteUtils {\n\n    public static  S..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/Utils/HomeDashClassPathTemplateLoader.java",
    "chars": 966,
    "preview": "package com.ftpix.homedash.Utils;\n\nimport de.neuland.jade4j.template.ClasspathTemplateLoader;\n\nimport java.io.File;\nimpo..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/ExposedModule.java",
    "chars": 1074,
    "preview": "package com.ftpix.homedash.models;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Created by gz on 22-Jun-16...."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/ExternalEndPointDefinition.java",
    "chars": 1113,
    "preview": "package com.ftpix.homedash.models;\n\nimport spark.ResponseTransformer;\nimport spark.Route;\nimport spark.TemplateViewRoute..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/Layout.java",
    "chars": 990,
    "preview": "package com.ftpix.homedash.models;\n\nimport com.google.gson.annotations.Expose;\nimport com.j256.ormlite.field.DatabaseFie..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/Module.java",
    "chars": 2430,
    "preview": "package com.ftpix.homedash.models;\n\nimport com.j256.ormlite.dao.ForeignCollection;\nimport com.j256.ormlite.field.DataTyp..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/ModuleData.java",
    "chars": 1391,
    "preview": "package com.ftpix.homedash.models;\n\nimport com.j256.ormlite.field.DataType;\nimport com.j256.ormlite.field.DatabaseField;..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/ModuleExposedData.java",
    "chars": 928,
    "preview": "package com.ftpix.homedash.models;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Created by gz on 06-Jun-1..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/ModuleLayout.java",
    "chars": 1656,
    "preview": "package com.ftpix.homedash.models;\n\nimport com.j256.ormlite.field.DatabaseField;\nimport com.j256.ormlite.table.DatabaseT..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/ModuleLocation.java",
    "chars": 297,
    "preview": "/*\n * To change this license header, choose License Headers in Project Properties.\n * To change this template file, choo..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/ModuleSettings.java",
    "chars": 1045,
    "preview": "package com.ftpix.homedash.models;\n\nimport com.j256.ormlite.field.DatabaseField;\nimport com.j256.ormlite.table.DatabaseT..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/Page.java",
    "chars": 929,
    "preview": "package com.ftpix.homedash.models;\n\nimport com.google.gson.annotations.Expose;\nimport com.j256.ormlite.dao.ForeignCollec..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/RemoteFavorite.java",
    "chars": 1266,
    "preview": "package com.ftpix.homedash.models;\n\nimport com.j256.ormlite.field.DatabaseField;\nimport com.j256.ormlite.table.DatabaseT..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/Schema.java",
    "chars": 433,
    "preview": "package com.ftpix.homedash.models;\n\nimport com.j256.ormlite.field.DatabaseField;\nimport com.j256.ormlite.table.DatabaseT..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/Settings.java",
    "chars": 938,
    "preview": "package com.ftpix.homedash.models;\n\nimport com.j256.ormlite.field.DatabaseField;\nimport com.j256.ormlite.table.DatabaseT..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/Version.java",
    "chars": 1809,
    "preview": "package com.ftpix.homedash.models;\n\nimport java.util.Comparator;\nimport java.util.stream.Stream;\n\n/**\n * Created by gz o..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/WebSocketMessage.java",
    "chars": 1443,
    "preview": "package com.ftpix.homedash.models;\n\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.annotations.SerializedNam..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/WebSocketSession.java",
    "chars": 1252,
    "preview": "package com.ftpix.homedash.models;\n\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/export/Export.java",
    "chars": 402,
    "preview": "package com.ftpix.homedash.models.export;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/export/LayoutExport.java",
    "chars": 695,
    "preview": "package com.ftpix.homedash.models.export;\n\nimport com.ftpix.homedash.models.Layout;\n\npublic class LayoutExport {\n    pub..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/export/ModuleExport.java",
    "chars": 1493,
    "preview": "package com.ftpix.homedash.models.export;\n\nimport com.ftpix.homedash.models.Module;\nimport com.ftpix.homedash.models.Mod..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/export/ModuleLayoutExport.java",
    "chars": 539,
    "preview": "package com.ftpix.homedash.models.export;\n\nimport com.ftpix.homedash.models.ModuleLayout;\n\npublic class ModuleLayoutExpo..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/models/export/PageExport.java",
    "chars": 537,
    "preview": "package com.ftpix.homedash.models.export;\n\nimport com.ftpix.homedash.models.Page;\n\npublic class PageExport {\n    public..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/plugins/Plugin.java",
    "chars": 19210,
    "preview": "package com.ftpix.homedash.plugins;\n\nimport com.ftpix.homedash.Utils.HomeDashClassPathTemplateLoader;\nimport com.ftpix.h..."
  },
  {
    "path": "models/src/main/java/com/ftpix/homedash/plugins/PluginListener.java",
    "chars": 285,
    "preview": "package com.ftpix.homedash.plugins;\n\nimport com.ftpix.homedash.models.Module;\nimport com.ftpix.homedash.models.ModuleDat..."
  },
  {
    "path": "models/src/test/java/com/ftpix/homedash/plugins/AppTest.java",
    "chars": 654,
    "preview": "package com.ftpix.homedash.plugins;\n\nimport junit.framework.Test;\nimport junit.framework.TestCase;\nimport junit.framewor..."
  },
  {
    "path": "notifications/pom.xml",
    "chars": 1638,
    "preview": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"..."
  },
  {
    "path": "notifications/src/main/java/com/ftpix/homedash/notifications/NotificationProvider.java",
    "chars": 251,
    "preview": "package com.ftpix.homedash.notifications;\n\nimport java.util.Map;\n\npublic interface NotificationProvider {\n\tString getNam..."
  },
  {
    "path": "notifications/src/main/java/com/ftpix/homedash/notifications/Notifications.java",
    "chars": 1610,
    "preview": "package com.ftpix.homedash.notifications;\n\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.L..."
  },
  {
    "path": "notifications/src/main/java/com/ftpix/homedash/notifications/implementations/PushBullet.java",
    "chars": 2623,
    "preview": "package com.ftpix.homedash.notifications.implementations;\n\nimport com.ftpix.homedash.notifications.NotificationProvider;..."
  },
  {
    "path": "notifications/src/main/java/com/ftpix/homedash/notifications/implementations/PushOver.java",
    "chars": 2896,
    "preview": "package com.ftpix.homedash.notifications.implementations;\n\nimport com.ftpix.homedash.notifications.NotificationProvider;..."
  },
  {
    "path": "notifications/src/main/java/com/ftpix/homedash/notifications/implementations/Pushalot.java",
    "chars": 2807,
    "preview": "package com.ftpix.homedash.notifications.implementations;\n\nimport com.ftpix.homedash.notifications.NotificationProvider;..."
  },
  {
    "path": "plugins/couchpotato/pom.xml",
    "chars": 1979,
    "preview": "<?xml version=\"1.0\"?>\n<project\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/js/couchpotato.js",
    "chars": 4911,
    "preview": "function couchpotato(moduleId) {\n\n    this.moduleId = moduleId;\n    this.movies = [];\n\n    this.documentReady = function..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/less/couchpotato.less",
    "chars": 2201,
    "preview": ".couchpotato {\n  background-repeat: no-repeat;\n  background-position: center;\n  background-size: cover;\n\n  background-co..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/templates/couchpotato-1x1.jade",
    "chars": 72,
    "preview": "button.btn.show-search\n    i.fa.fa-search\np.name\ninclude ./cp-modal.jade"
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/templates/couchpotato-1x3.jade",
    "chars": 30,
    "preview": "include ./couchpotato-1x1.jade"
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/templates/couchpotato-2x1.jade",
    "chars": 94,
    "preview": "button.btn.show-search\n    i.fa.fa-search\n    | &nbsp;Add movie\np.name\ninclude ./cp-modal.jade"
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/templates/couchpotato-2x2.jade",
    "chars": 30,
    "preview": "include ./couchpotato-2x1.jade"
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/templates/couchpotato-2x3.jade",
    "chars": 30,
    "preview": "include ./couchpotato-2x1.jade"
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/templates/couchpotato-3x2.jade",
    "chars": 30,
    "preview": "include ./couchpotato-2x1.jade"
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/templates/couchpotato-3x3.jade",
    "chars": 30,
    "preview": "include ./couchpotato-2x1.jade"
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/templates/couchpotato-settings.jade",
    "chars": 1030,
    "preview": ".form-group\n    label(for=\"type\") API Type\n    select(name=\"type\", id=\"type\")\n        each type, index in {'COUCHPOTATO'..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/assets/templates/cp-modal.jade",
    "chars": 729,
    "preview": ".modal.fade.cp-modal(tabindex=\"-1\", role=\"dialog\", aria-labelledby=\"mySmallModalLabel\" ,aria-hidden=\"true\")\n    .modal-d..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/CouchPotatoPlugin.java",
    "chars": 7543,
    "preview": "package com.ftpix.homedash.plugins.couchpotato;\n\nimport com.ftpix.homedash.models.ModuleExposedData;\nimport com.ftpix.ho..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/apis/CouchPotatoApi.java",
    "chars": 7429,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.apis;\n\nimport com.ftpix.homedash.plugins.couchpotato.models.*;\nimport com..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/apis/MovieProviderAPI.java",
    "chars": 2486,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.apis;\n\nimport com.ftpix.homedash.plugins.couchpotato.models.MovieObject;..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/apis/RadarrApi.java",
    "chars": 10490,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.apis;\n\nimport com.ftpix.homedash.plugins.couchpotato.apis.radarr.RadarrMo..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/apis/radarr/RadarrImage.java",
    "chars": 409,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.apis.radarr;\n\npublic class RadarrImage {\n    private String coverType, ur..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/apis/radarr/RadarrMovieRequest.java",
    "chars": 1565,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.apis.radarr;\n\nimport java.util.List;\n\npublic class RadarrMovieRequest {..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/ImagePath.java",
    "chars": 126,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.models;\n\n@FunctionalInterface\npublic interface ImagePath {\n    String get..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/MovieObject.java",
    "chars": 314,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.models;\n\n/**\n * Created by gz on 22-Jun-16.\n */\npublic class MovieObject..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/MovieProviderBuilder.java",
    "chars": 340,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.models;\n\nimport com.ftpix.homedash.plugins.couchpotato.apis.MovieProvider..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/MovieRequest.java",
    "chars": 663,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.models;\n\npublic class MovieRequest {\n    private MovieObject movie;\n    p..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/MoviesRootFolder.java",
    "chars": 377,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.models;\n\npublic class MoviesRootFolder {\n    private String name;\n    pri..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/QualityProfile.java",
    "chars": 376,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.models;\n\npublic class QualityProfile {\n    private String name;\n    priva..."
  },
  {
    "path": "plugins/couchpotato/src/main/java/com/ftpix/homedash/plugins/couchpotato/models/Type.java",
    "chars": 107,
    "preview": "package com.ftpix.homedash.plugins.couchpotato.models;\n\npublic enum Type {\n    COUCHPOTATO,\n    RADARR;\n\n}\n"
  },
  {
    "path": "plugins/docker/.gitignore",
    "chars": 9,
    "preview": "/target/\n"
  },
  {
    "path": "plugins/docker/pom.xml",
    "chars": 1575,
    "preview": "<?xml version=\"1.0\"?>\n<project\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd..."
  },
  {
    "path": "plugins/docker/src/main/java/assets/js/docker.js",
    "chars": 10301,
    "preview": "function docker(moduleId) {\n    this.moduleId = moduleId;\n    this.selectedContainer;\n    this.selectedImage;\n    this.l..."
  },
  {
    "path": "plugins/docker/src/main/java/assets/less/docker.less",
    "chars": 815,
    "preview": ".docker.size-1x1 {\n  background-color: #0087C9;\n  color: white;\n  background-image: url(\"../files/images/docker.svg\");..."
  },
  {
    "path": "plugins/docker/src/main/java/assets/templates/docker-1x1.jade",
    "chars": 56,
    "preview": "p.header Docker\nh1.count ...\np.footer running containers"
  },
  {
    "path": "plugins/docker/src/main/java/assets/templates/docker-full-screen.jade",
    "chars": 1494,
    "preview": "\ndiv\n    ul.nav.nav-tabs(role=\"tablist\")\n        li.active(role=\"presentation\")\n            a(href=\"#containers\", aria-c..."
  },
  {
    "path": "plugins/docker/src/main/java/assets/templates/docker-image-modal.jade",
    "chars": 621,
    "preview": "#image-modal.modal.fade(tabindex=\"-1\", role=\"dialog\")\n    .modal-dialog(role=\"document\")\n        .modal-content..."
  },
  {
    "path": "plugins/docker/src/main/java/assets/templates/docker-logs-modal.jade",
    "chars": 371,
    "preview": "#container-logs-modal.modal.fade(tabindex=\"-1\", role=\"dialog\")\n    .modal-dialog(role=\"document\")\n        .modal-content..."
  },
  {
    "path": "plugins/docker/src/main/java/assets/templates/docker-modal.jade",
    "chars": 886,
    "preview": "#container-modal.modal.fade(tabindex=\"-1\", role=\"dialog\")\n    .modal-dialog(role=\"document\")\n        .modal-content..."
  },
  {
    "path": "plugins/docker/src/main/java/assets/templates/docker-settings.jade",
    "chars": 497,
    "preview": ".form-group\n    label(for=\"url\") Docker API URL\n    if settings && settings.containsKey(\"url\")\n        input.form-contro..."
  },
  {
    "path": "plugins/docker/src/main/java/com/ftpix/homedash/plugins/docker/DockerPlugin.java",
    "chars": 13226,
    "preview": "package com.ftpix.homedash.plugins.docker;\n\nimport com.ftpix.homedash.Utils.ByteUtils;\nimport com.ftpix.homedash.models...."
  },
  {
    "path": "plugins/docker/src/main/java/com/ftpix/homedash/plugins/docker/models/DockerExtendedInfo.java",
    "chars": 275,
    "preview": "package com.ftpix.homedash.plugins.docker.models;\n\nimport com.spotify.docker.client.messages.Container;\n\n/**\n * Created..."
  },
  {
    "path": "plugins/docker/src/main/java/com/ftpix/homedash/plugins/docker/models/DockerImageInfo.java",
    "chars": 887,
    "preview": "package com.ftpix.homedash.plugins.docker.models;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Created by..."
  },
  {
    "path": "plugins/docker/src/main/java/com/ftpix/homedash/plugins/docker/models/DockerInfo.java",
    "chars": 1757,
    "preview": "package com.ftpix.homedash.plugins.docker.models;\n\nimport com.ftpix.homedash.Utils.ByteUtils;\nimport com.google.common.c..."
  },
  {
    "path": "plugins/dockercompose/pom.xml",
    "chars": 923,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www..."
  },
  {
    "path": "plugins/dockercompose/src/main/java/assets/js/dockercompose.js",
    "chars": 3469,
    "preview": "function dockercompose(moduleId) {\n    this.moduleId = moduleId;\n\n    this.documentReady = function (size) {\n        var..."
  },
  {
    "path": "plugins/dockercompose/src/main/java/assets/less/dockercompose.less",
    "chars": 715,
    "preview": ".dockercompose.size-1x1, .dockercompose.size-2x1 {\n  color: white;\n  background: #3949AB url(\"../files/images/docker.svg..."
  },
  {
    "path": "plugins/dockercompose/src/main/java/assets/templates/dockercompose-1x1.jade",
    "chars": 63,
    "preview": "p.title Docker Compose\np.count\np.containers containers\np.folder"
  },
  {
    "path": "plugins/dockercompose/src/main/java/assets/templates/dockercompose-2x1.jade",
    "chars": 27,
    "preview": "include ./dockercompose-1x1"
  },
  {
    "path": "plugins/dockercompose/src/main/java/assets/templates/dockercompose-full-screen.jade",
    "chars": 796,
    "preview": "h2 Actions\n.actions\n    button.btn.cmd(data-cmd=\"up -d\") up -d\n    button.btn.cmd(data-cmd=\"down\") down\n    button.btn.c..."
  },
  {
    "path": "plugins/dockercompose/src/main/java/assets/templates/dockercompose-modal.jade",
    "chars": 322,
    "preview": ".modal.fade(tabindex=\"-1\", role=\"dialog\")\n    .modal-dialog(role=\"document\")\n        .modal-content\n            .modal-h..."
  },
  {
    "path": "plugins/dockercompose/src/main/java/assets/templates/dockercompose-settings.jade",
    "chars": 437,
    "preview": "p This plugins only works for linux at the moment and the docker-compose executable has to be in your PATH\n.form-group..."
  },
  {
    "path": "plugins/dockercompose/src/main/java/com/ftpix/homedash/plugins/dockercompose/DockerComposePlugin.java",
    "chars": 10239,
    "preview": "package com.ftpix.homedash.plugins.dockercompose;\n\nimport com.ftpix.homedash.models.ModuleExposedData;\nimport com.ftpix...."
  },
  {
    "path": "plugins/dockercompose/src/main/java/com/ftpix/homedash/plugins/dockercompose/exceptions/CommandException.java",
    "chars": 428,
    "preview": "package com.ftpix.homedash.plugins.dockercompose.exceptions;\n\n/**\n * Created by gz on 4/22/17.\n */\npublic class CommandE..."
  },
  {
    "path": "plugins/dockercompose/src/main/java/com/ftpix/homedash/plugins/dockercompose/models/CommandOutput.java",
    "chars": 765,
    "preview": "package com.ftpix.homedash.plugins.dockercompose.models;\n\nimport java.util.List;\n\n/**\n * Created by gz on 4/22/17.\n */\np..."
  },
  {
    "path": "plugins/dockercompose/src/main/java/com/ftpix/homedash/plugins/dockercompose/models/DockerContainer.java",
    "chars": 728,
    "preview": "package com.ftpix.homedash.plugins.dockercompose.models;\n\n/**\n * Created by gz on 4/22/17.\n */\npublic class DockerContai..."
  },
  {
    "path": "plugins/dynamicdns/pom.xml",
    "chars": 1969,
    "preview": "<?xml version=\"1.0\"?>\n<project\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/assets/js/dynamicdns.js",
    "chars": 1621,
    "preview": "function dynamicdns(moduleId) {\n    this.moduleId = moduleId;\n\n    this.onConnect = function () {\n    };\n\n    this.docum..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/assets/less/dynamicdns.less",
    "chars": 741,
    "preview": ".dynamicdns {\n\n  .actions .btn {\n    font-size: 20px;\n    margin: 10px;\n  }\n}\n\n.dynamicdns.size-2x1 {\n  overflow-y: auto..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/assets/templates/dynamicdns-2x1.jade",
    "chars": 135,
    "preview": "h4.host\nh4.ip\n    | IP:&nbsp;\n    span.address\np.actions\n    a.refresh.btn.btn-primary\n        i.fa.fa-refresh\n        |..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/assets/templates/dynamicdns-settings.jade",
    "chars": 3503,
    "preview": "label\n    if settings && settings.containsKey(\"notifications\")\n        input(type=\"checkbox\", name=\"notifications\", valu..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/com/ftpix/homedash/plugins/dynamicdns/DynamicDnsPlugin.java",
    "chars": 10150,
    "preview": "package com.ftpix.homedash.plugins.dynamicdns;\n\nimport com.ftpix.homedash.plugins.dynamicdns.models.IpFromWeb;\nimport co..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/com/ftpix/homedash/plugins/dynamicdns/inputs/FormInput.java",
    "chars": 974,
    "preview": "package com.ftpix.homedash.plugins.dynamicdns.inputs;\n\nimport com.google.gson.annotations.Expose;\n\npublic class FormInpu..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/com/ftpix/homedash/plugins/dynamicdns/inputs/FormType.java",
    "chars": 99,
    "preview": "package com.ftpix.homedash.plugins.dynamicdns.inputs;\n\npublic enum FormType {\n    TEXT,PASSWORD;\n}\n"
  },
  {
    "path": "plugins/dynamicdns/src/main/java/com/ftpix/homedash/plugins/dynamicdns/models/Ip.java",
    "chars": 612,
    "preview": "package com.ftpix.homedash.plugins.dynamicdns.models;\n\nimport java.util.Date;\n\n/**\n * Created by gz on 15-Jul-16.\n */\npu..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/com/ftpix/homedash/plugins/dynamicdns/models/IpFromWeb.java",
    "chars": 259,
    "preview": "package com.ftpix.homedash.plugins.dynamicdns.models;\n\n/**\n * Created by gz on 4/9/17.\n */\npublic class IpFromWeb {..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/com/ftpix/homedash/plugins/dynamicdns/providers/DynDNSProvider.java",
    "chars": 639,
    "preview": "package com.ftpix.homedash.plugins.dynamicdns.providers;\n\nimport com.ftpix.homedash.plugins.dynamicdns.inputs.FormInput;..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/com/ftpix/homedash/plugins/dynamicdns/providers/implementations/DynDNS.java",
    "chars": 309,
    "preview": "package com.ftpix.homedash.plugins.dynamicdns.providers.implementations;\n\npublic class DynDNS extends StandardProvider {..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/com/ftpix/homedash/plugins/dynamicdns/providers/implementations/NoIP.java",
    "chars": 311,
    "preview": "package com.ftpix.homedash.plugins.dynamicdns.providers.implementations;\n\npublic class NoIP extends StandardProvider {..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/com/ftpix/homedash/plugins/dynamicdns/providers/implementations/OVH.java",
    "chars": 355,
    "preview": "package com.ftpix.homedash.plugins.dynamicdns.providers.implementations;\n\n\npublic class OVH extends StandardProvider {..."
  },
  {
    "path": "plugins/dynamicdns/src/main/java/com/ftpix/homedash/plugins/dynamicdns/providers/implementations/StandardProvider.java",
    "chars": 4003,
    "preview": "package com.ftpix.homedash.plugins.dynamicdns.providers.implementations;\n\nimport com.ftpix.homedash.plugins.dynamicdns.i..."
  },
  {
    "path": "plugins/googlepubliccalendar/pom.xml",
    "chars": 1655,
    "preview": "<?xml version=\"1.0\"?>\n<project\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd..."
  },
  {
    "path": "plugins/googlepubliccalendar/src/main/java/assets/js/googlepubliccalendar.js",
    "chars": 4092,
    "preview": "function googlepubliccalendar(moduleId) {\n    this.moduleId = moduleId;\n\n    this.onConnect = function () {\n    };..."
  },
  {
    "path": "plugins/googlepubliccalendar/src/main/java/assets/less/googlepubliccalendar.less",
    "chars": 1592,
    "preview": "@gcBackgroundColor: #00BCD4;\n@transition-speed: 0.25s;\n.googlepubliccalendar {\n  h4 {\n    padding: 10px;\n    font-size:..."
  },
  {
    "path": "plugins/googlepubliccalendar/src/main/java/assets/templates/googlepubliccalendar-3x1.jade",
    "chars": 102,
    "preview": "h4.title\n.next-event.gcal-event\n    h5.event-title\n    p.date\n\ninclude googlepubliccalendar-modal.jade"
  },
  {
    "path": "plugins/googlepubliccalendar/src/main/java/assets/templates/googlepubliccalendar-3x4.jade",
    "chars": 32,
    "preview": "include googlepubliccalendar-4x4"
  },
  {
    "path": "plugins/googlepubliccalendar/src/main/java/assets/templates/googlepubliccalendar-4x4.jade",
    "chars": 164,
    "preview": "h4.title\n\ntable.events.table.table-hover.table-condensed\n    thead\n        tr\n            th Date\n            th Event..."
  },
  {
    "path": "plugins/googlepubliccalendar/src/main/java/assets/templates/googlepubliccalendar-full-screen.jade",
    "chars": 32,
    "preview": "include googlepubliccalendar-4x4"
  },
  {
    "path": "plugins/googlepubliccalendar/src/main/java/assets/templates/googlepubliccalendar-modal.jade",
    "chars": 395,
    "preview": ".googlepubliccalendar-modal.modal.fade\n    .modal-dialog\n        .modal-content\n            .modal-header..."
  },
  {
    "path": "plugins/googlepubliccalendar/src/main/java/assets/templates/googlepubliccalendar-settings.jade",
    "chars": 718,
    "preview": ".form-group\n    label(for=\"calendarId\") Calendar ID\n    if settings && settings.containsKey(\"calendarId\")\n        input...."
  },
  {
    "path": "plugins/googlepubliccalendar/src/main/java/com/ftpix/homedash/plugins/googlepubliccalendar/GooglePublicCalendarPlugin.java",
    "chars": 8441,
    "preview": "package com.ftpix.homedash.plugins.googlepubliccalendar;\n\nimport com.ftpix.homedash.models.ModuleExposedData;\nimport com..."
  },
  {
    "path": "plugins/harddisk/pom.xml",
    "chars": 1624,
    "preview": "<?xml version=\"1.0\"?>\n<project\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd..."
  },
  {
    "path": "plugins/harddisk/src/main/java/assets/js/harddisk.js",
    "chars": 14559,
    "preview": "function harddisk(moduleId) {\n\n    this.moduleId = moduleId;\n\n    this.width = 1;\n\n    this.onConnect = function () {..."
  },
  {
    "path": "plugins/harddisk/src/main/java/assets/less/harddisk.less",
    "chars": 3776,
    "preview": "@background: rgb(10, 127, 116);\n@transitionSpeed: 0.25s;\n@speed: 3s;\n.harddisk.size-2x1, .harddisk.size-1x1, .harddisk.s..."
  },
  {
    "path": "plugins/harddisk/src/main/java/assets/templates/harddisk-1x1.jade",
    "chars": 37,
    "preview": ".storage-icon\n.hdd-container\nh4.path\n"
  },
  {
    "path": "plugins/harddisk/src/main/java/assets/templates/harddisk-2x1.jade",
    "chars": 44,
    "preview": ".storage-icon\np.data\n.hdd-container\nh4.path\n"
  },
  {
    "path": "plugins/harddisk/src/main/java/assets/templates/harddisk-full-screen.jade",
    "chars": 476,
    "preview": "h2 Info\np\n    strong Mount:\n    span.mount\n.progress\n    .progress-bar(role=\"progressbar\", style=\"width:0%\")\n        spa..."
  },
  {
    "path": "plugins/harddisk/src/main/java/assets/templates/harddisk-kiosk.jade",
    "chars": 21,
    "preview": "include harddisk-2x1\n"
  },
  {
    "path": "plugins/harddisk/src/main/java/assets/templates/harddisk-settings.jade",
    "chars": 418,
    "preview": ".form-group\n    p Select mount point\n    each val, index in model\n        .radio\n            label\n                if(se..."
  },
  {
    "path": "plugins/harddisk/src/main/java/com/ftpix/homedash/plugins/harddisk/DiskFile.java",
    "chars": 182,
    "preview": "package com.ftpix.homedash.plugins.harddisk;\n\n/**\n * Created by gz on 5/6/17.\n */\npublic class DiskFile {\n\n    public St..."
  },
  {
    "path": "plugins/harddisk/src/main/java/com/ftpix/homedash/plugins/harddisk/FileOperation.java",
    "chars": 638,
    "preview": "package com.ftpix.homedash.plugins.harddisk;\n\n/**\n * Created by gz on 5/6/17.\n */\npublic class FileOperation {\n    priva..."
  },
  {
    "path": "plugins/harddisk/src/main/java/com/ftpix/homedash/plugins/harddisk/HarddiskPlugin.java",
    "chars": 22013,
    "preview": "package com.ftpix.homedash.plugins.harddisk;\n\n\nimport com.ftpix.homedash.Utils.ByteUtils;\nimport com.ftpix.homedash.mode..."
  },
  {
    "path": "plugins/harddisk/src/main/java/com/ftpix/homedash/plugins/harddisk/OperationException.java",
    "chars": 215,
    "preview": "package com.ftpix.homedash.plugins.harddisk;\n\n/**\n * Created by gz on 5/6/17.\n */\npublic class OperationException extend..."
  },
  {
    "path": "plugins/kvm/pom.xml",
    "chars": 1722,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www..."
  },
  {
    "path": "plugins/kvm/src/main/java/assets/js/kvm.js",
    "chars": 3504,
    "preview": "function kvm(moduleId) {\n    this.moduleId = moduleId;\n\n    this.onConnect = function () {\n\n    };\n\n    this.documentRea..."
  },
  {
    "path": "plugins/kvm/src/main/java/assets/less/kvm.less",
    "chars": 362,
    "preview": ".kvm.size-1x1{\n  background-color: #9C27B0;\n  color:white;\n\n  background-image: url(\"../files/logo.svg\");\n\n  background-..."
  },
  {
    "path": "plugins/kvm/src/main/java/assets/templates/kvm-1x1.jade",
    "chars": 29,
    "preview": "h3 kvm\n.running\np VMs running"
  },
  {
    "path": "plugins/kvm/src/main/java/assets/templates/kvm-full-screen.jade",
    "chars": 146,
    "preview": "h2 Virtual Machines\ntable.table.table-striped\n    thead\n        tr\n            th name\n            th status..."
  },
  {
    "path": "plugins/kvm/src/main/java/assets/templates/kvm-settings.jade",
    "chars": 348,
    "preview": ".form-group\n    label(for=\"url\") KVM/Qemu connection string (ex: qemu+ssh://localhost/system)\n    if settings && setting..."
  },
  {
    "path": "plugins/kvm/src/main/java/com/ftpix/homedash/plugins/kvm/KvmPlugin.java",
    "chars": 10142,
    "preview": "package com.ftpix.homedash.plugins.kvm;\n\nimport com.ftpix.homedash.models.ModuleExposedData;\nimport com.ftpix.homedash.m..."
  },
  {
    "path": "plugins/kvm/src/main/java/com/ftpix/homedash/plugins/kvm/VMAction.java",
    "chars": 386,
    "preview": "package com.ftpix.homedash.plugins.kvm;\n\npublic class VMAction {\n    private String action, domain;\n\n    public String g..."
  },
  {
    "path": "plugins/kvm/src/main/java/com/ftpix/homedash/plugins/kvm/VMActionResponse.java",
    "chars": 109,
    "preview": "package com.ftpix.homedash.plugins.kvm;\n\npublic class VMActionResponse {\n    public String domain,message;\n}\n"
  },
  {
    "path": "plugins/kvm/src/main/java/com/ftpix/homedash/plugins/kvm/VMInfo.java",
    "chars": 192,
    "preview": "package com.ftpix.homedash.plugins.kvm;\n\npublic class VMInfo {\n    public String name;\n    public String status;\n    pub..."
  },
  {
    "path": "plugins/logreader/pom.xml",
    "chars": 915,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www..."
  },
  {
    "path": "plugins/logreader/src/main/java/assets/js/logreader.js",
    "chars": 1701,
    "preview": "function logreader(moduleId) {\n\n    this.moduleId = moduleId;\n\n    this.autoScroll = true;\n\n    this.maxActivity = 1;..."
  },
  {
    "path": "plugins/logreader/src/main/java/assets/less/logreader.less",
    "chars": 1428,
    "preview": ".logreader.size-1x1 {\n  @backgroundColor: #FF5722;\n  background-color: @backgroundColor;\n  color: white;\n  padding: 2px;..."
  },
  {
    "path": "plugins/logreader/src/main/java/assets/templates/logreader-1x1.jade",
    "chars": 44,
    "preview": "p Logs\n\n.max-activity\n    .activity\n\np.path\n"
  },
  {
    "path": "plugins/logreader/src/main/java/assets/templates/logreader-full-screen.jade",
    "chars": 101,
    "preview": "h3#file\np\n    button#auto-scroll.btn.btn-primary.active Auto scroll to bottom\ndiv\n    textarea#logs\n\n"
  },
  {
    "path": "plugins/logreader/src/main/java/assets/templates/logreader-settings.jade",
    "chars": 769,
    "preview": ".form-group\n    label(for=\"path\") Log file location\n    if settings && settings.containsKey(\"path\")\n        input.form-c..."
  },
  {
    "path": "plugins/logreader/src/main/java/com/ftpix/logreader/LogReaderPlugin.java",
    "chars": 5758,
    "preview": "package com.ftpix.logreader;\n\nimport com.ftpix.homedash.models.ModuleExposedData;\nimport com.ftpix.homedash.models.Modul..."
  },
  {
    "path": "plugins/mma/pom.xml",
    "chars": 1611,
    "preview": "<?xml version=\"1.0\"?>\n<project\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd..."
  },
  {
    "path": "plugins/mma/src/main/java/assets/js/mma.js",
    "chars": 12641,
    "preview": "function mma(moduleId) {\n    this.organization;\n    this.moduleId = moduleId;\n    this.history = [];\n\n    this.size;..."
  },
  {
    "path": "plugins/mma/src/main/java/assets/less/mma.less",
    "chars": 3941,
    "preview": "@backgroundColor: #039BE5;\n\n.mma {\n  h3 {\n    margin-left: 5px;\n  }\n\n  .table {\n    tbody tr {\n      cursor: pointer;..."
  },
  {
    "path": "plugins/mma/src/main/java/assets/templates/mma-3x2.jade",
    "chars": 132,
    "preview": ".event-container\n\n.buttons\n    a.previous\n        i.fa.fa-chevron-left\n    a.next\n        i.fa.fa-chevron-right\n\ninclude..."
  },
  {
    "path": "plugins/mma/src/main/java/assets/templates/mma-3x4.jade",
    "chars": 17,
    "preview": "include ./mma-4x4"
  },
  {
    "path": "plugins/mma/src/main/java/assets/templates/mma-4x4.jade",
    "chars": 130,
    "preview": "h3.org-name\nhr\ntable.table.org-events\n    thead\n        tr\n            th Date\n            th Event\n    tbody\n\ninclude ...."
  },
  {
    "path": "plugins/mma/src/main/java/assets/templates/mma-full-screen.jade",
    "chars": 197,
    "preview": "h3.org-name\nhr\np\n    input.search(type=\"text\", placeholder=\"Search for an event\")\ntable.table.org-events\n    thead..."
  },
  {
    "path": "plugins/mma/src/main/java/assets/templates/mma-kiosk.jade",
    "chars": 15,
    "preview": "include mma-3x2"
  },
  {
    "path": "plugins/mma/src/main/java/assets/templates/mma-modal.jade",
    "chars": 335,
    "preview": ".modal.fade.mma-modal(tabindex=\"-1\", role=\"dialog\", aria-labelledby=\"mySmallModalLabel\" ,aria-hidden=\"true\")\n    .modal-..."
  },
  {
    "path": "plugins/mma/src/main/java/assets/templates/mma-settings.jade",
    "chars": 573,
    "preview": ".form-group\n    label(for=\"url\") Organization Sherdog URL\n    if settings && settings.containsKey(\"url\")\n        input.f..."
  },
  {
    "path": "plugins/mma/src/main/java/com/ftpix/homedash/plugins/mma/MmaPlugin.java",
    "chars": 10128,
    "preview": "package com.ftpix.homedash.plugins.mma;\n\nimport com.ftpix.homedash.models.ModuleExposedData;\nimport com.ftpix.homedash.m..."
  },
  {
    "path": "plugins/mma/src/main/java/com/ftpix/homedash/plugins/mma/model/HomeDashEvent.java",
    "chars": 943,
    "preview": "package com.ftpix.homedash.plugins.mma.model;\n\n\nimport com.ftpix.sherdogparser.models.Event;\n\n/**\n * Created by gz on 4/..."
  },
  {
    "path": "plugins/mma/src/main/java/com/ftpix/homedash/plugins/mma/model/HomeDashOrganization.java",
    "chars": 613,
    "preview": "package com.ftpix.homedash.plugins.mma.model;\n\nimport com.ftpix.sherdogparser.models.Organization;\n\nimport java.util.Lis..."
  },
  {
    "path": "plugins/networkmonitor/pom.xml",
    "chars": 1636,
    "preview": "<?xml version=\"1.0\"?>\n<project\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd..."
  },
  {
    "path": "plugins/networkmonitor/src/main/java/assets/js/networkmonitor.js",
    "chars": 2799,
    "preview": "function networkmonitor(moduleId) {\n\n    this.moduleId = moduleId;\n\n    this.width = 1;\n\n    this.onConnect = function (..."
  },
  {
    "path": "plugins/networkmonitor/src/main/java/assets/less/networkmonitor.less",
    "chars": 1168,
    "preview": ".networkmonitor {\n  padding: 5px;\n  background-color: #5CAF50;\n  color: white;\n  //background-image: url(\"../files/image..."
  },
  {
    "path": "plugins/networkmonitor/src/main/java/assets/templates/graph.jade",
    "chars": 193,
    "preview": ".svg\n    svg.graph(preserveAspectRatio=\"none\", version=\"1.1\", xmlns:xlink=\"http://www.w3.org/1999/xlink\", xmlns=\"http://..."
  },
  {
    "path": "plugins/networkmonitor/src/main/java/assets/templates/networkmonitor-2x1.jade",
    "chars": 159,
    "preview": "include ./graph\n.net-info\n    h3.interface-name\n    p\n        i.fa.fa-arrow-down\n        span.down-txt 0\n\n    p..."
  },
  {
    "path": "plugins/networkmonitor/src/main/java/assets/templates/networkmonitor-3x2.jade",
    "chars": 356,
    "preview": "include ./graph\n.net-info\n    h3.interface-name\n    .row\n        .col-xs-6\n            h4 Download\n            p.down-tx..."
  },
  {
    "path": "plugins/networkmonitor/src/main/java/assets/templates/networkmonitor-kiosk.jade",
    "chars": 27,
    "preview": "include networkmonitor-3x2\n"
  },
  {
    "path": "plugins/networkmonitor/src/main/java/assets/templates/networkmonitor-settings.jade",
    "chars": 481,
    "preview": ".form-group\n    p Select network interface\n    each val, index in model\n        .radio\n            label..."
  },
  {
    "path": "plugins/networkmonitor/src/main/java/com/ftpix/homedash/plugins/networkmonitor/NetworkMonitorPlugin.java",
    "chars": 6810,
    "preview": "package com.ftpix.homedash.plugins.networkmonitor;\n\nimport com.ftpix.homedash.Utils.ByteUtils;\nimport com.ftpix.homedash..."
  },
  {
    "path": "plugins/networkmonitor/src/main/java/com/ftpix/homedash/plugins/networkmonitor/models/NetworkInfo.java",
    "chars": 269,
    "preview": "package com.ftpix.homedash.plugins.networkmonitor.models;\n\n/**\n * Created by gz on 01-Jul-16.\n */\npublic class NetworkIn..."
  },
  {
    "path": "plugins/pihole/pom.xml",
    "chars": 1797,
    "preview": "<?xml version=\"1.0\"?>\n<project\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd..."
  },
  {
    "path": "plugins/pihole/src/main/java/assets/js/pihole.js",
    "chars": 1898,
    "preview": "function pihole(moduleId) {\n\n    this.moduleId = moduleId;\n\n    this.onConnect = function () {\n\n    };\n\n    this.documen..."
  },
  {
    "path": "plugins/pihole/src/main/java/assets/less/pihole.less",
    "chars": 992,
    "preview": ".pihole {\n  padding: 5px;\n  background-color: #E67E22;\n  color: white;\n  overflow: hidden;\n  background-image: url(\"../f..."
  },
  {
    "path": "plugins/pihole/src/main/java/assets/templates/pihole-1x1.jade",
    "chars": 49,
    "preview": "h1 PiHole\n.ads-blocked\n    p 0\n    h3 Ads blocked"
  },
  {
    "path": "plugins/pihole/src/main/java/assets/templates/pihole-2x2.jade",
    "chars": 108,
    "preview": "h1 PiHole Stats\n.ads-blocked\n    p 0\n    h3 Ads blocked Today\n.dns-queries\n    p 0\n    h3 DNS Queries Today\n"
  },
  {
    "path": "plugins/pihole/src/main/java/assets/templates/pihole-3x2.jade",
    "chars": 312,
    "preview": "h1 PiHole Stats\n.row\n    .col-xs-6.ads-blocked\n        p 0\n        h3 Ads blocked Today\n    .col-xs-6.dns-queries..."
  },
  {
    "path": "plugins/pihole/src/main/java/assets/templates/pihole-4x2.jade",
    "chars": 18,
    "preview": "include pihole-3x2"
  },
  {
    "path": "plugins/pihole/src/main/java/assets/templates/pihole-full-screen.jade",
    "chars": 232,
    "preview": "h2 Queries\ntable.table.table-striped\n    thead\n        tr\n            th Date\n            th Blocked ?\n            th Ty..."
  },
  {
    "path": "plugins/pihole/src/main/java/assets/templates/pihole-kiosk.jade",
    "chars": 18,
    "preview": "include pihole-3x2"
  },
  {
    "path": "plugins/pihole/src/main/java/assets/templates/pihole-settings.jade",
    "chars": 741,
    "preview": ".form-group\n    label(for=\"url\") Pi-Hole admin URL (without /api.php)\n    if settings && settings.containsKey(\"url\")..."
  },
  {
    "path": "plugins/pihole/src/main/java/com/ftpix/homedash/plugins/pihole/PiHoleClient.java",
    "chars": 3361,
    "preview": "package com.ftpix.homedash.plugins.pihole;\n\nimport com.ftpix.homedash.plugins.pihole.models.AnswerType;\nimport com.ftpix..."
  },
  {
    "path": "plugins/pihole/src/main/java/com/ftpix/homedash/plugins/pihole/PiHolePlugin.java",
    "chars": 4371,
    "preview": "package com.ftpix.homedash.plugins.pihole;\n\nimport com.ftpix.homedash.models.ModuleLayout;\nimport com.google.gson.Gson;..."
  },
  {
    "path": "plugins/pihole/src/main/java/com/ftpix/homedash/plugins/pihole/UnauthorizedException.java",
    "chars": 182,
    "preview": "package com.ftpix.homedash.plugins.pihole;\n\npublic class UnauthorizedException extends Exception {\n    public Unauthoriz..."
  },
  {
    "path": "plugins/pihole/src/main/java/com/ftpix/homedash/plugins/pihole/models/AnswerType.java",
    "chars": 1000,
    "preview": "package com.ftpix.homedash.plugins.pihole.models;\n\nimport java.util.stream.Stream;\n\npublic enum AnswerType {\n    GRAVITY..."
  },
  {
    "path": "plugins/pihole/src/main/java/com/ftpix/homedash/plugins/pihole/models/PiHoleQuery.java",
    "chars": 1381,
    "preview": "package com.ftpix.homedash.plugins.pihole.models;\n\nimport java.time.LocalDateTime;\n\npublic class PiHoleQuery {\n    priva..."
  },
  {
    "path": "plugins/pihole/src/main/java/com/ftpix/homedash/plugins/pihole/models/PiHoleStats.java",
    "chars": 1353,
    "preview": "package com.ftpix.homedash.plugins.pihole.models;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * Created by..."
  },
  {
    "path": "plugins/plex/pom.xml",
    "chars": 1848,
    "preview": "<?xml version=\"1.0\"?>\n<project\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd..."
  },
  {
    "path": "plugins/plex/src/main/java/assets/js/plex.js",
    "chars": 1315,
    "preview": "function plex(moduleId) {\n\n    this.moduleId = moduleId;\n    this.currentIndex = 0;\n    this.onConnect = function () {..."
  },
  {
    "path": "plugins/plex/src/main/java/assets/less/plex.less",
    "chars": 1927,
    "preview": ".plex {\n  background-size: cover;\n  background-position: center center;\n  background-repeat: no-repeat;\n  background-col..."
  },
  {
    "path": "plugins/plex/src/main/java/assets/templates/plex-1x1.jade",
    "chars": 79,
    "preview": ".now-playing.nothing\n    .info\n        .progress\n        .name\n        .player\n"
  },
  {
    "path": "plugins/plex/src/main/java/assets/templates/plex-2x1.jade",
    "chars": 16,
    "preview": "include plex-1x1"
  },
  {
    "path": "plugins/plex/src/main/java/assets/templates/plex-3x2.jade",
    "chars": 16,
    "preview": "include plex-1x1"
  },
  {
    "path": "plugins/plex/src/main/java/assets/templates/plex-3x3.jade",
    "chars": 16,
    "preview": "include plex-1x1"
  },
  {
    "path": "plugins/plex/src/main/java/assets/templates/plex-4x4.jade",
    "chars": 16,
    "preview": "include plex-1x1"
  },
  {
    "path": "plugins/plex/src/main/java/assets/templates/plex-kiosk.jade",
    "chars": 16,
    "preview": "include plex-1x1"
  },
  {
    "path": "plugins/plex/src/main/java/assets/templates/plex-settings.jade",
    "chars": 1950,
    "preview": ".form-group\n    label(for=\"type\") API Type\n    select.form-control(name=\"type\", id=\"type\")\n        each type, index in {..."
  },
  {
    "path": "plugins/plex/src/main/java/com/ftpix/homedash/plugin/plex/PlexPlugin.java",
    "chars": 4230,
    "preview": "package com.ftpix.homedash.plugin.plex;\n\nimport com.ftpix.homedash.models.ModuleExposedData;\nimport com.ftpix.homedash.m..."
  },
  {
    "path": "plugins/plex/src/main/java/com/ftpix/homedash/plugin/plex/PlexResultParser.java",
    "chars": 588,
    "preview": "package com.ftpix.homedash.plugin.plex;\n\nimport com.ftpix.homedash.plugin.plex.model.MediaContainer;\nimport com.ftpix.ho..."
  },
  {
    "path": "plugins/plex/src/main/java/com/ftpix/homedash/plugin/plex/api/ApiType.java",
    "chars": 89,
    "preview": "package com.ftpix.homedash.plugin.plex.api;\n\npublic enum ApiType {\n    PLEX, JELLYFIN;\n}\n"
  },
  {
    "path": "plugins/plex/src/main/java/com/ftpix/homedash/plugin/plex/api/MediaServerApi.java",
    "chars": 932,
    "preview": "package com.ftpix.homedash.plugin.plex.api;\n\nimport com.ftpix.homedash.plugin.plex.api.impl.JellyFinApi;\nimport com.ftpi..."
  }
]

// ... and 219 more files (download for full content)

About this extraction

This page contains the full source code of the lamarios/Homedash2 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 420 files (1.5 MB), approximately 394.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!