package cz.matejsimek.scup; import java.awt.AWTException; import java.awt.Desktop; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsDevice; import java.awt.Image; import java.awt.SystemTray; import java.awt.Toolkit; import java.awt.TrayIcon; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.URI; import java.net.URL; import java.security.MessageDigest; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Formatter; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.prefs.Preferences; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import javax.swing.JCheckBoxMenuItem; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.UIManager; import org.imgscalr.Scalr; /** * Scup - Simple screenshot & file uploader <p>Easily upload screenshot or files * to FTP server and copy its URL address to clipboard. * * @author Matej Simek | www.matejsimek.cz */ public class Scup { /** * Simple new version checking with incremental number */ final static int VERSION = 5; // public static Clipboard clipboard; public static JXTrayIcon trayIcon; private static Preferences prefs; /** * 16x16 app icon */ public static BufferedImage iconImage = null, iconImageUpload = null; /** * User configuration keys */ public final static String KEY_FTP_SERVER = "FTP_SERVER"; public final static String KEY_FTP_USERNAME = "FTP_USERNAME"; public final static String KEY_FTP_PASSWORD = "FTP_PASSWORD"; public final static String KEY_DIRECTORY = "FTP_DIRECTORY"; public final static String KEY_URL = "URL"; public final static String KEY_UPLOAD = "UPLOAD"; public final static String KEY_UPLOAD_METHOD = "UPLOAD_METHOD"; public final static String KEY_MONITOR_ALL = "MONITOR_ALL"; public final static String KEY_INITIAL_SETTINGS = "INITIAL_SETTINGS"; public final static String KEY_DROPBOX_KEY = "DROPBOX_KEY"; public final static String KEY_DROPBOX_SECRET = "DROPBOX_SECRET"; /** * FTP configuration variables */ private static String FTP_SERVER, FTP_USERNAME, FTP_PASSWORD, FTP_DIRECTORY, URL, UPLOAD_METHOD, DROPBOX_KEY, DROPBOX_SECRET; /** * Flag which enable upload to FTP server */ public static boolean UPLOAD; /** * Flag which enable capture images from all sources, not only printscreen */ public static boolean MONITOR_ALL; /** * Flag indicates initial settings */ private static boolean INITIAL_SETTINGS; /** * Tray Popup menu items */ private static JCheckBoxMenuItem uploadEnabledCheckBox; private static JCheckBoxMenuItem monitorAllCheckBox; private static JMenu historySubmenu; private static ActionListener trayIconActionListener = null; /** * Startup initialization, then endless Thread sleep * * @param args not used yet * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException { // Set system windows theme and load default icon try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); iconImage = ImageIO.read(Scup.class.getResource("resources/icon.png")); iconImageUpload = ImageIO.read(Scup.class.getResource("resources/iconupload.png")); } catch (Exception ex) { ex.printStackTrace(); } // Read configuration prefs = Preferences.userNodeForPackage(cz.matejsimek.scup.Scup.class); readConfiguration(); // Init tray icon initTray(); // Get system clipboard and asign event handler to it clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); ClipboardChangeListener cl = new ClipboardChangeListener(clipboard); cl.start(); // Force users to download new version checkForUpdates(); // Show configuration form on startup until first save if (INITIAL_SETTINGS) { new SettingsForm().setVisible(true); } // Endless program run, events are handled in EDT thread while (true) { Thread.sleep(Long.MAX_VALUE); } } /** * Simple new version checking with incremental number under on url */ static private void checkForUpdates() { System.out.println("Checking for updates..."); try { final URI projectURL = new URI("https://github.com/enzy/Scup"); final URL url = new URL("https://raw.github.com/enzy/Scup/master/version.txt"); BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); String inputLine = in.readLine(); if (inputLine != null) { int remoteVersion = Integer.parseInt(inputLine); System.out.println("Found version " + remoteVersion + ", your is " + VERSION); if (remoteVersion > VERSION) { System.out.println("New version available! Get it at: " + projectURL); // Create option dialog JOptionPane pane = new JOptionPane("New version is available. Download it now?", JOptionPane.PLAIN_MESSAGE, JOptionPane.YES_NO_OPTION); JDialog dialog = pane.createDialog(new JFrame(), "Scup - Update check"); dialog.setIconImage(iconImage); dialog.setAlwaysOnTop(true); dialog.setVisible(true); // Get user choice Object obj = pane.getValue(); // Open browser on project page on yes and close program if (obj != null && !obj.equals(JOptionPane.UNINITIALIZED_VALUE) && (Integer) obj == 0) { if (openBrowserOn(projectURL)) { System.exit(0); } } } else { System.out.println("You have the latest version."); } } in.close(); } catch (Exception ex) { System.err.println("Check for updates failed."); ex.printStackTrace(); } } /** * Open default system browser on given URI * * @param uri * @return true if operation was successful */ static public boolean openBrowserOn(URI uri) { if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { try { Desktop.getDesktop().browse(uri); return true; } catch (Exception ex) { System.err.println("Error while opening browser"); ex.printStackTrace(); } } return false; } /** * Open given file with default associated program * * @param filepath * @return */ static public boolean openOnFile(String filepath) { if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { try { Desktop.getDesktop().open(new File(filepath)); return true; } catch (Exception ex) { System.err.println("Error while opening file with associated program"); ex.printStackTrace(); } } return false; } /** * Sets tray icon tooltip text, sets default if text is empty * * @param text */ static void setTrayTooltip(String text) { if (text.isEmpty()) { trayIcon.setToolTip("Scup v" + VERSION); } else { trayIcon.setToolTip(text); } } /** * Place app icon into system tray, build popupmenu and attach event handlers * to items */ static private void initTray() { if (SystemTray.isSupported()) { final SystemTray tray = SystemTray.getSystemTray(); // Different trayicon sizes, prefer downscalling String icoVersion; int icoWidth = tray.getTrayIconSize().width; if (icoWidth <= 16) { icoVersion = ""; } else if (icoWidth <= 24) { icoVersion = "24"; } else if (icoWidth <= 32) { icoVersion = "32"; } else if (icoWidth <= 48) { icoVersion = "48"; } else if (icoWidth <= 64) { icoVersion = "64"; } else if (icoWidth <= 96) { icoVersion = "96"; } else if (icoWidth <= 128) { icoVersion = "128"; } else if (icoWidth <= 256) { icoVersion = "256"; } else { icoVersion = "512"; } // Load tray icon try { trayIcon = new JXTrayIcon(ImageIO.read(Scup.class.getResource("resources/icon" + icoVersion + ".png"))); setTrayTooltip(""); trayIcon.setImageAutoSize(true); } catch (IOException ex) { System.err.println("IOException: TrayIcon could not be added."); System.exit(1); } // Add tray icon to system tray try { tray.add(trayIcon); } catch (AWTException e) { System.err.println("AWTException: TrayIcon could not be added."); System.exit(1); } // Build popup menu showed on trayicon right click (on Windows) final JPopupMenu jpopup = new JPopupMenu(); JMenuItem settingsItem = new JMenuItem("Settings..."); uploadEnabledCheckBox = new JCheckBoxMenuItem("Upload to server"); monitorAllCheckBox = new JCheckBoxMenuItem("Monitor all"); historySubmenu = new JMenu("History"); JMenuItem exitItem = new JMenuItem("Exit"); jpopup.add(settingsItem); jpopup.add(uploadEnabledCheckBox); jpopup.add(monitorAllCheckBox); jpopup.addSeparator(); jpopup.add(historySubmenu); jpopup.addSeparator(); jpopup.add(exitItem); // Add popup to tray trayIcon.setJPopupMenu(jpopup); // Set default flags uploadEnabledCheckBox.setState(UPLOAD); monitorAllCheckBox.setState(MONITOR_ALL); historySubmenu.setEnabled(false); // Add listener to settingsItem. settingsItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { new SettingsForm().setVisible(true); } }); // Add listener to uploadEnabledCheckBox uploadEnabledCheckBox.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { int chxbx = e.getStateChange(); if (chxbx == ItemEvent.SELECTED) { UPLOAD = true; } else { UPLOAD = false; } prefs.putBoolean(KEY_UPLOAD, UPLOAD); } }); // Add listener to monitorAllCheckBox monitorAllCheckBox.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { int chxbx = e.getStateChange(); if (chxbx == ItemEvent.SELECTED) { MONITOR_ALL = true; } else { MONITOR_ALL = false; } prefs.putBoolean(KEY_MONITOR_ALL, MONITOR_ALL); } }); // Add listener to exitItem. exitItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { tray.remove(trayIcon); System.exit(0); } }); trayIcon.displayMessage("Scup", "I am here to serve", TrayIcon.MessageType.NONE); } else { System.err.println("SystemTray is not supported"); } } /** * Fills class varibles: * <code>FTP_SERVER, FTP_USERNAME, FTP_PASSWORD, * FTP_DIRECTORY, URL, UPLOAD, MONITOR_ALL</code> * * @param filename to read configuration from */ static private void readConfiguration(/*String filename*/) { // Load config FTP_SERVER = prefs.get(KEY_FTP_SERVER, "localhost"); FTP_USERNAME = prefs.get(KEY_FTP_USERNAME, "anonymous"); FTP_PASSWORD = prefs.get(KEY_FTP_PASSWORD, ""); FTP_DIRECTORY = prefs.get(KEY_DIRECTORY, ""); URL = prefs.get(KEY_URL, "http://localhost"); UPLOAD = prefs.getBoolean(KEY_UPLOAD, false); UPLOAD_METHOD = prefs.get(KEY_UPLOAD_METHOD, "FTP"); MONITOR_ALL = prefs.getBoolean(KEY_MONITOR_ALL, true); INITIAL_SETTINGS = prefs.getBoolean(KEY_INITIAL_SETTINGS, true); DROPBOX_KEY = prefs.get(KEY_DROPBOX_KEY, ""); DROPBOX_SECRET = prefs.get(KEY_DROPBOX_SECRET, ""); } public static void reloadConfiguration() { readConfiguration(); uploadEnabledCheckBox.setState(UPLOAD); monitorAllCheckBox.setState(MONITOR_ALL); } /** * Handle uploading of any given file to chosen service (FTP, DROPBOX) * * @param file file to upload * @param remoteFilename remote filename * @return URL of uploaded file */ static String uploadFile(File file, String remoteFilename) { // Change tray icon image Image oldIcon = trayIcon.getImage(); trayIcon.setImage(iconImageUpload); // Inform about upload setTrayTooltip("Uploading " + remoteFilename + " to " + UPLOAD_METHOD); System.out.println("Uploading file to " + UPLOAD_METHOD + " server..."); String url = null; if ("FTP".equals(UPLOAD_METHOD)) { FileUpload fileupload = new FileUpload(FTP_SERVER, FTP_USERNAME, FTP_PASSWORD, FTP_DIRECTORY); boolean status = fileupload.uploadFile(file, remoteFilename); if (status) { url = (URL.endsWith("/") ? URL : URL + "/") + remoteFilename; } } else if ("DROPBOX".equals(UPLOAD_METHOD)) { try { DropboxUpload du = new DropboxUpload(DROPBOX_KEY, DROPBOX_SECRET); url = du.uploadFile(file, remoteFilename); // Save authentication tokens for next time DROPBOX_KEY = du.getKey(); DROPBOX_SECRET = du.getSecret(); prefs.put(KEY_DROPBOX_KEY, DROPBOX_KEY); prefs.put(KEY_DROPBOX_SECRET, DROPBOX_SECRET); } catch (Exception ex) { trayIcon.displayMessage("Dropbox error", ex.getLocalizedMessage(), TrayIcon.MessageType.ERROR); ex.printStackTrace(); } } // Revert back tray changes trayIcon.setImage(oldIcon); oldIcon.flush(); setTrayTooltip(""); // Return URL or null return url; } /** * Whole image handling process - display, crop, save on disk, transfer to * FTP, copy its URL to clipboard * * @param image to process * @param cropImage should be image cropped? * @param device to display image on */ static void processImage(BufferedImage image, boolean cropImage, GraphicsDevice device) { System.out.println("Processing image..."); System.out.println("Image: " + image.getWidth() + "x" + image.getHeight()); if (cropImage) { image = cropImage(image, device); } if (image == null) { System.out.println("Image is empty, canceling"); return; } File imageFile = saveImageToFile(image); final String imageUrl; if (UPLOAD) { // Calculate image hash String hash = generateHashForFile(imageFile); String newFilename = hash.substring(0, 10) + ".png"; // Rename file after its hash File renamedFile = new File(newFilename); imageFile = imageFile.renameTo(renamedFile) ? renamedFile : imageFile; // Transer image to FTP imageUrl = uploadFile(imageFile, imageFile.getName()); if (imageUrl == null) { // Upload failed, it happens trayIcon.displayMessage("Upload failed", "I can not serve, sorry", TrayIcon.MessageType.ERROR); System.err.println("Upload failed"); return; } else { // Don't keep copy of already uploaded image imageFile.delete(); } } // Copy local absolute path only when upload is disabled else { imageUrl = imageFile.getAbsolutePath(); } // Copy URL to clipboard setClipboard(imageUrl); // Notify user about it trayIcon.displayMessage("Image " + (UPLOAD ? "uploaded" : "saved"), imageUrl, TrayIcon.MessageType.INFO); System.out.println("Image " + (UPLOAD ? "uploaded " : "saved ") + imageUrl); // Display last uploaded image switchTrayIconActionListenerTo(new ActionListener() { public void actionPerformed(ActionEvent e) { setClipboard(imageUrl); trayIcon.displayMessage("Last image " + (UPLOAD ? "URL" : "path"), imageUrl, TrayIcon.MessageType.INFO); System.out.println("Last image " + (UPLOAD ? "URL" : "path") + imageUrl); } }); // Save it to history addImageToHistory(image, imageUrl, !UPLOAD); } /** * Adds image into history submenu * * @param image * @param path */ static private void addImageToHistory(BufferedImage image, final String path, boolean isLocalFile) { BufferedImage scaled; // Resize image to usable dimensions if (image.getWidth() > 140 || image.getHeight() > 80) { scaled = Scalr.resize(image, 140, 80); } else { scaled = image; } // Rape JMenuItem with big image JMenuItem item = new JMenuItem(path, new ImageIcon(scaled)); // Copy path on click item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Scup.setClipboard(path); } }); // Open browser/program on CTRL+click item.addMouseListener(new OpenFileMouseAdapter(path, isLocalFile)); // Finally add item to submenu historySubmenu.add(item); historySubmenu.setEnabled(true); // Clean old items if (historySubmenu.getItemCount() > 5) { historySubmenu.remove(0); } scaled.flush(); } /** * Copy given String to system clipboard (could fail) * * @param str string to copy */ static void setClipboard(String str) { try { clipboard.setContents(new StringSelection(str), null); } catch (IllegalStateException ex) { System.err.println("Can't set clipboard, trying again!"); ex.printStackTrace(); try { Thread.sleep(250); } catch (InterruptedException ex1) { ex1.printStackTrace(); } setClipboard(str); } } /** * Assure there is only one action listener on tratIcon * * @param newListener to switch */ static void switchTrayIconActionListenerTo(ActionListener newListener) { if (trayIconActionListener != null) { trayIcon.removeActionListener(trayIconActionListener); } trayIconActionListener = newListener; trayIcon.addActionListener(trayIconActionListener); } /** * Display image full screen and crop it by user celection * * @param image to crop * @param device to display image on * @return cropped image or null in case of crop cancel */ static BufferedImage cropImage(BufferedImage image, GraphicsDevice device) { CountDownLatch framerun = new CountDownLatch(1); FullscreenFrame fullscreenFrame = new FullscreenFrame(framerun, image); fullscreenFrame.setVisible(true); if (device.isFullScreenSupported()) { device.setFullScreenWindow(fullscreenFrame); } else { System.err.println("FullScreen is not supported"); } try { framerun.await(); } catch (InterruptedException ex) { ex.printStackTrace(); } // When its closed, get cropped image if (fullscreenFrame.isImageCropped()) { image.flush(); image = fullscreenFrame.getCroppedImage(); } else { image.flush(); image = null; } fullscreenFrame.setVisible(false); fullscreenFrame.dispose(); fullscreenFrame = null; return image; } /** * File handling process - upload single file in original format or multiple * files in one zip archive * <pre> * There is 4 situations, which can occur: * - single file, no upload - only absolute path to file is copied to clipboard * - single file, upload - file is uploaded to FTP server and named after its SHA hash, URL copied to clipboard * - multiple files, no upload - files are compressed into single archive named by actual date and located in running directory, absolute path copied to clipboard * - multiple files, upload - files are compressed into single archive and then uploaded to FTP server, file named by its SHA hash, URL copied to clipboard * </pre> * * @param files List of files to process */ static void processFiles(List<File> files) { System.out.println("Processing files..."); File fileToProcess; String extension; boolean isZip = false; // Compress files into archive if (files.size() > 1) { fileToProcess = zipFiles(files); extension = ".zip"; isZip = true; } // Keep single file in original format else { fileToProcess = files.get(0); String filename = fileToProcess.getName(); System.out.println("Processing file: " + filename); // Determinate file extension int pos = filename.lastIndexOf("."); extension = (pos >= 0) ? filename.substring(pos) : ""; } final String fileUrl; if (UPLOAD) { String hash = generateHashForFile(fileToProcess); String newFilename = hash.substring(0, 10) + extension; // Transer file to FTP fileUrl = uploadFile(fileToProcess, newFilename); if (fileUrl == null) { // Upload failed, it happens trayIcon.displayMessage("Upload failed", "I can not serve, sorry", TrayIcon.MessageType.ERROR); System.err.println("Upload failed"); return; } else if (isZip) { // Upload succeeded, now clean after myself fileToProcess.delete(); } } // Copy local absolute path only when upload is disabled else { fileUrl = fileToProcess.getAbsolutePath(); } // Copy URL to clipboard setClipboard(fileUrl); // Notify user about it trayIcon.displayMessage("File " + (UPLOAD ? "uploaded" : "located"), fileUrl, TrayIcon.MessageType.INFO); System.out.println("File " + (UPLOAD ? "uploaded " : "located ") + fileUrl); // Display last processed file switchTrayIconActionListenerTo(new ActionListener() { public void actionPerformed(ActionEvent e) { setClipboard(fileUrl); trayIcon.displayMessage("Last file " + (UPLOAD ? "URL" : "path"), fileUrl, TrayIcon.MessageType.INFO); System.out.println("Last file " + (UPLOAD ? "URL" : "path") + fileUrl); } }); // Save it to history BufferedImage ico = new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR); try { ico = ImageIO.read(Scup.class.getResource("resources/" + (isZip ? "package" : "page") + ".png")); } catch (Exception ex) { ex.printStackTrace(); } addImageToHistory(ico, fileUrl, !UPLOAD); } /** * Zip given files into one archive * * @param files Input files to compress * @return Zip file named in format yyyyMMdd-HHmmss.zip */ static File zipFiles(List<File> files) { DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss"); File outputFile = new File(dateFormat.format(Calendar.getInstance().getTime()) + ".zip"); try { FileOutputStream fos = new FileOutputStream(outputFile); ZipOutputStream zos = new ZipOutputStream(fos); byte[] buf = new byte[1024]; for (File file : files) { System.out.println("Compressing file: " + file.getName()); FileInputStream fis = null; try { fis = new FileInputStream(file); zos.putNextEntry(new ZipEntry(file.getName())); int len; while ((len = fis.read(buf)) > 0) { zos.write(buf, 0, len); } zos.closeEntry(); } catch (IOException ex) { ex.printStackTrace(); } fis.close(); } zos.close(); fos.close(); } catch (IOException ex) { ex.printStackTrace(); } return outputFile; } /** * Save image to PNG file named by its content hash into current directory * * @param img * @return */ static File saveImageToFile(BufferedImage img) { try { // Generate default image name DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss"); File outputFile = new File(dateFormat.format(Calendar.getInstance().getTime()) + ".png"); // Write image data ImageIO.write(img, "png", outputFile); return outputFile; } catch (IOException ex) { System.err.println("Can't write image to file!"); } return null; } /** * Generate SHA hash for given file * * @param file to calculate * @return Hash in hexadecimal format */ static String generateHashForFile(File file) { FileInputStream fis = null; try { byte[] buf = new byte[1024]; MessageDigest md = MessageDigest.getInstance("SHA"); fis = new FileInputStream(file); int len; while ((len = fis.read(buf)) > 0) { md.update(buf, 0, len); } Formatter formatter = new Formatter(); for (byte b : md.digest()) { formatter.format("%02x", b); } fis.close(); return formatter.toString(); } catch (Exception ex) { ex.printStackTrace(); } finally { try { fis.close(); } catch (IOException ex) { ex.printStackTrace(); } } return null; } }