├── data └── .gitkeep ├── start-screencast.cmd ├── start-screencast.sh ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src └── main │ ├── resources │ ├── static │ │ ├── img │ │ │ ├── ball-blue.png │ │ │ ├── ball-gray.png │ │ │ ├── new-note-icon-128x128.png │ │ │ └── scroll-down.svg │ │ ├── css │ │ │ └── app.css │ │ └── js │ │ │ └── app.js │ ├── application.yml │ └── templates │ │ └── index.html │ └── java │ └── de │ └── tdlabs │ └── apps │ └── screencaster │ ├── screencast │ ├── ScreenCastService.java │ ├── grabbing │ │ ├── ScreenGrabber.java │ │ ├── awt │ │ │ └── AwtScreenGrabber.java │ │ └── AbstractScreenGrabber.java │ ├── ScreenCastController.java │ └── SimpleScreenCastService.java │ ├── settings │ ├── Settings.java │ ├── SettingsEvent.java │ └── SettingsService.java │ ├── filestore │ ├── FileRepository.java │ ├── FileInfo.java │ ├── FileStore.java │ ├── FileService.java │ ├── FileEntity.java │ ├── SimpleFileService.java │ ├── FileStoreController.java │ └── LocalFileStore.java │ ├── config │ ├── WebsocketDestinations.java │ ├── MainConfig.java │ ├── FileUploadConfig.java │ ├── WebSecurityConfig.java │ └── WebSocketConfig.java │ ├── notes │ ├── NoteRepository.java │ ├── NoteService.java │ ├── Note.java │ ├── NoteEvent.java │ ├── NoteEntity.java │ ├── MarkdownFormatter.java │ ├── SimpleNoteService.java │ └── NotesController.java │ ├── security │ └── AccessGuard.java │ ├── web │ └── WebUi.java │ ├── ScreenCasterProperties.java │ ├── App.java │ └── tray │ └── SystemTrayRegistrar.java ├── suppressed-cves.xml ├── .gitignore ├── .editorconfig ├── readme.md ├── etc └── intellij-formatter │ └── tdlabs.xml ├── mvnw.cmd ├── mvnw └── pom.xml /data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /start-screencast.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | java -jar target/screen-casting-app.jar -------------------------------------------------------------------------------- /start-screencast.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | java -jar target/screen-casting-app.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdarimont/screen-casting-app/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip 2 | -------------------------------------------------------------------------------- /src/main/resources/static/img/ball-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdarimont/screen-casting-app/HEAD/src/main/resources/static/img/ball-blue.png -------------------------------------------------------------------------------- /src/main/resources/static/img/ball-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdarimont/screen-casting-app/HEAD/src/main/resources/static/img/ball-gray.png -------------------------------------------------------------------------------- /src/main/resources/static/img/new-note-icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdarimont/screen-casting-app/HEAD/src/main/resources/static/img/new-note-icon-128x128.png -------------------------------------------------------------------------------- /suppressed-cves.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/screencast/ScreenCastService.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.screencast; 2 | 3 | public interface ScreenCastService { 4 | 5 | byte[] getLatestScreenShotImageBytes(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/settings/Settings.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.settings; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | class Settings { 7 | private volatile boolean castEnabled; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/screencast/grabbing/ScreenGrabber.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.screencast.grabbing; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | public interface ScreenGrabber { 6 | BufferedImage grab(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/filestore/FileRepository.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.filestore; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | interface FileRepository extends CrudRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/config/WebsocketDestinations.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.config; 2 | 3 | public interface WebsocketDestinations { 4 | 5 | String TOPIC_NOTES = "/topic/notes"; 6 | 7 | String TOPIC_SETTINGS = "/topic/settings"; 8 | 9 | String TOPIC_POINTER = "/topic/pointer"; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/filestore/FileInfo.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.filestore; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class FileInfo { 7 | 8 | private final String id; 9 | 10 | private final String name; 11 | 12 | private final String contentType; 13 | 14 | private final long sizeInBytes; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/notes/NoteRepository.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.notes; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.List; 6 | 7 | interface NoteRepository extends JpaRepository { 8 | 9 | List findAllByOrderByCreatedAtAsc(); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/filestore/FileStore.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.filestore; 2 | 3 | import org.springframework.web.multipart.MultipartFile; 4 | 5 | import java.io.InputStream; 6 | import java.nio.file.Path; 7 | 8 | interface FileStore { 9 | 10 | Path save(MultipartFile file); 11 | 12 | InputStream getContents(Path path); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/notes/NoteService.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.notes; 2 | 3 | import java.util.List; 4 | 5 | public interface NoteService { 6 | 7 | NoteEntity save(NoteEntity note); 8 | 9 | NoteEntity findById(Long id); 10 | 11 | List findAll(); 12 | 13 | void delete(NoteEntity note); 14 | 15 | void deleteAll(); 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | !data/.gitkeep 5 | data/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | nbproject/private/ 23 | build/ 24 | nbbuild/ 25 | dist/ 26 | nbdist/ 27 | .nb-gradle/ 28 | 29 | *.log 30 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/filestore/FileService.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.filestore; 2 | 3 | import org.springframework.web.multipart.MultipartFile; 4 | 5 | import java.io.InputStream; 6 | import java.util.UUID; 7 | 8 | public interface FileService { 9 | 10 | FileInfo save(MultipartFile file); 11 | 12 | FileEntity loadFile(UUID id); 13 | 14 | InputStream getContents(FileEntity file); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/notes/Note.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.notes; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.NotEmpty; 6 | import java.time.LocalDateTime; 7 | 8 | @Data 9 | class Note { 10 | 11 | private Long id; 12 | 13 | @NotEmpty 14 | private String html; 15 | 16 | private String text; 17 | 18 | private LocalDateTime createdAt; 19 | 20 | private LocalDateTime updatedAt; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/settings/SettingsEvent.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.settings; 2 | 3 | import lombok.Data; 4 | 5 | public interface SettingsEvent { 6 | 7 | default String getType() { 8 | return getClass().getSimpleName().toLowerCase(); 9 | } 10 | 11 | static SettingsEvent updated(Settings settings) { 12 | return new SettingsEvent.Updated(settings); 13 | } 14 | 15 | @Data 16 | class Updated implements SettingsEvent { 17 | final Settings settings; 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/config/MainConfig.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 7 | 8 | @Configuration 9 | @EnableScheduling 10 | @EnableJpaAuditing 11 | @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) 12 | class MainConfig { 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | insert_final_newline=false 5 | indent_style=space 6 | indent_size=2 7 | 8 | [{*.sht,*.htm,*.html,*.shtm,*.shtml,*.ng}] 9 | indent_style=space 10 | indent_size=2 11 | 12 | [{.babelrc,.stylelintrc,.eslintrc,*.bowerrc,*.jsb3,*.jsb2,*.json}] 13 | indent_style=space 14 | indent_size=2 15 | 16 | [*.java] 17 | indent_style=space 18 | indent_size=2 19 | 20 | [{*.applejs,*.js}] 21 | indent_style=space 22 | indent_size=2 23 | 24 | [{*.ddl,*.sql}] 25 | indent_style=space 26 | indent_size=2 27 | 28 | [{*.yml,*.yaml}] 29 | indent_style=space 30 | indent_size=2 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/screencast/ScreenCastController.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.screencast; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.http.MediaType; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @RestController 9 | @RequiredArgsConstructor 10 | class ScreenCastController { 11 | 12 | private final ScreenCastService screenShotService; 13 | 14 | @GetMapping(path = "/screenshot.jpg", produces = MediaType.IMAGE_JPEG_VALUE) 15 | byte[] getScreenshot() { 16 | return screenShotService.getLatestScreenShotImageBytes(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/security/AccessGuard.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.security; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | 9 | @Slf4j 10 | @Component 11 | @RequiredArgsConstructor 12 | public class AccessGuard { 13 | 14 | private final HttpServletRequest currentRequest; 15 | 16 | public boolean isStreamerRequest() { 17 | String remoteAddr = currentRequest.getRemoteAddr(); 18 | String localAddr = currentRequest.getLocalAddr(); 19 | 20 | if (remoteAddr.equals(localAddr)) { 21 | return true; 22 | } 23 | 24 | log.warn("Access denied. remoteAddr=%s localAddr=%s%n", remoteAddr, localAddr); 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/notes/NoteEvent.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.notes; 2 | 3 | import lombok.Data; 4 | 5 | public interface NoteEvent { 6 | 7 | default String getType() { 8 | return getClass().getSimpleName().toLowerCase(); 9 | } 10 | 11 | static NoteEvent created(Note note) { 12 | return new Created(note); 13 | } 14 | 15 | static NoteEvent deleted(Note note) { 16 | return new Deleted(note.getId()); 17 | } 18 | 19 | static NoteEvent updated(Note note) { 20 | return new Updated(note); 21 | } 22 | 23 | @Data 24 | class Created implements NoteEvent { 25 | final Note note; 26 | } 27 | 28 | @Data 29 | class Deleted implements NoteEvent { 30 | final Long noteId; 31 | } 32 | 33 | @Data 34 | class Updated implements NoteEvent { 35 | final Note note; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Screen Casting App 2 | Java app that captures screenshots of the current machine and exposes them via HTTP. 3 | 4 | ## Build 5 | ``` 6 | mvn clean package 7 | ``` 8 | 9 | ## Run 10 | ``` 11 | java -jar target/screen-casting-app.jar 12 | ``` 13 | 14 | ## Configuration 15 | 16 | The screen-casting-app listens on port 9999 by default. One can customize the port by setting the 17 | `server.port` system property. 18 | ``` 19 | -Dserver.port=1234 20 | ``` 21 | 22 | The display to grab can be configured via the `screencaster.grabbing.screenNo` system property. 23 | The default `-1` selects the primary screen. 24 | 25 | To select the second screen use: 26 | ``` 27 | -Dscreencaster.grabbing.screenNo=1 28 | ``` 29 | 30 | The image quality can be adjusted via the `screencaster.grabbing.quality` system property. 31 | The value must be a float between `0.0` and `1.0`. The default quality is `0.7`. 32 | ``` 33 | -Dscreencaster.grabbing.quality=0.5 34 | ``` -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/screencast/grabbing/awt/AwtScreenGrabber.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.screencast.grabbing.awt; 2 | 3 | import de.tdlabs.apps.screencaster.ScreenCasterProperties; 4 | import de.tdlabs.apps.screencaster.screencast.grabbing.AbstractScreenGrabber; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.annotation.PostConstruct; 8 | import java.awt.*; 9 | import java.awt.image.BufferedImage; 10 | 11 | @Component 12 | class AwtScreenGrabber extends AbstractScreenGrabber { 13 | 14 | private Robot robot; 15 | 16 | public AwtScreenGrabber(ScreenCasterProperties screenCastProperties) { 17 | super(screenCastProperties); 18 | } 19 | 20 | @PostConstruct 21 | public void start() { 22 | try { 23 | this.robot = new Robot(getScreen()); 24 | } catch (AWTException e) { 25 | throw new RuntimeException(e); 26 | } 27 | } 28 | 29 | @Override 30 | public BufferedImage grab() { 31 | return robot.createScreenCapture(getScreenRect()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/filestore/FileEntity.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.filestore; 2 | 3 | import lombok.Data; 4 | import org.springframework.data.annotation.CreatedDate; 5 | import org.springframework.data.annotation.LastModifiedDate; 6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 7 | 8 | import javax.persistence.Entity; 9 | import javax.persistence.EntityListeners; 10 | import javax.persistence.Id; 11 | import javax.persistence.Table; 12 | import java.time.LocalDateTime; 13 | 14 | @Data 15 | @Entity 16 | @Table(name = "file") 17 | @EntityListeners(AuditingEntityListener.class) 18 | class FileEntity { 19 | 20 | @Id 21 | private String id; 22 | 23 | private String path; 24 | 25 | private String name; 26 | 27 | private String contentType; 28 | 29 | @CreatedDate 30 | private LocalDateTime createdAt; 31 | 32 | @LastModifiedDate 33 | private LocalDateTime updatedAt; 34 | 35 | private long sizeInBytes; 36 | 37 | public FileInfo toInfo() { 38 | return new FileInfo(id, name, contentType, sizeInBytes); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/config/FileUploadConfig.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.config; 2 | 3 | import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties; 4 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.web.multipart.commons.CommonsMultipartResolver; 8 | 9 | import javax.servlet.MultipartConfigElement; 10 | 11 | @Configuration 12 | @EnableConfigurationProperties(MultipartProperties.class) 13 | class FileUploadConfig { 14 | 15 | 16 | @Bean(name = "multipartResolver") 17 | public CommonsMultipartResolver multipartResolver(MultipartProperties multipartProperties) { 18 | 19 | MultipartConfigElement multipartConfig = multipartProperties.createMultipartConfig(); 20 | 21 | CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); 22 | multipartResolver.setMaxUploadSize(multipartConfig.getMaxFileSize()); 23 | return multipartResolver; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 | import org.springframework.security.core.userdetails.UserDetailsService; 8 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; 9 | 10 | import java.util.Collections; 11 | 12 | @Configuration 13 | class WebSecurityConfig extends WebSecurityConfigurerAdapter { 14 | 15 | @Override 16 | protected void configure(HttpSecurity http) throws Exception { 17 | 18 | http 19 | .authorizeRequests() 20 | .anyRequest().permitAll() 21 | .and().headers().frameOptions().deny() 22 | .and().formLogin().disable() 23 | .httpBasic().disable() 24 | ; 25 | } 26 | 27 | @Bean 28 | public UserDetailsService userDetailsService() { 29 | return new InMemoryUserDetailsManager(Collections.emptyList()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jackson: 3 | serialization: 4 | write_dates_as_timestamps: false 5 | thymeleaf: 6 | cache: false 7 | resources: 8 | chain: 9 | cache: false 10 | datasource: 11 | url: jdbc:h2:file:./data/${screencaster.database}/notesv2;DB_CLOSE_ON_EXIT=FALSE 12 | jpa: 13 | hibernate: 14 | ddl-auto: update 15 | servlet: 16 | multipart: 17 | # we replace springs default multipart handling 18 | enabled: false 19 | # but we still use the multipart configuration properties 20 | max-file-size: 64MB 21 | jmx: 22 | enabled: false 23 | 24 | server: 25 | port: 9999 26 | compression: 27 | enabled: true 28 | mime-types: application/json,application/xml,text/html,text/xml,text/plain,text/css,application/javascript,image/jpeg,image/png 29 | servlet: 30 | context-path: 31 | jetty: 32 | accesslog: 33 | enabled: true 34 | ignore-paths: ["/screenshot.jpg","/webjars/*","/css/*","/js/*"] 35 | 36 | 37 | screencaster: 38 | refreshIntervalMillis: 250 39 | refreshPointerMillis: 150 40 | database: default 41 | grabbing: 42 | quality: 0.7 43 | fileStore: 44 | location: data 45 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | import static de.tdlabs.apps.screencaster.config.WebsocketDestinations.TOPIC_NOTES; 10 | import static de.tdlabs.apps.screencaster.config.WebsocketDestinations.TOPIC_POINTER; 11 | import static de.tdlabs.apps.screencaster.config.WebsocketDestinations.TOPIC_SETTINGS; 12 | 13 | @Configuration 14 | @EnableWebSocketMessageBroker 15 | class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 16 | 17 | @Override 18 | public void configureMessageBroker(MessageBrokerRegistry config) { 19 | config.enableSimpleBroker(TOPIC_NOTES, TOPIC_SETTINGS, TOPIC_POINTER); 20 | config.setApplicationDestinationPrefixes("/app"); 21 | } 22 | 23 | @Override 24 | public void registerStompEndpoints(StompEndpointRegistry registry) { 25 | registry.addEndpoint("/screencaster/ws").withSockJS(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/web/WebUi.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.web; 2 | 3 | import de.tdlabs.apps.screencaster.settings.SettingsService; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.core.env.Environment; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import java.net.Inet4Address; 12 | 13 | @Controller 14 | @RequiredArgsConstructor 15 | class WebUi { 16 | 17 | private final SettingsService settingsService; 18 | 19 | private final Environment env; 20 | 21 | @GetMapping("/") 22 | String index(Model model, HttpServletRequest request) throws Exception { 23 | 24 | model.addAttribute("hostname", Inet4Address.getLocalHost().getHostName()); 25 | 26 | model.addAttribute("screencastUrl", "http://" + Inet4Address.getLocalHost().getHostName() + ":" + env.getProperty("server.port") + "/"); 27 | model.addAttribute("settings", this.settingsService); 28 | model.addAttribute("requestType", isLocalRequest(request) ? "caster" : "watcher"); 29 | 30 | return "index"; 31 | } 32 | 33 | private boolean isLocalRequest(HttpServletRequest request) { 34 | return request.getRemoteAddr().equals(request.getLocalAddr()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/filestore/SimpleFileService.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.filestore; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.web.multipart.MultipartFile; 6 | 7 | import java.io.InputStream; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.util.UUID; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | class SimpleFileService implements FileService { 15 | 16 | private final FileRepository fileRepository; 17 | 18 | private final FileStore fileStore; 19 | 20 | 21 | @Override 22 | public FileInfo save(MultipartFile file) { 23 | 24 | Path path = fileStore.save(file); 25 | 26 | FileEntity fileEntity = new FileEntity(); 27 | fileEntity.setId(UUID.randomUUID().toString()); 28 | fileEntity.setName(file.getOriginalFilename()); 29 | fileEntity.setContentType(file.getContentType()); 30 | fileEntity.setPath(path.normalize().toString()); 31 | fileEntity.setSizeInBytes(file.getSize()); 32 | 33 | FileEntity saved = fileRepository.save(fileEntity); 34 | 35 | return saved.toInfo(); 36 | } 37 | 38 | @Override 39 | public FileEntity loadFile(UUID id) { 40 | return fileRepository.findById(id.toString()).orElse(null); 41 | } 42 | 43 | @Override 44 | public InputStream getContents(FileEntity file) { 45 | return fileStore.getContents(Paths.get(file.getPath())); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/settings/SettingsService.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.settings; 2 | 3 | import de.tdlabs.apps.screencaster.ScreenCasterProperties; 4 | import de.tdlabs.apps.screencaster.config.WebsocketDestinations; 5 | import org.springframework.messaging.simp.SimpMessagingTemplate; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | @Service 12 | public class SettingsService { 13 | 14 | private final SimpMessagingTemplate messagingTemplate; 15 | 16 | private Settings settings = new Settings(); 17 | 18 | public SettingsService(SimpMessagingTemplate messagingTemplate, ScreenCasterProperties screenCasterProperties) { 19 | this.messagingTemplate = messagingTemplate; 20 | this.settings.setCastEnabled(screenCasterProperties.getScreencast().isAutoStart()); 21 | } 22 | 23 | public boolean isCastEnabled() { 24 | return settings.isCastEnabled(); 25 | } 26 | 27 | public void enableCast() { 28 | this.settings.setCastEnabled(true); 29 | publishSettingsUpdate(); 30 | } 31 | 32 | public void disableCast() { 33 | this.settings.setCastEnabled(false); 34 | Executors.newSingleThreadScheduledExecutor() // 35 | .schedule(this::publishSettingsUpdate, 1500L, TimeUnit.MILLISECONDS); 36 | } 37 | 38 | void publishSettingsUpdate() { 39 | messagingTemplate.convertAndSend(WebsocketDestinations.TOPIC_SETTINGS, SettingsEvent.updated(settings)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /etc/intellij-formatter/tdlabs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/notes/NoteEntity.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.notes; 2 | 3 | import lombok.Data; 4 | import org.springframework.data.annotation.CreatedDate; 5 | import org.springframework.data.annotation.LastModifiedDate; 6 | import org.springframework.data.jpa.domain.AbstractPersistable; 7 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 8 | 9 | import javax.persistence.Column; 10 | import javax.persistence.Entity; 11 | import javax.persistence.EntityListeners; 12 | import javax.persistence.Table; 13 | import javax.persistence.Transient; 14 | import javax.persistence.Version; 15 | import javax.validation.constraints.NotEmpty; 16 | import java.time.LocalDateTime; 17 | 18 | @Data 19 | @Entity 20 | @Table(name = "note") 21 | @EntityListeners(AuditingEntityListener.class) 22 | class NoteEntity extends AbstractPersistable { 23 | 24 | @NotEmpty 25 | @Column(length = 64000) 26 | private String text; 27 | 28 | @Transient 29 | private String html; 30 | 31 | @CreatedDate 32 | private LocalDateTime createdAt; 33 | 34 | @LastModifiedDate 35 | private LocalDateTime updatedAt; 36 | 37 | @Version 38 | private Long version; 39 | 40 | public static NoteEntity valueOf(Note note) { 41 | NoteEntity ne = new NoteEntity(); 42 | ne.setText(note.getText()); 43 | return ne; 44 | } 45 | 46 | public Note toNote() { 47 | 48 | Note n = new Note(); 49 | n.setText(getText()); 50 | n.setHtml(this.html); 51 | n.setId(getId()); 52 | n.setCreatedAt(getCreatedAt()); 53 | n.setUpdatedAt(getUpdatedAt()); 54 | 55 | return n; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/notes/MarkdownFormatter.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.notes; 2 | 3 | import org.commonmark.Extension; 4 | import org.commonmark.ext.autolink.AutolinkExtension; 5 | import org.commonmark.node.Image; 6 | import org.commonmark.node.Link; 7 | import org.commonmark.node.Node; 8 | import org.commonmark.parser.Parser; 9 | import org.commonmark.renderer.html.AttributeProvider; 10 | import org.commonmark.renderer.html.HtmlRenderer; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.Collections; 14 | import java.util.Map; 15 | 16 | @Component 17 | class MarkdownFormatter { 18 | 19 | private final Extension autolinkExtension; 20 | 21 | public MarkdownFormatter() { 22 | autolinkExtension = AutolinkExtension.create(); 23 | } 24 | 25 | public String format(String text) { 26 | 27 | Parser parser = Parser.builder() // 28 | .extensions(Collections.singletonList(autolinkExtension)) 29 | .build(); 30 | Node document = parser.parse(text); 31 | HtmlRenderer renderer = HtmlRenderer.builder() // 32 | .attributeProviderFactory(context -> CustomAttributeProvider.INSTANCE) // 33 | .build(); 34 | 35 | return renderer.render(document); 36 | } 37 | 38 | enum CustomAttributeProvider implements AttributeProvider { 39 | 40 | INSTANCE; 41 | 42 | @Override 43 | public void setAttributes(Node node, String tagName, Map attributes) { 44 | 45 | if (node instanceof Link) { 46 | attributes.put("target", "_blank"); 47 | } else if (node instanceof Image) { 48 | attributes.put("loading", "lazy"); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/filestore/FileStoreController.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.filestore; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.core.io.InputStreamResource; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import org.springframework.web.multipart.MultipartFile; 13 | 14 | import java.io.InputStream; 15 | import java.util.UUID; 16 | 17 | import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION; 18 | import static org.springframework.http.HttpHeaders.CONTENT_TYPE; 19 | 20 | @RestController 21 | @RequestMapping("/files") 22 | @RequiredArgsConstructor 23 | class FileStoreController { 24 | 25 | private final FileService fileService; 26 | 27 | @PostMapping 28 | @PreAuthorize("@accessGuard.isStreamerRequest()") 29 | public ResponseEntity> upload(MultipartFile file) { 30 | 31 | FileInfo ref = fileService.save(file); 32 | 33 | return ResponseEntity.ok(ref); 34 | } 35 | 36 | @GetMapping("/{id}") 37 | public ResponseEntity> download(@PathVariable("id") UUID id) { 38 | 39 | FileEntity file = fileService.loadFile(id); 40 | InputStream is = fileService.getContents(file); 41 | 42 | return ResponseEntity.ok() 43 | .header(CONTENT_TYPE, file.getContentType()) 44 | .header(CONTENT_DISPOSITION, "inline; filename=" + file.getName()) 45 | .body(new InputStreamResource(is)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/screencast/grabbing/AbstractScreenGrabber.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.screencast.grabbing; 2 | 3 | import de.tdlabs.apps.screencaster.ScreenCasterProperties; 4 | 5 | import java.awt.*; 6 | 7 | public abstract class AbstractScreenGrabber implements ScreenGrabber{ 8 | 9 | private final Rectangle screenRect; 10 | 11 | private final GraphicsDevice screen; 12 | 13 | public AbstractScreenGrabber(ScreenCasterProperties screenCastProperties) { 14 | 15 | ScreenCasterProperties.ScreenGrabbingProperties screenGrabbingProperties = screenCastProperties.getGrabbing(); 16 | this.screen = selectScreenDevice(screenGrabbingProperties); 17 | this.screenRect = getScreenRectangle(screen); 18 | } 19 | 20 | private Rectangle getScreenRectangle(GraphicsDevice screen) { 21 | DisplayMode displayMode = screen.getDisplayMode(); 22 | return new Rectangle(displayMode.getWidth(), displayMode.getHeight()); 23 | } 24 | 25 | private GraphicsDevice selectScreenDevice(ScreenCasterProperties.ScreenGrabbingProperties screenGrabbingProperties) { 26 | 27 | GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); 28 | if (screenGrabbingProperties.isGrabDefaultScreen()) { 29 | return ge.getDefaultScreenDevice(); 30 | } 31 | 32 | GraphicsDevice[] screenDevices = ge.getScreenDevices(); 33 | 34 | int screenNo = screenGrabbingProperties.getScreenNo(); 35 | if (screenNo < 0 || screenNo >= screenDevices.length) { 36 | throw new IllegalArgumentException("invalid screenNo: " + screenNo); 37 | } 38 | 39 | return screenDevices[screenNo]; 40 | } 41 | 42 | public Rectangle getScreenRect() { 43 | return screenRect; 44 | } 45 | 46 | public GraphicsDevice getScreen() { 47 | return screen; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/ScreenCasterProperties.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Data 8 | @Component 9 | @ConfigurationProperties(prefix = "screencaster") 10 | public class ScreenCasterProperties { 11 | 12 | private ScreenGrabbingProperties grabbing = new ScreenGrabbingProperties(); 13 | 14 | private ScreencastProperties screencast = new ScreencastProperties(); 15 | 16 | private FileStoreProperties fileStore = new FileStoreProperties(); 17 | 18 | @Data 19 | public static class ScreencastProperties { 20 | 21 | /** 22 | * Controls whether screen casting should start immediately after program start. Defaults to {@literal true}. 23 | */ 24 | private boolean autoStart = true; 25 | 26 | /** 27 | * Controls whether the mouse pointer should be visible. Defaults to {@literal true}. 28 | */ 29 | private boolean mouseVisible = true; 30 | } 31 | 32 | @Data 33 | public static class ScreenGrabbingProperties { 34 | 35 | public static final int DEFAULT_SCREEN = -1; 36 | 37 | public static final float DEFAULT_QUALITY = 0.7f; 38 | 39 | /** 40 | * Selects the screen to grab. {@literal -1} means default screen. 41 | */ 42 | private int screenNo = DEFAULT_SCREEN; 43 | 44 | /** 45 | * Selects the default quality. Must be between {@literal 0.0} and {@literal 1.0}. 46 | */ 47 | private float quality = DEFAULT_QUALITY; 48 | 49 | public boolean isGrabDefaultScreen() { 50 | return screenNo == DEFAULT_SCREEN; 51 | } 52 | } 53 | 54 | 55 | @Data 56 | public static class FileStoreProperties { 57 | 58 | private String location; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/App.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.context.event.ApplicationReadyEvent; 7 | import org.springframework.context.event.EventListener; 8 | 9 | import java.net.InetAddress; 10 | import java.net.UnknownHostException; 11 | 12 | @SpringBootApplication 13 | public class App { 14 | 15 | @Value("${server.port}") 16 | String serverPort; 17 | 18 | String scheme = "http"; 19 | 20 | public static void main(String[] args) { 21 | 22 | System.setProperty("java.awt.headless", "false"); 23 | 24 | SpringApplication.run(App.class, args); 25 | } 26 | 27 | @EventListener 28 | public void run(ApplicationReadyEvent are) { 29 | 30 | String hostName = tryResolveHostnameWithFallbackToLocalhost(); 31 | String ipAddress = tryGetIpAddressWithFallbackToLoopback(); 32 | 33 | System.out.println("########################################################>"); 34 | System.out.printf("####### Screencast URLs%n"); 35 | System.out.printf("####### %s://%s:%s/%n", scheme, hostName, serverPort); 36 | System.out.printf("####### %s://%s:%s/%n", scheme, ipAddress, serverPort); 37 | System.out.println("########################################################>"); 38 | } 39 | 40 | private String tryResolveHostnameWithFallbackToLocalhost() { 41 | 42 | try { 43 | return InetAddress.getLocalHost().getHostName(); 44 | } catch (UnknownHostException uhe) { 45 | System.err.printf("%s. Trying IP based configuration...%n", uhe.getMessage()); 46 | return "localhost"; 47 | } 48 | } 49 | 50 | private String tryGetIpAddressWithFallbackToLoopback() { 51 | try { 52 | return InetAddress.getLocalHost().getHostAddress(); 53 | } catch (UnknownHostException innerUhe) { 54 | System.err.printf("Could not determine host address. Using 127.0.0.1 as fallback...%n"); 55 | System.err.println("You need to determine the hostname yourself!"); 56 | return "127.0.0.1"; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/filestore/LocalFileStore.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.filestore; 2 | 3 | import de.tdlabs.apps.screencaster.ScreenCasterProperties; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.multipart.MultipartFile; 7 | 8 | import java.io.BufferedInputStream; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.time.LocalDateTime; 15 | import java.time.format.DateTimeFormatter; 16 | import java.util.concurrent.atomic.AtomicLong; 17 | 18 | @Slf4j 19 | @Component 20 | class LocalFileStore implements FileStore { 21 | 22 | private final String storeLocation; 23 | 24 | private final AtomicLong counter = new AtomicLong(); 25 | 26 | public LocalFileStore(ScreenCasterProperties props) { 27 | this.storeLocation = props.getFileStore().getLocation(); 28 | } 29 | 30 | @Override 31 | public Path save(MultipartFile file) { 32 | 33 | LocalDateTime now = LocalDateTime.now(); 34 | 35 | String folderName = DateTimeFormatter.ofPattern("'upload/'yyyy-MM-dd").format(now); 36 | long differencer = counter.incrementAndGet() % Long.MAX_VALUE; 37 | String filename = DateTimeFormatter.ofPattern("yyyy-MM-dd-hh-mm-ss-SSS").format(now); 38 | 39 | File folder = new File(storeLocation, folderName); 40 | 41 | if (!folder.exists() && !folder.mkdirs()) { 42 | log.error("Could not create folder {}", folder); 43 | } 44 | 45 | File destination = new File(folder, filename + "_" + differencer); 46 | 47 | try { 48 | Files.copy(file.getInputStream(), destination.toPath()); 49 | } catch (IOException e) { 50 | log.error("Could not save file to {}", destination, e); 51 | } 52 | 53 | return destination.toPath(); 54 | } 55 | 56 | @Override 57 | public InputStream getContents(Path path) { 58 | 59 | try { 60 | return new BufferedInputStream(Files.newInputStream(path)); 61 | } catch (IOException e) { 62 | log.error("Could not read file contents from {}", path, e); 63 | } 64 | 65 | return null; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/notes/SimpleNoteService.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.notes; 2 | 3 | import de.tdlabs.apps.screencaster.config.WebsocketDestinations; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.messaging.simp.SimpMessagingTemplate; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | 13 | @Service 14 | @Transactional 15 | @RequiredArgsConstructor 16 | class SimpleNoteService implements NoteService { 17 | 18 | private final NoteRepository noteRepository; 19 | 20 | private final SimpMessagingTemplate messagingTemplate; 21 | 22 | private final MarkdownFormatter markdownFormatter; 23 | 24 | public NoteEntity save(NoteEntity noteEntity) { 25 | 26 | boolean newNote = noteEntity.isNew(); 27 | 28 | noteEntity.setHtml(markdownFormatter.format(noteEntity.getText())); 29 | NoteEntity saved = noteRepository.save(noteEntity); 30 | 31 | Note note = saved.toNote(); 32 | 33 | NoteEvent noteEvent = newNote ? NoteEvent.created(note) : NoteEvent.updated(note); 34 | 35 | this.messagingTemplate.convertAndSend(WebsocketDestinations.TOPIC_NOTES, noteEvent); 36 | 37 | return saved; 38 | } 39 | 40 | public void delete(NoteEntity noteEntity) { 41 | 42 | noteRepository.delete(noteEntity); 43 | this.messagingTemplate.convertAndSend("/topic/notes", NoteEvent.deleted(noteEntity.toNote())); 44 | } 45 | 46 | public void deleteAll() { 47 | noteRepository.deleteAllInBatch(); 48 | } 49 | 50 | @Transactional(readOnly = true) 51 | public NoteEntity findById(Long id) { 52 | return noteRepository.findById(id).map(this::renderNoteHtml).orElse(null); 53 | } 54 | 55 | @Transactional(readOnly = true) 56 | public List findAll() { 57 | 58 | Stream noteStream = noteRepository.findAllByOrderByCreatedAtAsc().stream(); 59 | return noteStream.map(this::renderNoteHtml).collect(Collectors.toList()); 60 | } 61 | 62 | NoteEntity renderNoteHtml(NoteEntity noteEntity) { 63 | 64 | String text = noteEntity.getText(); 65 | String html = markdownFormatter.format(text); 66 | noteEntity.setHtml(html); 67 | 68 | return noteEntity; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/resources/static/css/app.css: -------------------------------------------------------------------------------- 1 | #screen { 2 | /* display: none; */ 3 | /* User cannot mark the image anymore */ 4 | -moz-user-select: none; 5 | -webkit-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | -o-user-select: none; 9 | 10 | -webkit-user-drag: auto | element | none; 11 | } 12 | 13 | html, body { /* Removes unnecessary borders */ 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | .container { 19 | margin-left: 0 !important; 20 | margin-right: 0 !important; 21 | width: 100%; 22 | } 23 | 24 | #screenContainer { 25 | text-align: center; 26 | position: relative; 27 | } 28 | 29 | .screen-fit { 30 | height: 100vh; 31 | } 32 | 33 | .modal-dialog { 34 | width: 80% !important; 35 | background-color: #ffffff; 36 | border: 1px solid grey; 37 | } 38 | 39 | #btnShowNotes { 40 | position: absolute; 41 | top: 15px; 42 | left: 15px; 43 | opacity: 0.35; 44 | z-index: 1000; 45 | } 46 | 47 | #btnShowNotes:hover { 48 | opacity: 1.0; 49 | } 50 | 51 | #notesListContainer { 52 | max-height: 500px; 53 | overflow-y: auto; 54 | position: relative; 55 | padding-right: 15px; 56 | } 57 | 58 | #notesList li { 59 | margin-bottom: 20px; 60 | margin-left: 40px; 61 | border: none !important; 62 | } 63 | 64 | #notesList li:last-child { 65 | margin-bottom: 0px; 66 | } 67 | 68 | #notesList li img { 69 | max-width: 100%; 70 | max-height: 100%; 71 | } 72 | 73 | #notesList { 74 | margin-bottom: 0px; 75 | } 76 | 77 | .note-time { 78 | color: #999; 79 | float: right; 80 | } 81 | 82 | .note.read span.new-note-marker { 83 | visibility: hidden; 84 | } 85 | 86 | .note.new .new-note-marker { 87 | color: #f3f; 88 | } 89 | 90 | .note-section-separator { 91 | margin-top: 5px; 92 | margin-bottom: 5px; 93 | } 94 | 95 | .modal-title { 96 | display: inline; 97 | } 98 | 99 | .screen-cast-url { 100 | color: #f3f; 101 | font-size: 80px; 102 | } 103 | 104 | #overlay { 105 | position: absolute; 106 | right: 0; 107 | top: 0; 108 | left: 0; 109 | bottom: 0; 110 | width: 100%; 111 | height: 100%; 112 | } 113 | 114 | button.close { 115 | font-size: 42px; 116 | } 117 | 118 | .note-content img { 119 | width: 480px; 120 | } 121 | 122 | .note-content img:hover + .hint-fullscreen { 123 | display: inline-block; 124 | position: relative; 125 | left: -240px; 126 | opacity: 0.10; 127 | font-size: 42px; 128 | } 129 | 130 | .note-id { 131 | color: gray; 132 | float: left; 133 | font-size: 20px; 134 | margin-left: -50px; 135 | } 136 | 137 | .hint-fullscreen { 138 | display: none; 139 | } -------------------------------------------------------------------------------- /src/main/resources/static/img/scroll-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 55 | 59 | 64 | 69 | 70 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/notes/NotesController.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.notes; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.http.MediaType; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | import org.springframework.web.bind.annotation.DeleteMapping; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.PutMapping; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import org.springframework.web.util.UriComponentsBuilder; 15 | 16 | import java.net.URI; 17 | import java.util.List; 18 | 19 | import static java.util.stream.Collectors.toList; 20 | 21 | @RestController 22 | @RequestMapping("/notes") 23 | @RequiredArgsConstructor 24 | class NotesController { 25 | 26 | private final NoteService noteService; 27 | 28 | @PreAuthorize("@accessGuard.isStreamerRequest()") 29 | @PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_FORM_URLENCODED_VALUE}) 30 | public ResponseEntity> createNote(Note note, UriComponentsBuilder uriBuilder) { 31 | 32 | NoteEntity saved = noteService.save(NoteEntity.valueOf(note)); 33 | URI location = uriBuilder.path("/notes/{id}").buildAndExpand(saved.getId()).toUri(); 34 | 35 | return ResponseEntity.created(location).build(); 36 | } 37 | 38 | @PreAuthorize("@accessGuard.isStreamerRequest()") 39 | @PutMapping(path = "/{noteId}", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_FORM_URLENCODED_VALUE}) 40 | public ResponseEntity> updateNote(@PathVariable("noteId") Long id, Note note) { 41 | 42 | NoteEntity stored = noteService.findById(id); 43 | if (stored == null) { 44 | return ResponseEntity.notFound().build(); 45 | } 46 | 47 | stored.setText(note.getText()); 48 | 49 | stored = noteService.save(stored); 50 | return ResponseEntity.ok(stored.toNote()); 51 | } 52 | 53 | @GetMapping 54 | public ResponseEntity> findAll() { 55 | return ResponseEntity.ok( 56 | noteService.findAll() 57 | .stream() 58 | .map(NoteEntity::toNote) 59 | .collect(toList()) 60 | ); 61 | } 62 | 63 | @DeleteMapping 64 | @PreAuthorize("@accessGuard.isStreamerRequest()") 65 | public ResponseEntity> deleteAll() { 66 | 67 | noteService.deleteAll(); 68 | return ResponseEntity.noContent().build(); 69 | } 70 | 71 | @GetMapping("/{id}") 72 | public ResponseEntity findById(@PathVariable("id") Long id) { 73 | return ResponseEntity.ok(noteService.findById(id).toNote()); 74 | } 75 | 76 | @DeleteMapping("/{id}") 77 | @PreAuthorize("@accessGuard.isStreamerRequest()") 78 | public ResponseEntity> delete(@PathVariable("id") Long id) { 79 | 80 | NoteEntity note = noteService.findById(id); 81 | if (note == null) { 82 | return ResponseEntity.notFound().build(); 83 | } 84 | 85 | noteService.delete(note); 86 | 87 | return ResponseEntity.noContent().build(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/tray/SystemTrayRegistrar.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.tray; 2 | 3 | import de.tdlabs.apps.screencaster.ScreenCasterProperties; 4 | import de.tdlabs.apps.screencaster.settings.SettingsService; 5 | import dorkbox.systemTray.Menu; 6 | import dorkbox.systemTray.MenuItem; 7 | import dorkbox.systemTray.SystemTray; 8 | import dorkbox.util.Desktop; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.context.ConfigurableApplicationContext; 12 | import org.springframework.core.env.Environment; 13 | import org.springframework.stereotype.Component; 14 | 15 | import javax.annotation.PostConstruct; 16 | import javax.annotation.PreDestroy; 17 | import javax.imageio.ImageIO; 18 | import java.awt.image.BufferedImage; 19 | import java.io.IOException; 20 | 21 | @Slf4j 22 | @Component 23 | @RequiredArgsConstructor 24 | class SystemTrayRegistrar { 25 | 26 | private final SettingsService settingsService; 27 | 28 | private final ScreenCasterProperties screenCasterProperties; 29 | 30 | private final ConfigurableApplicationContext applicationContext; 31 | 32 | private final Environment env; 33 | 34 | @PostConstruct 35 | public void init() { 36 | 37 | SystemTray systemTray = SystemTray.get(); 38 | systemTray.setTooltip("Screen Caster"); 39 | 40 | toggleTrayStatus(systemTray, screenCasterProperties.getScreencast().isAutoStart()); 41 | 42 | setupMenu(systemTray); 43 | } 44 | 45 | private void setupMenu(SystemTray systemTray) { 46 | 47 | Menu menu = systemTray.getMenu(); 48 | 49 | if (menu == null) { 50 | log.warn("Skipping menu registration: Couldn't access SystemTray menu."); 51 | return; 52 | } 53 | 54 | menu.add(new MenuItem("Open Screencaster", (e) -> { 55 | try { 56 | Desktop.browseURL("http://localhost:" + env.getProperty("server.port")); 57 | } catch (IOException ioe) { 58 | log.error("Could not browse to Screencaster URL", ioe); 59 | } 60 | })); 61 | 62 | menu.add(new MenuItem("Start Screencast", (e) -> { 63 | settingsService.enableCast(); 64 | toggleTrayStatus(systemTray, true); 65 | })); 66 | 67 | menu.add(new MenuItem("Stop Screencast", (e) -> { 68 | settingsService.disableCast(); 69 | toggleTrayStatus(systemTray, false); 70 | })); 71 | 72 | menu.add(new MenuItem("Quit", (e) -> { 73 | settingsService.disableCast(); 74 | applicationContext.close(); 75 | System.exit(0); 76 | })); 77 | } 78 | 79 | @PreDestroy 80 | public void destroy() { 81 | SystemTray systemTray = SystemTray.get(); 82 | 83 | if (systemTray == null || systemTray.getMenu() == null) { 84 | return; 85 | } 86 | 87 | systemTray.getMenu().remove(); 88 | } 89 | 90 | private void toggleTrayStatus(SystemTray systemTray, boolean enabled) { 91 | 92 | if (enabled) { 93 | systemTray.setImage(ImageResources.CAST_ENABLED_ICON); 94 | systemTray.setStatus("Screencast running"); 95 | } else { 96 | systemTray.setImage(ImageResources.CAST_DISABLED_ICON); 97 | systemTray.setStatus("Screencast paused"); 98 | } 99 | 100 | systemTray.setTooltip(systemTray.getStatus()); 101 | } 102 | 103 | static class ImageResources { 104 | 105 | private static final BufferedImage CAST_ENABLED_ICON; 106 | private static final BufferedImage CAST_DISABLED_ICON; 107 | 108 | static { 109 | try { 110 | CAST_ENABLED_ICON = ImageIO.read(ImageResources.class.getClassLoader().getResourceAsStream("static/img/ball-blue.png")); 111 | CAST_DISABLED_ICON = ImageIO.read(ImageResources.class.getClassLoader().getResourceAsStream("static/img/ball-gray.png")); 112 | } catch (IOException e) { 113 | throw new RuntimeException(e); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/de/tdlabs/apps/screencaster/screencast/SimpleScreenCastService.java: -------------------------------------------------------------------------------- 1 | package de.tdlabs.apps.screencaster.screencast; 2 | 3 | import de.tdlabs.apps.screencaster.ScreenCasterProperties; 4 | import de.tdlabs.apps.screencaster.config.WebsocketDestinations; 5 | import de.tdlabs.apps.screencaster.screencast.grabbing.ScreenGrabber; 6 | import de.tdlabs.apps.screencaster.settings.SettingsService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.messaging.simp.SimpMessagingTemplate; 9 | import org.springframework.scheduling.annotation.Scheduled; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.imageio.IIOImage; 13 | import javax.imageio.ImageIO; 14 | import javax.imageio.ImageWriteParam; 15 | import javax.imageio.ImageWriter; 16 | import javax.imageio.stream.MemoryCacheImageOutputStream; 17 | import java.awt.*; 18 | import java.awt.image.BufferedImage; 19 | import java.io.ByteArrayOutputStream; 20 | import java.util.concurrent.atomic.AtomicReference; 21 | 22 | @Component 23 | @RequiredArgsConstructor 24 | class SimpleScreenCastService implements ScreenCastService { 25 | 26 | private final ScreenGrabber screenGrabber; 27 | 28 | private final SettingsService settingsService; 29 | 30 | private final ScreenCasterProperties screenCasterProperties; 31 | 32 | private final SimpMessagingTemplate messagingTemplate; 33 | 34 | private final AtomicReference currentImage = new AtomicReference<>(); 35 | 36 | private final AtomicReference currentLocation = new AtomicReference<>(); 37 | 38 | @Scheduled(fixedDelayString = "#{${screencaster.refreshIntervalMillis:-1}}") 39 | void updateImage() { 40 | 41 | if (!settingsService.isCastEnabled()) { 42 | usePauseImage(); 43 | return; 44 | } 45 | 46 | useLiveImage(); 47 | } 48 | 49 | @Scheduled(fixedDelayString = "#{${screencaster.refreshPointerMillis:-1}}") 50 | void updatePointerLocation() { 51 | 52 | Point location = MouseInfo.getPointerInfo().getLocation(); 53 | 54 | Point curLoc = currentLocation.get(); 55 | if (curLoc != null && curLoc.equals(location)) { 56 | return; 57 | } 58 | currentLocation.set(location); 59 | 60 | messagingTemplate.convertAndSend(WebsocketDestinations.TOPIC_POINTER, location); 61 | } 62 | 63 | private void useLiveImage() { 64 | currentImage.lazySet(new LiveImage(screenGrabber.grab(), screenCasterProperties.getGrabbing().getQuality())); 65 | } 66 | 67 | private void usePauseImage() { 68 | 69 | LiveImage current = currentImage.get(); 70 | if (current instanceof PauseImage) { 71 | return; 72 | } 73 | 74 | currentImage.lazySet(new PauseImage("Paused...", current)); 75 | } 76 | 77 | public byte[] getLatestScreenShotImageBytes() { 78 | return currentImage.get().getBytes(); 79 | } 80 | 81 | static class LiveImage { 82 | 83 | final BufferedImage screenshot; 84 | final float quality; 85 | 86 | byte[] bytes; 87 | 88 | public LiveImage(BufferedImage screenshot, float quality) { 89 | this.screenshot = screenshot; 90 | this.quality = quality; 91 | } 92 | 93 | public byte[] getBytes() { 94 | 95 | if (bytes != null) { 96 | return bytes; 97 | } 98 | 99 | bytes = toJpegImageAsBytes(getScreenshot()); 100 | 101 | return bytes; 102 | } 103 | 104 | public BufferedImage getScreenshot() { 105 | return screenshot; 106 | } 107 | 108 | private byte[] toJpegImageAsBytes(BufferedImage image) { 109 | 110 | ByteArrayOutputStream baos = new ByteArrayOutputStream(1024 * 400); 111 | 112 | ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next(); 113 | try { 114 | jpgWriter.setOutput(new MemoryCacheImageOutputStream(baos)); 115 | IIOImage outputImage = new IIOImage(image, null, null); 116 | 117 | ImageWriteParam jpgWriteParam = jpgWriter.getDefaultWriteParam(); 118 | jpgWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); 119 | jpgWriteParam.setCompressionQuality(quality); 120 | 121 | jpgWriter.write(null, outputImage, jpgWriteParam); 122 | } catch (Exception e) { 123 | e.printStackTrace(); 124 | } finally { 125 | jpgWriter.dispose(); 126 | } 127 | 128 | return baos.toByteArray(); 129 | } 130 | } 131 | 132 | static class PauseImage extends LiveImage { 133 | 134 | private final String text; 135 | 136 | public PauseImage(String text, LiveImage liveImage) { 137 | super(liveImage.screenshot, liveImage.quality); 138 | this.text = text; 139 | } 140 | 141 | @Override 142 | public BufferedImage getScreenshot() { 143 | 144 | BufferedImage pauseScreenshot = super.getScreenshot(); 145 | 146 | Graphics2D g = (Graphics2D) pauseScreenshot.getGraphics(); 147 | 148 | Font font = new Font(g.getFont().getName(), Font.BOLD, 96); 149 | 150 | FontMetrics metrics = g.getFontMetrics(font); 151 | g.setColor(Color.MAGENTA); 152 | 153 | int x = (pauseScreenshot.getWidth() - metrics.stringWidth(text)) / 2; 154 | int y = ((pauseScreenshot.getHeight() - metrics.getHeight()) / 2) + metrics.getAscent(); 155 | 156 | g.setFont(font); 157 | g.drawString(text, x, y); 158 | g.dispose(); 159 | 160 | return pauseScreenshot; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | Screencast@localhost 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Notes 0 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Screencast 55 | http://localhost:9999 56 | Status: active 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | × 68 | Notes 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | New Note 90 | Markdown 91 | Syntax 92 | 94 | 95 | 96 | Hint: Click Sent or press CTRL+Enter to submit a message 97 | 98 | Sent 99 | Reset 100 | 101 | 102 | 103 | 105 | 106 | 107 | 108 | 109 | 134 | 135 | 136 | 137 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | de.asw.apps.streaming 7 | screen-casting-app 8 | 2.0.0.BUILD-SNAPSHOT 9 | jar 10 | 11 | screen-casting-app 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.2.5.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | HTML 25 | 26 | 1.8 27 | 0.12.1 28 | 3.17 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | javax.xml.bind 38 | jaxb-api 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-properties-migrator 44 | runtime 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-thymeleaf 50 | 51 | 52 | 53 | org.thymeleaf.extras 54 | thymeleaf-extras-springsecurity5 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-security 60 | 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-starter-web 65 | 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-tomcat 70 | 71 | 72 | 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-starter-jetty 77 | 78 | 79 | 80 | org.springframework.boot 81 | spring-boot-starter-websocket 82 | 83 | 84 | 85 | org.springframework.boot 86 | spring-boot-starter-data-jpa 87 | 88 | 89 | 90 | org.springframework.boot 91 | spring-boot-starter-json 92 | 93 | 94 | 95 | org.apache.commons 96 | commons-lang3 97 | 3.7 98 | 99 | 100 | 101 | commons-fileupload 102 | commons-fileupload 103 | 1.4 104 | 105 | 106 | 107 | com.h2database 108 | h2 109 | 110 | 111 | 112 | com.dorkbox 113 | SystemTray 114 | ${dorkbox-systray.version} 115 | 116 | 117 | 118 | com.atlassian.commonmark 119 | commonmark 120 | ${commonmark.version} 121 | 122 | 123 | 124 | com.atlassian.commonmark 125 | commonmark-ext-autolink 126 | ${commonmark.version} 127 | 128 | 129 | 130 | org.thymeleaf.extras 131 | thymeleaf-extras-java8time 132 | 133 | 134 | 135 | com.github.mxab.thymeleaf.extras 136 | thymeleaf-extras-data-attribute 137 | 138 | 139 | 140 | org.webjars 141 | webjars-locator-core 142 | 143 | 144 | 145 | org.webjars 146 | jquery 147 | 3.3.1-1 148 | 149 | 150 | 151 | org.webjars 152 | bootstrap 153 | 3.3.7-1 154 | 155 | 156 | 157 | org.webjars.npm 158 | mustache 159 | 2.3.0 160 | 161 | 162 | 163 | org.webjars.npm 164 | moment 165 | 2.19.1 166 | 167 | 168 | 169 | org.webjars 170 | sockjs-client 171 | 1.1.2 172 | 173 | 174 | 175 | org.webjars 176 | stomp-websocket 177 | 2.3.3-1 178 | 179 | 180 | 181 | org.webjars 182 | prismjs 183 | 1.6.0 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | org.projectlombok 194 | lombok 195 | true 196 | 197 | 198 | 199 | org.springframework.boot 200 | spring-boot-configuration-processor 201 | true 202 | 203 | 204 | 205 | org.springframework.boot 206 | spring-boot-starter-test 207 | test 208 | 209 | 210 | 211 | 212 | screen-casting-app 213 | 214 | 215 | org.springframework.boot 216 | spring-boot-maven-plugin 217 | 218 | 219 | 220 | org.apache.maven.plugins 221 | maven-jar-plugin 222 | 223 | 224 | 225 | org.owasp 226 | dependency-check-maven 227 | 4.0.2 228 | 229 | 230 | true 231 | 232 | suppressed-cves.xml 233 | ${dependency-check-format} 234 | 235 | 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /src/main/resources/static/js/app.js: -------------------------------------------------------------------------------- 1 | function ScreenCaster(config) { 2 | 3 | this.enabled = config.enabled === true; 4 | this.watcher = config.watcher === true; 5 | this.showUpdates = true; 6 | this.stompClient = null; 7 | this.screenUpdateInterval = 250; 8 | this.currentPointerLocation = null; 9 | this.notificationStatus = { 10 | enabled: false 11 | }; 12 | this.headers = {}; 13 | 14 | this.init = function init() { 15 | 16 | this.headers["X-CSRF-TOKEN"] = $("meta[name=csrf]").attr("value"); 17 | 18 | this.initWebSocketConnection(); 19 | 20 | this.initNotes(); 21 | 22 | if (this.watcher) { 23 | this.initScreenCast(); 24 | return; 25 | } 26 | 27 | this.notificationStatus.enabled = false; 28 | this.setupNotesForm(); 29 | this.initClipboardSupport(); 30 | this.initDragAndDropSupport(); 31 | }; 32 | 33 | this.uploadFile = function uploadSingleFile(fileData) { 34 | 35 | console.log('uploading ' + fileData.name); 36 | this.uploadFileViaAjax({ 37 | filename: fileData.name, 38 | data: fileData.data, 39 | contentType: fileData.type 40 | }, function (fileInfo) { 41 | 42 | if (!fileInfo) { 43 | return; 44 | } 45 | 46 | if (fileInfo.contentType.indexOf("image") === 0) { 47 | this.storeNote({ 48 | text: "### " + fileInfo.name + "\n" + 49 | "" + 50 | "" 51 | + "" 52 | }); 53 | } else { 54 | this.storeNote({ 55 | text: "### File " + "" + fileInfo.name + "" 56 | }); 57 | } 58 | }.bind(this)); 59 | }.bind(this); 60 | 61 | this.initDragAndDropSupport = function initDragAndDropSupport() { 62 | 63 | function removeDragData(ev) { 64 | console.log('Removing drag data'); 65 | 66 | if (ev.dataTransfer.items) { 67 | // Use DataTransferItemList interface to remove the drag data 68 | ev.dataTransfer.items.clear(); 69 | } else { 70 | // Use DataTransfer interface to remove the drag data 71 | ev.dataTransfer.clearData(); 72 | } 73 | } 74 | 75 | var htmlElement = document.querySelector("html"); 76 | htmlElement.addEventListener("dragover", function(evt){ 77 | evt.preventDefault(); 78 | }); 79 | 80 | htmlElement.addEventListener("drop", function(evt){ 81 | evt.preventDefault(); 82 | }); 83 | 84 | var notesDialog = document.querySelector("#notesDialog"); 85 | notesDialog.addEventListener("drop", function(evt){ 86 | evt.preventDefault(); 87 | 88 | let dataTrans = evt.dataTransfer; 89 | if (dataTrans.items) { 90 | // Use DataTransferItemList interface to access the file(s) 91 | for (var i = 0; i < dataTrans.items.length; i++) { 92 | // If dropped items aren't files, reject them 93 | if (dataTrans.items[i].kind === 'file') { 94 | this.uploadFile(this.toFileData(dataTrans.items[i].getAsFile())); 95 | } 96 | } 97 | } else { 98 | // Use DataTransfer interface to access the file(s) 99 | for (var i = 0; i < dataTrans.files.length; i++) { 100 | this.uploadFile(this.toFileData(dataTrans.files[i])); 101 | } 102 | } 103 | 104 | // TODO cleanup dataTransfer 105 | // removeDragData(evt); 106 | }.bind(this)); 107 | 108 | }.bind(this); 109 | 110 | this.toFileData = function toFileData (file) { 111 | return { 112 | name: file.name, 113 | type: file.type, 114 | data: file 115 | } 116 | }.bind(this); 117 | 118 | this.initNotes = function initNotes() { 119 | this.loadNotes(); 120 | }.bind(this); 121 | 122 | this.initScreenCast = function initScreenCast() { 123 | 124 | this.$screenImage = $("#screen")[0]; 125 | this.$overlay = $("#overlay")[0]; 126 | 127 | this.initScreenVisibilityHandling(); 128 | this.initNotifications(); 129 | this.initResizeTools(); 130 | 131 | }.bind(this); 132 | 133 | this.start = function start() { 134 | 135 | this.startScreenCast(); 136 | this.startPointerAnimation(); 137 | 138 | }.bind(this); 139 | 140 | this.initWebSocketConnection = function initWebSocketConnection() { 141 | 142 | this.stompClient = Stomp.over(new SockJS("/screencaster/ws")); 143 | this.stompClient.debug = null; 144 | 145 | this.stompClient.connect({}, function (frame) { 146 | console.log('Connected: ' + frame); 147 | 148 | this.stompClient.subscribe("/topic/notes", function (noteMessage) { 149 | this.onNoteEvent(JSON.parse(noteMessage.body)); 150 | }.bind(this)); 151 | 152 | this.stompClient.subscribe("/topic/settings", function (settingsMessage) { 153 | this.onSettingsEvent(JSON.parse(settingsMessage.body)); 154 | }.bind(this)); 155 | 156 | this.stompClient.subscribe("/topic/pointer", function (pointerMessage) { 157 | this.onPointerEvent(JSON.parse(pointerMessage.body)); 158 | }.bind(this)); 159 | 160 | }.bind(this)); 161 | 162 | }.bind(this); 163 | 164 | this.onPointerEvent = function onPointerEvent(pointerEvent) { 165 | // console.log(pointerEvent); 166 | this.currentPointerLocation = pointerEvent; 167 | }.bind(this); 168 | 169 | this.onSettingsEvent = function onSettingsEvent(settingsEvent) { 170 | 171 | if (settingsEvent.type === "updated") { 172 | 173 | var enabledChanged = this.enabled !== settingsEvent.settings.castEnabled; 174 | this.enabled = settingsEvent.settings.castEnabled; 175 | 176 | if (this.enabled && enabledChanged) { 177 | this.startScreenCast(); 178 | } 179 | 180 | $("#screenCastStatus").text(this.enabled ? "active" : "not active"); 181 | } 182 | }.bind(this); 183 | 184 | this.scrollToLatestNote = function scrollToLatestNote() { 185 | var $notesListContainer = $("#notesListContainer"); 186 | $notesListContainer.animate({scrollTop: $notesListContainer.prop("scrollHeight")}, 250); 187 | }; 188 | 189 | this.onNoteEvent = function onNoteEvent(noteEvent) { 190 | 191 | if (noteEvent.type === "created") { 192 | 193 | console.log("note created", noteEvent); 194 | 195 | this.addNote(noteEvent.note); 196 | 197 | if (this.notificationStatus.enabled) { 198 | 199 | var notification = new Notification("New notes", { 200 | body: "there are new notes available", 201 | icon: "/img/new-note-icon-128x128.png", 202 | timestamp: Date.now() 203 | }); 204 | 205 | setTimeout(notification.close.bind(notification), 3000); 206 | } 207 | 208 | } else if (noteEvent.type === "deleted") { 209 | console.log("note deleted", noteEvent); 210 | $("li[data-note-id='" + noteEvent.noteId + "']").remove(); 211 | 212 | } else if (noteEvent.type === "updated") { 213 | console.log("note updated", noteEvent); 214 | 215 | var $noteContent = $("span[data-note-id='" + noteEvent.note.id + "'].note-content"); 216 | $noteContent.html(noteEvent.note.html); 217 | $noteContent.show(); 218 | 219 | var $rawContent = $("textarea[data-note-id='" + noteEvent.note.id + "'].raw-content"); 220 | $rawContent.text(noteEvent.note.text); 221 | $rawContent.hide(); 222 | } 223 | 224 | this.updateUnreadNotesCount(); 225 | }.bind(this); 226 | 227 | this.addNote = function addNote(note) { 228 | 229 | var template = $('#note-template').html(); 230 | Mustache.parse(template); // optional, speeds up future uses 231 | 232 | // hack to wrap element in span 233 | note.html = "" + note.html + ""; 234 | note.createdAtHuman = moment(note.createdAt).format("DD.MM.YY HH:mm:ss"); 235 | 236 | var renderedNote = Mustache.render(template, note).trim(); 237 | 238 | var noteElement = $("#notesList").prepend(renderedNote); 239 | 240 | // FIXME find a way to highlight only the added note 241 | Prism.highlightAll(); 242 | }; 243 | 244 | this.updateUnreadNotesCount = function updateUnreadNotesCount() { 245 | $("#unreadNotesCounter").text($(".note.new").length); 246 | }; 247 | 248 | this.onNoteAction = function onNoteAction(event) { 249 | 250 | event.preventDefault(); 251 | 252 | var button = event.currentTarget; 253 | var noteId = $(button.parentElement).data("note-id"); 254 | 255 | var $rawContent = $("textarea[data-note-id='" + noteId + "'].raw-content"); 256 | var $noteContent = $("span[data-note-id='" + noteId + "'].note-content"); 257 | 258 | if (button.value === 'delete') { 259 | 260 | $.ajax({ 261 | url: $(event.target.form).attr("action"), 262 | type: "delete", 263 | headers: this.headers 264 | }).done(function (response) { 265 | console.log("note deleted"); 266 | }); 267 | 268 | } else if (button.value === 'deleteAll') { 269 | 270 | if (!window.confirm("Delete all notes?")) { 271 | return; 272 | } 273 | 274 | $.ajax({ 275 | url: "/notes", 276 | type: "delete", 277 | headers: this.headers 278 | }).done(function (response) { 279 | console.log("all notes deleted"); 280 | 281 | $("li[data-note-id]").remove(); 282 | }); 283 | } else if (button.value === 'edit') { 284 | console.log("edit", button); 285 | 286 | $noteContent.hide(); 287 | 288 | $rawContent.data("text-backup", $rawContent.text()); 289 | $rawContent.show(); 290 | 291 | $(button.parentElement).find("button[name='actionUpdate']").show() 292 | $(button.parentElement).find("button[name='actionReset']").show() 293 | } else if (button.value === 'update') { 294 | 295 | var note = { 296 | id: noteId, 297 | text: $rawContent.val() 298 | }; 299 | 300 | this.updateNote(note); 301 | 302 | $(button.parentElement).find("button[name='actionUpdate']").hide(); 303 | $(button.parentElement).find("button[name='actionReset']").hide(); 304 | 305 | } else if (button.value === 'reset') { 306 | 307 | $rawContent.val($rawContent.data("text-backup")); 308 | $rawContent.data("text-backup", null); 309 | 310 | $(button.parentElement).find("button[name='actionUpdate']").hide(); 311 | $(button.parentElement).find("button[name='actionReset']").hide(); 312 | 313 | $noteContent.show(); 314 | $rawContent.hide(); 315 | } 316 | }.bind(this); 317 | 318 | this.markNoteAsRead = function markNoteAsRead(note) { 319 | 320 | var $note = $(note); 321 | $note.removeClass('new'); 322 | $note.addClass('read'); 323 | 324 | this.updateUnreadNotesCount(); 325 | }.bind(this); 326 | 327 | 328 | this.loadNotes = function loadNotes() { 329 | 330 | $.getJSON("/notes", function (notes) { 331 | for (var i = 0; i < notes.length; i++) { 332 | this.addNote(notes[i]); 333 | } 334 | }.bind(this)); 335 | }.bind(this); 336 | 337 | this.setupNotesForm = function setupNotesForm() { 338 | $("#notesForm").submit(function (event) { 339 | 340 | event.preventDefault(); 341 | 342 | var note = { 343 | text: $("#txtNote").val() 344 | }; 345 | 346 | this.storeNote(note); 347 | 348 | $("#notesForm")[0].reset(); 349 | }.bind(this)); 350 | 351 | $("#notesForm").keydown(function (keyEvent) { 352 | 353 | var ctrlAndEnterWasPressed = (keyEvent.ctrlKey || keyEvent.metaKey) && keyEvent.which == 13; 354 | if (ctrlAndEnterWasPressed) { 355 | 356 | $("#notesForm").submit(); 357 | $("#txtNote").focus(); 358 | 359 | event.preventDefault(); 360 | return false; 361 | } 362 | 363 | return true; 364 | }); 365 | 366 | }.bind(this); 367 | 368 | this.storeNote = function storeNote(note) { 369 | 370 | $.ajax({ 371 | url: "/notes", 372 | type: "post", 373 | data: note, 374 | headers: this.headers 375 | }).done(function (response) { // 376 | // console.log(response); 377 | }); 378 | }.bind(this); 379 | 380 | this.updateNote = function updateNote(note) { 381 | 382 | $.ajax({ 383 | url: "/notes/" + note.id, 384 | type: "put", 385 | data: note, 386 | headers: this.headers 387 | }).done(function (response) { // 388 | // console.log(response); 389 | 390 | // FIXME find a way to highlight only the added note 391 | Prism.highlightAll(); 392 | }); 393 | }.bind(this); 394 | 395 | this.initClipboardSupport = function initClipboardSupport() { 396 | 397 | function onPaste(evt) { 398 | 399 | if (!evt.clipboardData) { 400 | return; 401 | } 402 | 403 | var items = evt.clipboardData.items; 404 | if (!items) { 405 | return; 406 | } 407 | 408 | if (items.length === 0) { 409 | return; 410 | } 411 | 412 | var currentItem = items[0]; 413 | if (currentItem.type.indexOf("image") === -1) { 414 | return; 415 | } 416 | 417 | var blob = currentItem.getAsFile(); 418 | 419 | this.uploadFile({ 420 | name: "Screenshot " + moment(Date.now()).format("DD-MM-YY_HH-mm-ss"), 421 | data: blob, 422 | type: currentItem.type 423 | }); 424 | } 425 | 426 | document.addEventListener('paste', onPaste.bind(this), false); 427 | }.bind(this); 428 | 429 | this.uploadFileViaAjax = function uploadFileViaAjax(fileData, callback) { 430 | 431 | var data = new FormData(); 432 | data.append('file', fileData.data, fileData.filename); 433 | 434 | $.ajax({ 435 | url: "/files", 436 | type: "post", 437 | enctype: 'multipart/form-data', 438 | data: data, 439 | processData: false, 440 | contentType: false, 441 | headers: this.headers, 442 | success: callback 443 | }); 444 | }.bind(this); 445 | 446 | this.initScreenVisibilityHandling = function initScreenVisibilityHandling() { 447 | 448 | var hiddenAttributeName, visibilityChangeEventName; 449 | if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support 450 | hiddenAttributeName = "hidden"; 451 | visibilityChangeEventName = "visibilitychange"; 452 | } else if (typeof document.msHidden !== "undefined") { 453 | hiddenAttributeName = "msHidden"; 454 | visibilityChangeEventName = "msvisibilitychange"; 455 | } else if (typeof document.webkitHidden !== "undefined") { 456 | hiddenAttributeName = "webkitHidden"; 457 | visibilityChangeEventName = "webkitvisibilitychange"; 458 | } else { 459 | // step out, since some older browsers don't support handling of visibility changes. 460 | return; 461 | } 462 | 463 | var handler = function () { 464 | 465 | if (document[hiddenAttributeName]) { 466 | document.title = "Paused..."; 467 | console.log("stop updating"); 468 | this.showUpdates = false; 469 | } else { 470 | document.title = "Active..."; 471 | console.log("start updating"); 472 | this.showUpdates = true; 473 | 474 | this.startScreenCast(); 475 | } 476 | }.bind(this); 477 | 478 | document.addEventListener(visibilityChangeEventName, handler, false); 479 | }.bind(this); 480 | 481 | this.initNotifications = function initNotifications() { 482 | 483 | if (!("Notification" in window)) { 484 | console.log("This browser does not support system notifications"); 485 | this.notificationStatus.enabled = false; 486 | return; 487 | } 488 | 489 | if (Notification.permission === "granted") { 490 | this.notificationStatus.enabled = true; 491 | return; 492 | } 493 | 494 | if (Notification.permission !== 'denied') { 495 | Notification.requestPermission(function (permission) { 496 | if (permission === "granted") { 497 | this.notificationStatus.enabled = true; 498 | } 499 | }.bind(this)); 500 | } 501 | }.bind(this); 502 | 503 | this.initResizeTools = function initResizeTools() { 504 | 505 | // TODO fix resizing 506 | 507 | $("#screenContainer").onclick = function (evt) { 508 | $(this.$screenImage).toggleClass("screen-fit") 509 | }; 510 | }.bind(this); 511 | 512 | /** 513 | * Starts the screenshot fetching 514 | * 515 | * @type {any} 516 | */ 517 | this.startScreenCast = function startScreenCast() { 518 | 519 | if ("URLSearchParams" in window) { 520 | var urlParams = new URLSearchParams(window.location.search); 521 | if (urlParams.has("screenUpdateInterval")) { 522 | this.screenUpdateInterval = parseInt(urlParams.get("screenUpdateInterval"), 10); 523 | } 524 | } 525 | 526 | if (!this.$screenImage) { 527 | return; 528 | } 529 | 530 | this.$screenImage.onload = (function () { 531 | 532 | if (!this.enabled) { 533 | return; 534 | } 535 | 536 | if (!this.casterScreenDimensions) { 537 | 538 | // resize canvas... 539 | this.casterScreenDimensions = { 540 | w: this.$screenImage.naturalWidth, 541 | h: this.$screenImage.naturalHeight 542 | }; 543 | 544 | this.$overlay.width = this.$screenImage.width; 545 | this.$overlay.height = this.$screenImage.height; 546 | } 547 | 548 | setTimeout(refreshImage.bind(this), this.screenUpdateInterval); 549 | }).bind(this); 550 | 551 | function refreshImage() { 552 | 553 | if (!this.showUpdates) { 554 | return; 555 | } 556 | 557 | console.log("screen update"); 558 | 559 | this.$screenImage.src = "/screenshot.jpg?" + Date.now(); 560 | } 561 | 562 | setTimeout(refreshImage.bind(this), this.screenUpdateInterval); 563 | }.bind(this); 564 | 565 | /** 566 | * Renders the remote mouse pointer 567 | */ 568 | this.startPointerAnimation = function startPointerAnimation() { 569 | 570 | requestAnimationFrame(renderLoop.bind(this)); 571 | 572 | var startTime = Date.now(); 573 | var pulseDuration = 1.45; 574 | 575 | function renderLoop() { 576 | 577 | let pointerLocation = this.currentPointerLocation; 578 | let screenDimensions = this.casterScreenDimensions; 579 | 580 | if (pointerLocation && screenDimensions) { 581 | 582 | var time = (Date.now() - startTime) / 1000.0; 583 | 584 | var pulseCompletion = (time % pulseDuration) / pulseDuration; 585 | 586 | var cvs = this.$overlay; 587 | 588 | var context = cvs.getContext('2d'); 589 | context.globalAlpha = 0.95; 590 | 591 | context.clearRect(0, 0, cvs.width, cvs.height); 592 | 593 | var scalingW = cvs.width / screenDimensions.w; 594 | var scalingH = cvs.height / screenDimensions.h; 595 | 596 | var radius = 4; 597 | 598 | context.beginPath(); 599 | context.arc(pointerLocation.x * scalingW, pointerLocation.y * scalingH, radius, 0, 2 * Math.PI, false); 600 | context.fillStyle = 'magenta'; 601 | context.fill(); 602 | 603 | context.beginPath(); 604 | context.arc(pointerLocation.x * scalingW, pointerLocation.y * scalingH, radius + pulseCompletion * 10, 0, 2 * Math.PI, false); 605 | 606 | context.lineWidth = 1; 607 | context.strokeStyle = 'magenta'; 608 | context.globalAlpha = 1 - pulseCompletion; 609 | context.stroke(); 610 | } 611 | 612 | requestAnimationFrame(renderLoop.bind(this)); 613 | } 614 | } 615 | } --------------------------------------------------------------------------------
Status: active 57 |