diff --git a/pom.xml b/pom.xml index bdf66c9..8e14f19 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ + org.openjfx javafx-maven-plugin @@ -24,6 +25,34 @@ com.rdesk.Main + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.3 + + + + package + + shade + + + + + false + + + + com.rdesk.Main + + + + + + + + diff --git a/src/main/java/com/rdesk/Main.java b/src/main/java/com/rdesk/Main.java index 513f8ea..cd16d7d 100644 --- a/src/main/java/com/rdesk/Main.java +++ b/src/main/java/com/rdesk/Main.java @@ -1,5 +1,6 @@ package com.rdesk; +import com.rdesk.applications.entertainment.comic.Xkcd; import com.rdesk.applications.productivity.Calculator; import com.rdesk.applications.productivity.browsing.TabbedBrowser; import com.rdesk.applications.productivity.code.CodeDesk; @@ -88,6 +89,7 @@ public class Main { launcher.addApp(new TabbedBrowser(), "Productivity"); launcher.addApp(new CodeDesk(), "Productivity"); launcher.addApp(new Calculator(), "Productivity"); + launcher.addApp(new Xkcd(), "Entertainment"); launcher.addApp(new Exit(), "System"); launcher.addApp(new TaskManager(), "System"); launcher.addApp(new FileExplorerApp(), "System"); diff --git a/src/main/java/com/rdesk/applications/entertainment/comic/ComicData.java b/src/main/java/com/rdesk/applications/entertainment/comic/ComicData.java new file mode 100644 index 0000000..9e4a22d --- /dev/null +++ b/src/main/java/com/rdesk/applications/entertainment/comic/ComicData.java @@ -0,0 +1,27 @@ +package com.rdesk.applications.entertainment.comic; + +public class ComicData { + private int num; + private String title; + private String img; + private String alt; + + // Gson requires default constructor + public ComicData() {} + + public int getNum() { + return num; + } + + public String getTitle() { + return title; + } + + public String getImg() { + return img; + } + + public String getAlt() { + return alt; + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/entertainment/comic/ComicFetcher.java b/src/main/java/com/rdesk/applications/entertainment/comic/ComicFetcher.java new file mode 100644 index 0000000..ac0d607 --- /dev/null +++ b/src/main/java/com/rdesk/applications/entertainment/comic/ComicFetcher.java @@ -0,0 +1,58 @@ +package com.rdesk.applications.entertainment.comic; + +import com.google.gson.Gson; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class ComicFetcher { + private static final String BASE_URL = "https://xkcd.com/"; + private static final String INFO_URL = BASE_URL + "info.0.json"; + private static final String COMIC_URL_TEMPLATE = BASE_URL + "%d/info.0.json"; + + private final HttpClient httpClient; + private final Gson gson; + + public ComicFetcher() { + this.httpClient = HttpClient.newHttpClient(); + this.gson = new Gson(); + } + + public int fetchLatestComicNumber() throws IOException, InterruptedException { + String json = fetchJson(INFO_URL); + LatestInfo info = gson.fromJson(json, LatestInfo.class); + if (info == null || info.num == 0) { + throw new IOException("api response error"); + } + return info.num; + } + + public ComicData fetchComicData(int num) throws IOException, InterruptedException { + String url = String.format(COMIC_URL_TEMPLATE, num); + String json = fetchJson(url); + ComicData comic = gson.fromJson(json, ComicData.class); + if (comic == null || comic.getImg() == null) { + throw new IOException("error , idk bro #" + num); + } + return comic; + } + + private String fetchJson(String urlString) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(urlString)) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode() + " for " + urlString); + } + return response.body(); + } + + private static class LatestInfo { + int num; + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/entertainment/comic/Xkcd.java b/src/main/java/com/rdesk/applications/entertainment/comic/Xkcd.java new file mode 100644 index 0000000..19224e7 --- /dev/null +++ b/src/main/java/com/rdesk/applications/entertainment/comic/Xkcd.java @@ -0,0 +1,54 @@ +package com.rdesk.applications.entertainment.comic; + +import com.rdesk.kernel.DesktopApp; +import com.rdesk.kernel.Kernel; + +import javax.swing.*; +import java.awt.*; + +public class Xkcd implements DesktopApp { + + @Override + public String getId() { + return "xkcd"; + } + + @Override + public String getName() { + return "XKCD"; + } + + @Override + public JInternalFrame createWindow(Kernel kernel) { + JInternalFrame frame = new JInternalFrame( + "XKCD", + true, true, true, true + ); + frame.setSize(800, 700); + frame.setLayout(new BorderLayout(5, 5)); + + JPanel topPanel = new JPanel(new BorderLayout(5, 0)); + JButton refreshButton = new JButton("Random Comic"); + JLabel titleLabel = new JLabel(" ", SwingConstants.CENTER); + titleLabel.setFont(new Font("SansSerif", Font.BOLD, 14)); + topPanel.add(refreshButton, BorderLayout.WEST); + topPanel.add(titleLabel, BorderLayout.CENTER); + frame.add(topPanel, BorderLayout.NORTH); + + ZoomableImagePanel imagePanel = new ZoomableImagePanel(); + frame.add(imagePanel, BorderLayout.CENTER); + + JTextArea altTextArea = new JTextArea(4, 40); + altTextArea.setEditable(false); + altTextArea.setLineWrap(true); + altTextArea.setWrapStyleWord(true); + altTextArea.setFont(new Font("SansSerif", Font.PLAIN, 12)); + JScrollPane altScrollPane = new JScrollPane(altTextArea); + frame.add(altScrollPane, BorderLayout.SOUTH); + + new XkcdController(refreshButton, titleLabel, altTextArea, imagePanel).loadRandomComic(); + + frame.setVisible(true); + return frame; + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/entertainment/comic/XkcdController.java b/src/main/java/com/rdesk/applications/entertainment/comic/XkcdController.java new file mode 100644 index 0000000..8cc2949 --- /dev/null +++ b/src/main/java/com/rdesk/applications/entertainment/comic/XkcdController.java @@ -0,0 +1,97 @@ +package com.rdesk.applications.entertainment.comic; + +import javax.swing.*; +import java.awt.image.BufferedImage; +import java.net.URL; +import java.util.concurrent.ExecutionException; + +public class XkcdController { + private final JButton refreshButton; + private final JLabel titleLabel; + private final JTextArea altTextArea; + private final ZoomableImagePanel imagePanel; + private final ComicFetcher fetcher; + + public XkcdController(JButton refreshButton, JLabel titleLabel, + JTextArea altTextArea, ZoomableImagePanel imagePanel) { + this.refreshButton = refreshButton; + this.titleLabel = titleLabel; + this.altTextArea = altTextArea; + this.imagePanel = imagePanel; + this.fetcher = new ComicFetcher(); + + refreshButton.addActionListener(e -> loadRandomComic()); + } + + public void loadRandomComic() { + setLoadingState(true); + SwingWorker worker = new SwingWorker<>() { + @Override + protected ComicData doInBackground() throws Exception { + int latest = fetcher.fetchLatestComicNumber(); + int randomNum = (int) (Math.random() * latest) + 1; + return fetcher.fetchComicData(randomNum); + } + + @Override + protected void done() { + setLoadingState(false); + try { + ComicData comic = get(); + if (comic != null) { + displayComic(comic); + } else { + showError("cant load comic"); + } + } catch (InterruptedException | ExecutionException e) { + showError("Error: " + e.getCause().getMessage()); + } + } + }; + worker.execute(); + } + + private void displayComic(ComicData comic) { + titleLabel.setText(String.format("#%d: %s", comic.getNum(), comic.getTitle())); + altTextArea.setText(comic.getAlt()); + + SwingWorker imageWorker = new SwingWorker<>() { + @Override + protected BufferedImage doInBackground() throws Exception { + URL url = new URL(comic.getImg()); + return javax.imageio.ImageIO.read(url); + } + + @Override + protected void done() { + try { + BufferedImage img = get(); + imagePanel.setImage(img); + } catch (Exception e) { + showError("failed to load image: " + e.getMessage()); + imagePanel.clearImage(); + } + } + }; + imageWorker.execute(); + } + + private void setLoadingState(boolean loading) { + SwingUtilities.invokeLater(() -> { + refreshButton.setEnabled(!loading); + if (loading) { + titleLabel.setText("fetching image"); + altTextArea.setText(""); + imagePanel.clearImage(); + } + }); + } + + private void showError(String message) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE); + titleLabel.setText(" "); + altTextArea.setText(""); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/entertainment/comic/ZoomableImagePanel.java b/src/main/java/com/rdesk/applications/entertainment/comic/ZoomableImagePanel.java new file mode 100644 index 0000000..34ac368 --- /dev/null +++ b/src/main/java/com/rdesk/applications/entertainment/comic/ZoomableImagePanel.java @@ -0,0 +1,136 @@ +package com.rdesk.applications.entertainment.comic; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.image.BufferedImage; + +public class ZoomableImagePanel extends JPanel { + private BufferedImage originalImage; + private double zoom = 1.0; + private Point dragStart; + private Point viewportStart; + private final JViewport viewport; + + private static final double ZOOM_STEP = 0.1; + private static final double MIN_ZOOM = 0.2; + private static final double MAX_ZOOM = 3.0; + + public ZoomableImagePanel() { + setLayout(new BorderLayout()); + JLabel imageLabel = new JLabel("", SwingConstants.CENTER); + JScrollPane scrollPane = new JScrollPane(imageLabel); + add(scrollPane, BorderLayout.CENTER); + this.viewport = scrollPane.getViewport(); + + imageLabel.addMouseWheelListener(this::onMouseWheel); + + DragHandler dragHandler = new DragHandler(imageLabel); + imageLabel.addMouseListener(dragHandler); + imageLabel.addMouseMotionListener(dragHandler); + } + + public void setImage(BufferedImage image) { + this.originalImage = image; + this.zoom = 1.0; + updateDisplay(); + SwingUtilities.invokeLater(() -> viewport.setViewPosition(new Point(0, 0))); + } + + public void clearImage() { + this.originalImage = null; + removeImageFromLabel(); + } + + private void updateDisplay() { + if (originalImage == null) { + removeImageFromLabel(); + return; + } + JLabel label = getImageLabel(); + int newWidth = (int) (originalImage.getWidth() * zoom); + int newHeight = (int) (originalImage.getHeight() * zoom); + Image scaled = originalImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH); + label.setIcon(new ImageIcon(scaled)); + label.setText(""); + label.setPreferredSize(new Dimension(newWidth, newHeight)); + revalidate(); + repaint(); + } + + private void onMouseWheel(MouseWheelEvent e) { + if (originalImage == null) return; + double oldZoom = zoom; + double newZoom = zoom + (e.getWheelRotation() < 0 ? ZOOM_STEP : -ZOOM_STEP); + newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom)); + if (newZoom == zoom) return; + + Point mouseOnScreen = e.getPoint(); + Point viewPos = viewport.getViewPosition(); + double xRatio = (viewPos.x + mouseOnScreen.x) / (double) viewport.getExtentSize().width; + double yRatio = (viewPos.y + mouseOnScreen.y) / (double) viewport.getExtentSize().height; + + zoom = newZoom; + updateDisplay(); + + Dimension contentSize = getImageLabel().getPreferredSize(); + Dimension viewSize = viewport.getExtentSize(); + int newX = (int) (contentSize.width * xRatio - viewSize.width / 2.0); + int newY = (int) (contentSize.height * yRatio - viewSize.height / 2.0); + newX = Math.max(0, Math.min(newX, contentSize.width - viewSize.width)); + newY = Math.max(0, Math.min(newY, contentSize.height - viewSize.height)); + viewport.setViewPosition(new Point(newX, newY)); + } + + private JLabel getImageLabel() { + return (JLabel) ((JScrollPane) getComponent(0)).getViewport().getView(); + } + + private void removeImageFromLabel() { + JLabel label = getImageLabel(); + label.setIcon(null); + label.setText("Loading"); + label.setPreferredSize(null); + revalidate(); + } + + private class DragHandler extends MouseAdapter { + private final JLabel label; + + DragHandler(JLabel label) { + this.label = label; + } + + @Override + public void mousePressed(MouseEvent e) { + if (!SwingUtilities.isLeftMouseButton(e)) return; + dragStart = e.getPoint(); + viewportStart = viewport.getViewPosition(); + label.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + } + + @Override + public void mouseReleased(MouseEvent e) { + if (!SwingUtilities.isLeftMouseButton(e)) return; + dragStart = null; + viewportStart = null; + label.setCursor(Cursor.getDefaultCursor()); + } + + @Override + public void mouseDragged(MouseEvent e) { + if (dragStart == null || viewportStart == null) return; + int deltaX = dragStart.x - e.getX(); + int deltaY = dragStart.y - e.getY(); + Point newPos = new Point(viewportStart.x + deltaX, viewportStart.y + deltaY); + + Dimension contentSize = label.getPreferredSize(); + Dimension viewSize = viewport.getExtentSize(); + newPos.x = Math.max(0, Math.min(newPos.x, contentSize.width - viewSize.width)); + newPos.y = Math.max(0, Math.min(newPos.y, contentSize.height - viewSize.height)); + viewport.setViewPosition(newPos); + } + } +}