From 5eeffde3c3f71c213ff9f9cbfc497afdadbad7d0 Mon Sep 17 00:00:00 2001 From: Gurkengewuerz Date: Sat, 28 Mar 2020 05:17:50 +0100 Subject: [PATCH] =?UTF-8?q?w=C3=B6rk=20w=C3=B6rk=20(day=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added License file * added changelog feature * added zsync user-agent * added application and taskbar icon * added taskbar progressbar * added social buttons * added about page with disclaimer, copyright and library license * changed gui design alot * fixed some gui errors * fixed generateRepo.sh skip empty files --- LICENSE | 21 + linux/.sync/changelog.txt | 1 + linux/generateRepo.sh | 14 + pom.xml | 11 +- .../mc8051/arma3launcher/ArmA3Launcher.java | 37 +- .../de/mc8051/arma3launcher/LauncherGUI.form | 2107 ++++++++++------- .../de/mc8051/arma3launcher/LauncherGUI.java | 351 ++- .../arma3launcher/objects/Changelog.java | 29 + .../mc8051/arma3launcher/objects/ModFile.java | 4 + .../arma3launcher/repo/FileChecker.java | 7 + .../arma3launcher/repo/RepositoryManger.java | 27 +- .../de/mc8051/arma3launcher/repo/Syncer.java | 74 +- .../mc8051/arma3launcher/utils/Callback.java | 8 +- .../mc8051/arma3launcher/utils/Humanize.java | 2 +- .../arma3launcher/utils/ImageUtils.java | 21 + .../arma3launcher/utils/TaskBarUtils.java | 143 ++ src/main/resources/arma3launcher.json | 5 + src/main/resources/disclaimer.html | 189 ++ src/main/resources/icons/changelog_16.png | Bin 0 -> 536 bytes src/main/resources/icons/download_16.png | Bin 0 -> 476 bytes src/main/resources/icons/github_32.png | Bin 0 -> 1571 bytes src/main/resources/icons/logo_256.png | Bin 0 -> 11651 bytes src/main/resources/icons/logo_32.png | Bin 0 -> 4691 bytes src/main/resources/icons/play_16.png | Bin 0 -> 511 bytes src/main/resources/icons/preset_16.png | Bin 0 -> 237 bytes src/main/resources/icons/settings_16.png | Bin 0 -> 566 bytes src/main/resources/icons/twitter_32.png | Bin 0 -> 912 bytes src/main/resources/lang_de_DE.properties | 11 +- src/main/resources/lang_en_US.properties | 11 +- 29 files changed, 2020 insertions(+), 1053 deletions(-) create mode 100644 LICENSE create mode 100644 linux/.sync/changelog.txt create mode 100644 src/main/java/de/mc8051/arma3launcher/objects/Changelog.java create mode 100644 src/main/java/de/mc8051/arma3launcher/utils/ImageUtils.java create mode 100644 src/main/java/de/mc8051/arma3launcher/utils/TaskBarUtils.java create mode 100644 src/main/resources/disclaimer.html create mode 100644 src/main/resources/icons/changelog_16.png create mode 100644 src/main/resources/icons/download_16.png create mode 100644 src/main/resources/icons/github_32.png create mode 100644 src/main/resources/icons/logo_256.png create mode 100644 src/main/resources/icons/logo_32.png create mode 100644 src/main/resources/icons/play_16.png create mode 100644 src/main/resources/icons/preset_16.png create mode 100644 src/main/resources/icons/settings_16.png create mode 100644 src/main/resources/icons/twitter_32.png diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b5fbf99 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Niklas Schütrumpf (Gurkengewuerz) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/linux/.sync/changelog.txt b/linux/.sync/changelog.txt new file mode 100644 index 0000000..0557e1a --- /dev/null +++ b/linux/.sync/changelog.txt @@ -0,0 +1 @@ +write your changelog here \ No newline at end of file diff --git a/linux/generateRepo.sh b/linux/generateRepo.sh index f42f7cd..f0a62e0 100644 --- a/linux/generateRepo.sh +++ b/linux/generateRepo.sh @@ -92,15 +92,29 @@ while IFS= read -r folder; do FILEFOLDER=$(find "${folder}" -type f ! -path "*.zsync" | sed 's|^./||') while IFS= read -r folderfile; do filebyte=$(wc -c < "${folderfile}") + + if [ $filebyte -eq 0 ]; then + echo "Skipping \"${folderfile}\" because file is empty" + continue + fi + foldersize=$(expr $foldersize + $filebyte) name=$(echo "${folderfile}" | cut -d"/" -f2-) x="\"${name}\":{\"size\": ${filebyte}, \"sha1\": \"${SHASUMS[$folderfile]}\"},${x}" done <<< "$FILEFOLDER" x=$(echo ${x} | rev | cut -c2- | rev) + + if [ $foldersize -eq 0 ]; then + echo "Skipping complete folder \"${$folder}\" because all files are empty" + continue + fi JSONDATA+=( "\"${folder}\": {\"size\":${foldersize},\"content\":{${x}}}" ) else echo "is file" filebyte=$(wc -c < "${folder}") + if [ $filebyte -eq 0 ]; then + continue + fi JSONDATA+=( "\"${folder}\": {\"size\":${filebyte}, \"sha1\": \"${SHASUMS[$folder]}\"}" ) fi done <<< "$FILELIST" diff --git a/pom.xml b/pom.xml index 2b1aefa..5c46cb4 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ de.mc8051 arma3launcher - 1.0-SNAPSHOT + 0.1.1000 @@ -44,7 +44,7 @@ com.github.Gurkengewuerz zsyncer - locale_fix-2f7565d392-1 + 1de0d3f651 @@ -65,6 +65,13 @@ **/* + + src/main/resources/icons + false + + **/* + + diff --git a/src/main/java/de/mc8051/arma3launcher/ArmA3Launcher.java b/src/main/java/de/mc8051/arma3launcher/ArmA3Launcher.java index be6430d..e52e9c7 100644 --- a/src/main/java/de/mc8051/arma3launcher/ArmA3Launcher.java +++ b/src/main/java/de/mc8051/arma3launcher/ArmA3Launcher.java @@ -1,9 +1,34 @@ +/* + * MIT License + * + * Copyright (c) 2020-2020 Niklas Schütrumpf (Gurkengewuerz) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + package de.mc8051.arma3launcher; import com.formdev.flatlaf.FlatDarkLaf; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import de.mc8051.arma3launcher.steam.SteamTimer; +import de.mc8051.arma3launcher.utils.TaskBarUtils; import org.ini4j.Ini; import javax.swing.*; @@ -28,6 +53,7 @@ public class ArmA3Launcher { public static String VERSION; public static String CLIENT_NAME; public static String APPLICATION_PATH; + public static String USER_AGENT; public static Config config; public static Ini user_config; @@ -39,11 +65,12 @@ public class ArmA3Launcher { final Properties properties = new Properties(); properties.load(ArmA3Launcher.class.getClassLoader().getResourceAsStream("project.properties")); - VERSION = properties.getProperty("version"); APPLICATION_PATH = getAppData() + CLIENT_NAME; + USER_AGENT = config.getString("sync.useragent") + "/" + VERSION; + if (new File(APPLICATION_PATH).mkdirs()) { Logger.getLogger(ArmA3Launcher.class.getName()).log(Level.SEVERE, "Can not create " + APPLICATION_PATH); System.exit(0); @@ -66,6 +93,8 @@ public class ArmA3Launcher { UIManager.setLookAndFeel(new FlatDarkLaf()); JFrame frame = new JFrame(CLIENT_NAME); + TaskBarUtils.getInstance().setWindow(frame); + LauncherGUI gui = new LauncherGUI(); frame.setContentPane(gui.mainPanel); @@ -74,14 +103,18 @@ public class ArmA3Launcher { public void windowClosing(WindowEvent e) { steamTimer.cancel(); steamTimer.purge(); + TaskBarUtils.getInstance().removeTrayIcon(); + gui.exit(); frame.dispose(); } }); - frame.setMinimumSize(new Dimension(1000, 500)); + frame.setMinimumSize(new Dimension(1000, 550)); frame.pack(); + frame.setIconImage(TaskBarUtils.IMAGE_ICON); frame.setLocationRelativeTo(null); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); steamTimer.scheduleAtFixedRate( new SteamTimer(), diff --git a/src/main/java/de/mc8051/arma3launcher/LauncherGUI.form b/src/main/java/de/mc8051/arma3launcher/LauncherGUI.form index 25f042d..93864aa 100644 --- a/src/main/java/de/mc8051/arma3launcher/LauncherGUI.form +++ b/src/main/java/de/mc8051/arma3launcher/LauncherGUI.form @@ -3,9 +3,11 @@ - + - + + + @@ -16,24 +18,26 @@ - + - - + + - + + + - + @@ -41,6 +45,7 @@ + @@ -55,26 +60,28 @@ - + + - + + - + @@ -82,10 +89,20 @@ - + + - + + + + + + + + + + @@ -101,7 +118,7 @@ - + @@ -110,7 +127,7 @@ - + @@ -118,7 +135,7 @@ - + @@ -126,7 +143,7 @@ - + @@ -134,7 +151,7 @@ - + @@ -147,18 +164,72 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -260,7 +331,7 @@ - + @@ -268,123 +339,33 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + + - + + + - + + + @@ -410,7 +391,9 @@ - + + + @@ -553,7 +536,9 @@ - + + + @@ -576,7 +561,7 @@ - + @@ -592,7 +577,7 @@ - + @@ -600,7 +585,7 @@ - + @@ -616,7 +601,7 @@ - + @@ -624,23 +609,7 @@ - - - - - - - - - - - - - - - - - + @@ -670,7 +639,9 @@ - + + + @@ -693,7 +664,7 @@ - + @@ -704,6 +675,7 @@ + @@ -714,6 +686,7 @@ + @@ -722,7 +695,7 @@ - + @@ -762,6 +735,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -814,7 +935,7 @@ - + @@ -899,7 +1020,7 @@ - + @@ -963,790 +1084,978 @@ - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - + - - - - + + - - - - + + - - + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + diff --git a/src/main/java/de/mc8051/arma3launcher/LauncherGUI.java b/src/main/java/de/mc8051/arma3launcher/LauncherGUI.java index 18fe726..f5d67a9 100644 --- a/src/main/java/de/mc8051/arma3launcher/LauncherGUI.java +++ b/src/main/java/de/mc8051/arma3launcher/LauncherGUI.java @@ -8,10 +8,12 @@ import de.mc8051.arma3launcher.model.PresetTableModel; import de.mc8051.arma3launcher.model.RepositoryTreeNode; import de.mc8051.arma3launcher.model.ServerTableModel; import de.mc8051.arma3launcher.objects.AbstractMod; +import de.mc8051.arma3launcher.objects.Changelog; import de.mc8051.arma3launcher.objects.Mod; import de.mc8051.arma3launcher.objects.ModFile; import de.mc8051.arma3launcher.objects.Modset; import de.mc8051.arma3launcher.objects.Server; +import de.mc8051.arma3launcher.repo.DownloadStatus; import de.mc8051.arma3launcher.repo.FileChecker; import de.mc8051.arma3launcher.repo.RepositoryManger; import de.mc8051.arma3launcher.repo.SyncList; @@ -19,11 +21,12 @@ import de.mc8051.arma3launcher.repo.Syncer; import de.mc8051.arma3launcher.steam.SteamTimer; import de.mc8051.arma3launcher.utils.Callback; import de.mc8051.arma3launcher.utils.Humanize; +import de.mc8051.arma3launcher.utils.ImageUtils; import de.mc8051.arma3launcher.utils.LangUtils; +import de.mc8051.arma3launcher.utils.TaskBarUtils; import javax.swing.*; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; +import javax.swing.border.EmptyBorder; import javax.swing.plaf.basic.BasicTabbedPaneUI; import javax.swing.text.DefaultFormatter; import javax.swing.tree.DefaultMutableTreeNode; @@ -34,14 +37,23 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.lang.management.ManagementFactory; +import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.Properties; +import java.util.Scanner; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; @@ -60,7 +72,6 @@ public class LauncherGUI implements Observer { private JLabel steamStatus; private JLabel armaStatus; private JButton presetPanelButton; - private JPanel logo; private JPanel presetsTab; private JButton playPresetButton; private JButton clonePresetButton; @@ -129,17 +140,32 @@ public class LauncherGUI implements Observer { private JLabel syncDeletedFilesLabel; private JLabel syncAddedFilesLabel; private JLabel syncChangedFilesLabel; - private JLabel syncSizeLabel; + public JLabel syncSizeLabel; private JLabel syncChangedFileSizeLabel; - private JLabel syncFileCountLabel; - public JLabel syncDownloadedLabel; + public JLabel syncFileCountLabel; public JLabel syncDownloadSpeedLabel; private JSplitPane splitView; + public JLabel syncStatusLabel; + private JLabel logo; + private JLabel aboutLabel; + private JButton changelogButton; + private JPanel changelogTab; + private JPanel aboutTab; + private JTextArea changelogPane; + private JScrollPane changelogScroll; + private JLabel twitterIcon; + private JLabel githubIcon; + private JTextPane disclaimer; + private JLabel aboutLogo; + private JLabel aboutClient; + private JLabel aboutProjectLabel; + private JLabel aboutDeveloperLabel; + private JLabel aboutCopyrightLabel; private JCheckBoxTree repoTree; private FileChecker fileChecker; private Syncer syncer; - private SyncList lastSynclist; + private SyncList lastSynclist = null; public LauncherGUI() { fileChecker = new FileChecker(syncCheckProgress); @@ -150,6 +176,16 @@ public class LauncherGUI implements Observer { fileChecker.addObserver(this); syncer.addObserver(this); + new Thread(() -> { + RepositoryManger.getInstance().refreshMeta(); + try { + Thread.sleep(750); + } catch (InterruptedException e) { + e.printStackTrace(); + } + RepositoryManger.getInstance().refreshModset(); + }).start(); + updateTreePanel.remove(tree1); repoTree = new JCheckBoxTree(); @@ -184,63 +220,14 @@ public class LauncherGUI implements Observer { updatePanelButton.setMargin(x); playPanelButton.setMargin(x); presetPanelButton.setMargin(x); + changelogButton.setMargin(x); playPresetButton.setMargin(new Insets(10, 10, 10, 10)); - playPanelButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - tabbedPane1.setSelectedIndex(0); - } - }); - - updatePanelButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - tabbedPane1.setSelectedIndex(1); - } - }); - - presetPanelButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - tabbedPane1.setSelectedIndex(2); - } - }); - - settingsPanelButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - tabbedPane1.setSelectedIndex(3); - } - }); - serverTable.setModel(new ServerTableModel()); - presetList.setModel(new PresetTableModel()); + presetList.setCellRenderer(new PresetListRenderer()); - presetList.addListSelectionListener(new ListSelectionListener() { - - @Override - public void valueChanged(ListSelectionEvent e) { - if (!e.getValueIsAdjusting()) { - PresetTableModel m = (PresetTableModel) presetList.getModel(); - Modset modset = (Modset) m.getElementAt(presetList.getSelectedIndex()); - - if (modset.getType() == Modset.Type.SERVER) { - renamePresetButton.setEnabled(false); - removePresetButtom.setEnabled(false); - } else { - renamePresetButton.setEnabled(true); - removePresetButtom.setEnabled(true); - } - clonePresetButton.setEnabled(true); - - updateModList(modset); - } - } - }); - modList.setCellRenderer(new ModListRenderer()); subtitle.setText( @@ -255,6 +242,49 @@ public class LauncherGUI implements Observer { initSettings(); + logo.setIcon(new ImageIcon(ImageUtils.getScaledImage(TaskBarUtils.IMAGE_LGO, 128, 128))); + aboutLogo.setIcon(new ImageIcon(ImageUtils.getScaledImage(TaskBarUtils.IMAGE_LGO, 128, 128))); + + aboutClient.setText(ArmA3Launcher.config.getString("name") + " v" + ArmA3Launcher.VERSION); + + aboutDeveloperLabel.setText("https://gurkengewuerz.de"); + aboutProjectLabel.setText(""+ArmA3Launcher.config.getString("social.github")+""); + + InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream("disclaimer.html"); + if(resourceAsStream != null) { + Scanner s = new Scanner(resourceAsStream).useDelimiter("\\A"); + String result = s.hasNext() ? s.next() : ""; + disclaimer.setText(result); + } + + + aboutCopyrightLabel.setText(aboutCopyrightLabel.getText().replace("{year}", "" + Calendar.getInstance().get(Calendar.YEAR))); + + twitterIcon.setBorder(new EmptyBorder(2,2,2,2)); + githubIcon.setBorder(new EmptyBorder(2,2,2,2)); + + settingScrollPane.getVerticalScrollBar().setUnitIncrement(16); + updateTreeScrolPane.getVerticalScrollBar().setUnitIncrement(16); + splitView.setDividerLocation(-1); + + presetList.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + PresetTableModel m = (PresetTableModel) presetList.getModel(); + Modset modset = (Modset) m.getElementAt(presetList.getSelectedIndex()); + + if (modset.getType() == Modset.Type.SERVER) { + renamePresetButton.setEnabled(false); + removePresetButtom.setEnabled(false); + } else { + renamePresetButton.setEnabled(true); + removePresetButtom.setEnabled(true); + } + clonePresetButton.setEnabled(true); + + updateModList(modset); + } + }); + settingsResetDefault.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -268,73 +298,106 @@ public class LauncherGUI implements Observer { } }); - settingScrollPane.getVerticalScrollBar().setUnitIncrement(16); - updateTreeScrolPane.getVerticalScrollBar().setUnitIncrement(16); + + collapseAllButton.addActionListener(e -> repoTree.collapseAllNodes()); + playPanelButton.addActionListener(e -> tabbedPane1.setSelectedIndex(0)); + updatePanelButton.addActionListener(e -> tabbedPane1.setSelectedIndex(1)); + changelogButton.addActionListener(e -> { + tabbedPane1.setSelectedIndex(2); + Changelog.refresh(); + }); + presetPanelButton.addActionListener(e -> tabbedPane1.setSelectedIndex(3)); + settingsPanelButton.addActionListener(e -> tabbedPane1.setSelectedIndex(4)); refreshRepoButton.addActionListener(e -> RepositoryManger.getInstance().refreshModset()); - expandAllButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - repoTree.expandAllNodes(); - } + expandAllButton.addActionListener(e -> repoTree.expandAllNodes()); + syncDownloadAbortButton.addActionListener(e -> syncer.stop()); + syncCheckAbortButton.addActionListener(e -> fileChecker.stop()); + + + syncCheckButton.addActionListener(e -> { + syncCheckButton.setEnabled(false); + syncCheckAbortButton.setEnabled(true); + syncCheckStatusLabel.setText("Running!"); + new Thread(() -> fileChecker.check()).start(); + + refreshRepoButton.setEnabled(false); + + repoTree.setCheckboxesEnabled(false); + repoTree.setCheckboxesChecked(false); }); - collapseAllButton.addActionListener(new ActionListener() { + + syncDownloadButton.addActionListener(e -> { + if (!fileChecker.isChecked()) return; + syncDownloadButton.setEnabled(false); + syncDownloadAbortButton.setEnabled(true); + syncPauseButton.setEnabled(true); + syncCheckButton.setEnabled(false); + refreshRepoButton.setEnabled(false); + new Thread(() -> syncer.sync(lastSynclist.clone())).start(); + }); + + syncPauseButton.addActionListener(e -> { + syncer.setPaused(!syncer.isPaused()); + syncPauseButton.setEnabled(false); + }); + + twitterIcon.addMouseListener(new MouseAdapter() { @Override - public void actionPerformed(ActionEvent e) { - repoTree.collapseAllNodes(); + public void mouseEntered(MouseEvent e) { + twitterIcon.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2)); + } + + @Override + public void mouseExited(MouseEvent e) { + twitterIcon.setBorder(new EmptyBorder(2,2,2,2)); } }); - new Thread(() -> { - RepositoryManger.getInstance().refreshMeta(); - try { - Thread.sleep(750); - } catch (InterruptedException e) { - e.printStackTrace(); - } - RepositoryManger.getInstance().refreshModset(); - }).start(); - - syncCheckButton.addActionListener(new ActionListener() { + githubIcon.addMouseListener(new MouseAdapter() { @Override - public void actionPerformed(ActionEvent e) { - syncCheckButton.setEnabled(false); - syncCheckAbortButton.setEnabled(true); - syncCheckStatusLabel.setText("Running!"); - new Thread(() -> fileChecker.check()).start(); + public void mouseEntered(MouseEvent e) { + githubIcon.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2)); + } - repoTree.setCheckboxesEnabled(false); - repoTree.setCheckboxesChecked(false); + @Override + public void mouseExited(MouseEvent e) { + githubIcon.setBorder(new EmptyBorder(2,2,2,2)); } }); - syncCheckAbortButton.addActionListener(new ActionListener() { + aboutLabel.addMouseListener(new MouseAdapter() { @Override - public void actionPerformed(ActionEvent e) { - fileChecker.stop(); + public void mouseClicked(MouseEvent e) { + tabbedPane1.setSelectedIndex(5); } }); - syncDownloadButton.addActionListener(new ActionListener() { + twitterIcon.addMouseListener(new MouseAdapter() { @Override - public void actionPerformed(ActionEvent e) { - if(lastSynclist == null) return; - new Thread(() -> syncer.sync(lastSynclist.clone())).start(); + public void mouseClicked(MouseEvent e) { + openURL(ArmA3Launcher.config.getString("social.twitter")); } }); - syncDownloadAbortButton.addActionListener(new ActionListener() { + githubIcon.addMouseListener(new MouseAdapter() { @Override - public void actionPerformed(ActionEvent e) { - syncer.stop(); + public void mouseClicked(MouseEvent e) { + openURL(ArmA3Launcher.config.getString("social.github")); } }); - syncPauseButton.addActionListener(new ActionListener() { + aboutDeveloperLabel.addMouseListener(new MouseAdapter() { @Override - public void actionPerformed(ActionEvent e) { - syncer.setPaused(!syncer.isPaused()); - syncPauseButton.setEnabled(false); + public void mouseClicked(MouseEvent e) { + openURL("https://gurkengewuerz.de"); + } + }); + + aboutProjectLabel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + openURL(ArmA3Launcher.config.getString("social.github")); } }); } @@ -377,6 +440,7 @@ public class LauncherGUI implements Observer { playPresetButton.setEnabled(false); syncCheckButton.setEnabled(false); refreshRepoButton.setEnabled(false); + syncDownloadButton.setEnabled(false); playButton.setToolTipText(LangUtils.getInstance().getString("arma_running")); playPresetButton.setToolTipText(LangUtils.getInstance().getString("arma_running")); @@ -408,11 +472,15 @@ public class LauncherGUI implements Observer { syncCheckButton.setEnabled(true); refreshRepoButton.setEnabled(true); + syncDownloadButton.setEnabled(fileChecker.isChecked()); + syncCheckButton.setToolTipText(null); refreshRepoButton.setToolTipText(null); } else { - syncCheckButton.setEnabled(true); - refreshRepoButton.setEnabled(true); + syncCheckButton.setEnabled(false); + refreshRepoButton.setEnabled(false); + + syncDownloadButton.setEnabled(false); syncCheckButton.setToolTipText(LangUtils.getInstance().getString("path_not_set")); refreshRepoButton.setToolTipText(LangUtils.getInstance().getString("path_not_set")); @@ -655,7 +723,7 @@ public class LauncherGUI implements Observer { RepositoryTreeNode lastNode = modFolder; ArrayList path = modfile.getPath(); - for (int i = 0; i < path.size() -1; i++) { + for (int i = 0; i < path.size() - 1; i++) { boolean found = false; for (int j = 0; j < lastNode.getChildCount(); j++) { @@ -703,11 +771,11 @@ public class LauncherGUI implements Observer { public void checkStateChanged(JCheckBoxTree.CheckChangeEvent event) { lastSynclist = getSyncList(); if (lastSynclist.getSize() != 0) - syncSizeLabel.setText(Humanize.binaryPrefix(lastSynclist.getSize())); - else syncSizeLabel.setText("0.0 B"); + syncSizeLabel.setText("0.0 B/" + Humanize.binaryPrefix(lastSynclist.getSize())); + else syncSizeLabel.setText("0.0 B/0.0 B"); if (lastSynclist.getCount() != 0) { syncDownloadButton.setEnabled(true); - syncFileCountLabel.setText("" + lastSynclist.getCount()); + syncFileCountLabel.setText("0/" + lastSynclist.getCount()); } else { syncDownloadButton.setEnabled(false); syncFileCountLabel.setText(""); @@ -838,6 +906,17 @@ public class LauncherGUI implements Observer { refreshRepoButton.setEnabled(false); break; } + } else if (s.equals(RepositoryManger.Type.CHANGELOG.toString())) { + if (RepositoryManger.getInstance().getStatus(RepositoryManger.Type.CHANGELOG) == DownloadStatus.FINNISHED) { + SwingUtilities.invokeLater(() -> { + changelogPane.setText(Changelog.get()); + changelogPane.setCaretPosition(0); + changelogPane.setLineWrap(true); + changelogPane.setWrapStyleWord(true); + changelogPane.revalidate(); + changelogPane.repaint(); + }); + } } else if (s.equals("fileChecker")) { syncCheckButton.setEnabled(true); syncCheckAbortButton.setEnabled(false); @@ -867,26 +946,70 @@ public class LauncherGUI implements Observer { syncPauseButton.setEnabled(false); repoTree.setCheckboxesChecked(false); + refreshRepoButton.setEnabled(true); syncAddedFilesLabel.setText("" + 0); syncChangedFilesLabel.setText("" + 0); syncDeletedFilesLabel.setText("" + 0); syncChangedFileSizeLabel.setText("0.0 B"); + } else if (s.equals("syncStopped")) { new Thread(() -> fileChecker.check()).start(); + SwingUtilities.invokeLater(() -> { + syncDownloadButton.setEnabled(false); + syncDownloadAbortButton.setEnabled(false); + syncPauseButton.setEnabled(false); + + syncStatusLabel.setText("Sync stopped"); + syncFileProgress.setValue(0); + TaskBarUtils.getInstance().setValue(0); + TaskBarUtils.getInstance().off(); + }); } else if (s.equals("syncComplete")) { new Thread(() -> fileChecker.check()).start(); + SwingUtilities.invokeLater(() -> { + syncDownloadButton.setEnabled(false); + syncDownloadAbortButton.setEnabled(false); + syncPauseButton.setEnabled(false); + + syncStatusLabel.setText("Sync finished"); + syncFileProgress.setValue(0); + syncFileProgress.setString(""); + TaskBarUtils.getInstance().setValue(0); + TaskBarUtils.getInstance().off(); + TaskBarUtils.getInstance().attention(); + TaskBarUtils.getInstance().notification("Sync complete", "", TrayIcon.MessageType.INFO); + }); } else if (s.equals("syncContinue")) { - syncDownloadAbortButton.setEnabled(true); - syncPauseButton.setEnabled(true); - syncPauseButton.setText(LangUtils.getInstance().getString("pause")); - syncDownloadButton.setEnabled(false); + SwingUtilities.invokeLater(() -> { + syncDownloadAbortButton.setEnabled(true); + syncPauseButton.setEnabled(true); + syncPauseButton.setText(LangUtils.getInstance().getString("pause")); + syncDownloadButton.setEnabled(false); + TaskBarUtils.getInstance().normal(); + }); } else if (s.equals("syncPaused")) { - syncDownloadAbortButton.setEnabled(true); - syncPauseButton.setEnabled(true); - syncPauseButton.setText(LangUtils.getInstance().getString("resume")); - syncDownloadButton.setEnabled(false); + SwingUtilities.invokeLater(() -> { + syncDownloadAbortButton.setEnabled(true); + syncPauseButton.setEnabled(true); + syncPauseButton.setText(LangUtils.getInstance().getString("resume")); + syncDownloadButton.setEnabled(false); + syncFileProgress.setValue(0); + TaskBarUtils.getInstance().paused(); + }); + } + } + + public void exit() { + fileChecker.stop(); + syncer.stop(); + } + + public void openURL(String url) { + try { + Desktop.getDesktop().browse(new URL(url).toURI()); + } catch (Exception ignored) { } } } diff --git a/src/main/java/de/mc8051/arma3launcher/objects/Changelog.java b/src/main/java/de/mc8051/arma3launcher/objects/Changelog.java new file mode 100644 index 0000000..c59aaae --- /dev/null +++ b/src/main/java/de/mc8051/arma3launcher/objects/Changelog.java @@ -0,0 +1,29 @@ +package de.mc8051.arma3launcher.objects; + +import de.mc8051.arma3launcher.repo.RepositoryManger; +import de.mc8051.arma3launcher.utils.Callback; + +/** + * Created by gurkengewuerz.de on 27.03.2020. + */ +public class Changelog { + + private static long lastUpdate = 0; + private static String cache = ""; + + + public static void refresh() { + if(cache.isEmpty() || System.currentTimeMillis() - lastUpdate > 5 * 60 * 1000) { // 5 Minuten + RepositoryManger.getInstance().refreshChangelog(); + lastUpdate = System.currentTimeMillis(); + } + } + + public static String get() { + return cache; + } + + public static void setChangelog(String changelog) { + cache = changelog; + } +} diff --git a/src/main/java/de/mc8051/arma3launcher/objects/ModFile.java b/src/main/java/de/mc8051/arma3launcher/objects/ModFile.java index 43338f5..b504616 100644 --- a/src/main/java/de/mc8051/arma3launcher/objects/ModFile.java +++ b/src/main/java/de/mc8051/arma3launcher/objects/ModFile.java @@ -79,6 +79,10 @@ public class ModFile implements AbstractMod { return modfileString; } + public String getModPath() { + return (parent == null ? "" : parent + "/") + modfileString; + } + public File getLocaleFile() { return f; } diff --git a/src/main/java/de/mc8051/arma3launcher/repo/FileChecker.java b/src/main/java/de/mc8051/arma3launcher/repo/FileChecker.java index 34c3118..1787827 100644 --- a/src/main/java/de/mc8051/arma3launcher/repo/FileChecker.java +++ b/src/main/java/de/mc8051/arma3launcher/repo/FileChecker.java @@ -30,6 +30,8 @@ public class FileChecker implements Observable { private JProgressBar pb; private boolean stop = false; + private boolean checked = false; + private ArrayList deleted = new ArrayList<>(); private HashMap> changed = new HashMap<>(); int changedCount = 0; @@ -93,6 +95,11 @@ public class FileChecker implements Observable { checkDeleted(); notifyObservers("fileChecker"); + checked = true; + } + + public boolean isChecked() { + return checked; } public void stop() { diff --git a/src/main/java/de/mc8051/arma3launcher/repo/RepositoryManger.java b/src/main/java/de/mc8051/arma3launcher/repo/RepositoryManger.java index df3a077..aa202f8 100644 --- a/src/main/java/de/mc8051/arma3launcher/repo/RepositoryManger.java +++ b/src/main/java/de/mc8051/arma3launcher/repo/RepositoryManger.java @@ -4,6 +4,7 @@ import de.mc8051.arma3launcher.ArmA3Launcher; import de.mc8051.arma3launcher.interfaces.Observable; import de.mc8051.arma3launcher.interfaces.Observer; import de.mc8051.arma3launcher.objects.AbstractMod; +import de.mc8051.arma3launcher.objects.Changelog; import de.mc8051.arma3launcher.objects.Mod; import de.mc8051.arma3launcher.objects.ModFile; import de.mc8051.arma3launcher.objects.Modset; @@ -45,6 +46,7 @@ public class RepositoryManger implements Observable { private RepositoryManger() { statusMap.put(Type.METADATA, DownloadStatus.FINNISHED); statusMap.put(Type.MODSET, DownloadStatus.FINNISHED); + statusMap.put(Type.CHANGELOG, DownloadStatus.FINNISHED); } private void getAsync(String urlS, Callback.HttpCallback callback) { @@ -55,7 +57,7 @@ public class RepositoryManger implements Observable { HttpRequest request = HttpRequest.newBuilder() .uri(url) .GET() - .headers("Content-Type", "text/plain;charset=UTF-8") + .headers("User-Agent", ArmA3Launcher.USER_AGENT) .timeout(Duration.of(3, SECONDS)) .build(); @@ -191,6 +193,26 @@ public class RepositoryManger implements Observable { }); } + public void refreshChangelog() { + statusMap.replace(Type.CHANGELOG, DownloadStatus.RUNNING); + RepositoryManger.getInstance().notifyObservers(Type.CHANGELOG.toString()); + + getAsync(ArmA3Launcher.config.getString("sync.url") + "/.sync/changelog.txt", new Callback.HttpCallback() { + @Override + public void response(Response r) { + if (!r.isSuccessful()) { + statusMap.replace(Type.CHANGELOG, DownloadStatus.ERROR); + RepositoryManger.getInstance().notifyObservers(Type.CHANGELOG.toString()); + return; + } + + statusMap.replace(Type.CHANGELOG, DownloadStatus.FINNISHED); + RepositoryManger.getInstance().notifyObservers(Type.CHANGELOG.toString()); + Changelog.setChangelog(r.getBody()); + } + }); + } + public static RepositoryManger getInstance() { if (instance == null) instance = new RepositoryManger(); return instance; @@ -218,7 +240,8 @@ public class RepositoryManger implements Observable { public enum Type { METADATA("metadata"), - MODSET("modset"); + MODSET("modset"), + CHANGELOG("changelog"); private String modset; diff --git a/src/main/java/de/mc8051/arma3launcher/repo/Syncer.java b/src/main/java/de/mc8051/arma3launcher/repo/Syncer.java index ec66c6a..cf91072 100644 --- a/src/main/java/de/mc8051/arma3launcher/repo/Syncer.java +++ b/src/main/java/de/mc8051/arma3launcher/repo/Syncer.java @@ -10,6 +10,7 @@ import de.mc8051.arma3launcher.interfaces.Observer; import de.mc8051.arma3launcher.objects.AbstractMod; import de.mc8051.arma3launcher.objects.ModFile; import de.mc8051.arma3launcher.utils.Humanize; +import de.mc8051.arma3launcher.utils.TaskBarUtils; import javax.swing.*; import java.io.IOException; @@ -36,17 +37,19 @@ public class Syncer extends ZsyncObserver implements Observable { private SyncList modlist; private boolean currentDownload_failed = false; + private String currentDownload_sizeS; private boolean controlfile_downloaded = false; private int failed = 0; private int success = 0; - long syncSize; - int syncCount; + private long syncSize; + private String syncSizeString; + private int syncCount; - long downloadStarted; - long downloadEnded; - long downloadSize; - long downloadDownloaded; + private long downloadStarted; + private long downloadEnded; + private long downloadSize; + private long downloadDownloaded; private Zsync zsync; private LauncherGUI gui; @@ -69,10 +72,12 @@ public class Syncer extends ZsyncObserver implements Observable { success = 0; syncSize = ml.getSize(); + syncSizeString = Humanize.binaryPrefix(syncSize); syncCount = ml.getCount(); SwingUtilities.invokeLater(() -> { gui.syncDownloadProgress.setMaximum(syncCount); gui.syncDownloadProgress.setValue(0); + TaskBarUtils.getInstance().normal(); }); boolean lastPause = false; @@ -120,11 +125,13 @@ public class Syncer extends ZsyncObserver implements Observable { if (mf != null) { Zsync.Options o = new Zsync.Options(); o.setOutputFile(Paths.get(mf.getLocaleFile().getAbsolutePath())); + o.setUseragent(ArmA3Launcher.USER_AGENT); try { currentDownload = mf; currentDownload_failed = false; controlfile_downloaded = false; + currentDownload_sizeS = Humanize.binaryPrefix(currentDownload.getSize()); zsync.zsync(URI.create(mf.getRemoteFile() + ".zsync"), o, this); } catch (ZsyncException | IllegalArgumentException e) { @@ -178,14 +185,14 @@ public class Syncer extends ZsyncObserver implements Observable { @Override public void zsyncStarted(URI requestedZsyncUri, Zsync.Options options) { super.zsyncStarted(requestedZsyncUri, options); - System.out.println("ZSync started " + options.getOutputFile()); - } - @Override - public void controlFileDownloadingComplete() { - super.controlFileDownloadingComplete(); - System.out.println("controlFileDownloadingComplete"); - controlfile_downloaded = true; + SwingUtilities.invokeLater(() -> { + gui.syncFileProgress.setValue(0); + gui.syncFileProgress.setString("0 %"); + }); + + System.out.println("ZSync started " + options.getOutputFile()); + SwingUtilities.invokeLater(() -> gui.syncStatusLabel.setText(currentDownload.getModPath() + ": Sync started")); } @Override @@ -193,6 +200,7 @@ public class Syncer extends ZsyncObserver implements Observable { super.zsyncFailed(exception); currentDownload_failed = true; System.out.println("Zsync failed " + exception.getMessage()); + SwingUtilities.invokeLater(() -> gui.syncStatusLabel.setText(currentDownload.getModPath() + ": Sync failed")); } @Override @@ -211,13 +219,22 @@ public class Syncer extends ZsyncObserver implements Observable { else success++; final long finalSize = syncSize - modlist.getSize(); - int i = success + failed; - int percentage = (int) ((double)i / (double)Long.valueOf(syncCount).intValue() * 100); + final int i = success + failed; + final int percentage = (int) ((double) i / (double) Long.valueOf(syncCount).intValue() * 100); + final String modPath = currentDownload.getModPath(); SwingUtilities.invokeLater(() -> { gui.syncDownloadProgress.setValue(i); - gui.syncDownloadedLabel.setText(Humanize.binaryPrefix(finalSize) + " " + " (" + failed + " failed)"); - gui.syncDownloadProgress.setString(percentage + "%"); + gui.syncFileCountLabel.setText(i + "/" + syncCount + " (" + failed + " failed)"); + gui.syncSizeLabel.setText(Humanize.binaryPrefix(finalSize) + "/" + syncSizeString); + + if (currentDownload_failed) + gui.syncStatusLabel.setText(modPath + ": Sync failed"); + else + gui.syncStatusLabel.setText(modPath + ": Sync finished"); + + gui.syncDownloadProgress.setString(percentage + " %"); + TaskBarUtils.getInstance().setValue(percentage); }); finnishCurrent(); @@ -227,6 +244,16 @@ public class Syncer extends ZsyncObserver implements Observable { public void controlFileDownloadingStarted(URI uri, long length) { super.controlFileDownloadingStarted(uri, length); System.out.println("controlFileDownloadingStarted " + length); + SwingUtilities.invokeLater(() -> gui.syncStatusLabel.setText(currentDownload.getModPath() + ": Get Header")); + } + + @Override + public void controlFileDownloadingComplete() { + super.controlFileDownloadingComplete(); + System.out.println("controlFileDownloadingComplete"); + controlfile_downloaded = true; + + SwingUtilities.invokeLater(() -> gui.syncStatusLabel.setText(currentDownload.getModPath() + ": Hashing")); } @Override @@ -234,26 +261,23 @@ public class Syncer extends ZsyncObserver implements Observable { super.remoteFileDownloadingStarted(uri, length); System.out.println("remoteFileDownloadingStarted " + length); - SwingUtilities.invokeLater(() -> { - gui.syncFileProgress.setMaximum(Long.valueOf(length).intValue()); - gui.syncFileProgress.setValue(0); - }); - downloadSize = length; downloadDownloaded = 0; downloadStarted = System.nanoTime(); + + SwingUtilities.invokeLater(() -> gui.syncStatusLabel.setText(currentDownload.getModPath() + ": Downloading")); } @Override public void bytesDownloaded(long bytes) { super.bytesDownloaded(bytes); -// System.out.println("Downloaded " + bytes); downloadDownloaded += bytes; - // TODO: Fix file Download Progress if (controlfile_downloaded) { + final int percentage = (int) (((double) downloadDownloaded / (double) downloadSize) * 100); SwingUtilities.invokeLater(() -> { - gui.syncFileProgress.setValue(Long.valueOf(downloadDownloaded).intValue()); + gui.syncFileProgress.setValue(percentage); + gui.syncFileProgress.setString(percentage + " % " + Humanize.binaryPrefix(downloadDownloaded) + "/" + currentDownload_sizeS); }); } } diff --git a/src/main/java/de/mc8051/arma3launcher/utils/Callback.java b/src/main/java/de/mc8051/arma3launcher/utils/Callback.java index 868c297..9ab2801 100644 --- a/src/main/java/de/mc8051/arma3launcher/utils/Callback.java +++ b/src/main/java/de/mc8051/arma3launcher/utils/Callback.java @@ -9,11 +9,15 @@ import java.io.File; */ public class Callback { - public static interface JFileSelectCallback { //declare an interface with the callback methods, so you can use on more than one class and just refer to the interface + public interface JFileSelectCallback { boolean allowSelection(File path); } - public static interface HttpCallback { + public interface HttpCallback { void response(Response r); } + + public interface ChangelogCallback { + void response(String changelog); + } } diff --git a/src/main/java/de/mc8051/arma3launcher/utils/Humanize.java b/src/main/java/de/mc8051/arma3launcher/utils/Humanize.java index 8959561..1c871e9 100644 --- a/src/main/java/de/mc8051/arma3launcher/utils/Humanize.java +++ b/src/main/java/de/mc8051/arma3launcher/utils/Humanize.java @@ -16,7 +16,7 @@ public class Humanize { final long[] dividers = new long[] { T, G, M, K, 1 }; final String[] units = new String[] { "TB", "GB", "MB", "KB", "B" }; if(value < 1) - throw new IllegalArgumentException("Invalid file size: " + value); + return "0.0 B"; String result = null; for(int i = 0; i < dividers.length; i++){ final long divider = dividers[i]; diff --git a/src/main/java/de/mc8051/arma3launcher/utils/ImageUtils.java b/src/main/java/de/mc8051/arma3launcher/utils/ImageUtils.java new file mode 100644 index 0000000..aa1acde --- /dev/null +++ b/src/main/java/de/mc8051/arma3launcher/utils/ImageUtils.java @@ -0,0 +1,21 @@ +package de.mc8051.arma3launcher.utils; + +import java.awt.*; +import java.awt.image.BufferedImage; + +/** + * Created by gurkengewuerz.de on 27.03.2020. + */ +public class ImageUtils { + + public static Image getScaledImage(Image srcImg, int w, int h){ + BufferedImage resizedImg = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = resizedImg.createGraphics(); + + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(srcImg, 0, 0, w, h, null); + g2.dispose(); + + return resizedImg; + } +} diff --git a/src/main/java/de/mc8051/arma3launcher/utils/TaskBarUtils.java b/src/main/java/de/mc8051/arma3launcher/utils/TaskBarUtils.java new file mode 100644 index 0000000..bc5d2d0 --- /dev/null +++ b/src/main/java/de/mc8051/arma3launcher/utils/TaskBarUtils.java @@ -0,0 +1,143 @@ +package de.mc8051.arma3launcher.utils; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Created by gurkengewuerz.de on 27.03.2020. + */ +public class TaskBarUtils { + + public static BufferedImage IMAGE_ICON = createIcon(); + public static BufferedImage IMAGE_LGO = createLogo(); + + private static TaskBarUtils instance; + + private final boolean isTaskbarSupported; + private final boolean isSystemtraySupported; + private Taskbar taskbar; + private SystemTray tray; + private TrayIcon trayIcon; + private Window w; + + private TaskBarUtils() { + isTaskbarSupported = Taskbar.isTaskbarSupported(); + if (isTaskbarSupported) { + taskbar = Taskbar.getTaskbar(); + } + + isSystemtraySupported = SystemTray.isSupported(); + if (isSystemtraySupported) { + tray = SystemTray.getSystemTray(); + + try { + trayIcon = new TrayIcon(IMAGE_ICON); + trayIcon.setImageAutoSize(true); + tray.add(trayIcon); + trayIcon.addActionListener(e -> { + if (w == null) return; + if(!(w instanceof JFrame)) return; + SwingUtilities.invokeLater(() -> { + JFrame frame = (JFrame) w; + if(frame.getState()!=Frame.NORMAL) { frame.setState(Frame.NORMAL); } + frame.setVisible(true); + frame.setAlwaysOnTop(true); + frame.toFront(); + frame.requestFocus(); + frame.setAlwaysOnTop(false); + frame.repaint(); + }); + }); + } catch (AWTException e) { + Logger.getLogger(TaskBarUtils.class.getName()).log(Level.SEVERE, null, e); + } + } + } + + public static TaskBarUtils getInstance() { + if (instance == null) instance = new TaskBarUtils(); + return instance; + } + + public void error(Window w) { + if (w == null) return; + if (!isTaskbarSupported) return; + if (!taskbar.isSupported(Taskbar.Feature.PROGRESS_STATE_WINDOW)) return; + taskbar.setWindowProgressState(w, Taskbar.State.ERROR); + } + + public void normal() { + if (w == null) return; + if (!isTaskbarSupported) return; + if (!taskbar.isSupported(Taskbar.Feature.PROGRESS_STATE_WINDOW)) return; + taskbar.setWindowProgressState(w, Taskbar.State.NORMAL); + } + + public void off() { + if (w == null) return; + if (!isTaskbarSupported) return; + if (!taskbar.isSupported(Taskbar.Feature.PROGRESS_STATE_WINDOW)) return; + taskbar.setWindowProgressState(w, Taskbar.State.OFF); + } + + public void paused() { + if (w == null) return; + if (!isTaskbarSupported) return; + if (!taskbar.isSupported(Taskbar.Feature.PROGRESS_STATE_WINDOW)) return; + taskbar.setWindowProgressState(w, Taskbar.State.PAUSED); + } + + public void setValue(int val) { + if (w == null) return; + if (!isTaskbarSupported) return; + if (!taskbar.isSupported(Taskbar.Feature.PROGRESS_VALUE_WINDOW)) return; + taskbar.setWindowProgressValue(w, val); + } + + public void attention() { + if (w == null) return; + if (!isTaskbarSupported) return; + if (!taskbar.isSupported(Taskbar.Feature.USER_ATTENTION_WINDOW)) return; + taskbar.requestWindowUserAttention(w); + } + + public void notification(String caption, String text, TrayIcon.MessageType type) { + if (!isSystemtraySupported) return; + if (trayIcon == null) return; + + trayIcon.displayMessage(caption, text, type); + } + + public void setWindow(Window w) { + this.w = w; + } + + public void removeTrayIcon() { + if (!isSystemtraySupported) return; + if (trayIcon == null) return; + tray.remove(trayIcon); + } + + static BufferedImage createIcon() { + try { + return ImageIO.read(TaskBarUtils.class.getResourceAsStream("/icons/logo_32.png")); + } catch (IOException e) { + Logger.getLogger(TaskBarUtils.class.getName()).log(Level.SEVERE, null, e); + return null; + } + } + + static BufferedImage createLogo() { + try { + return ImageIO.read(TaskBarUtils.class.getResourceAsStream("/icons/logo_256.png")); + } catch (IOException e) { + Logger.getLogger(TaskBarUtils.class.getName()).log(Level.SEVERE, null, e); + return null; + } + } +} diff --git a/src/main/resources/arma3launcher.json b/src/main/resources/arma3launcher.json index 1492c4f..ce1dec0 100644 --- a/src/main/resources/arma3launcher.json +++ b/src/main/resources/arma3launcher.json @@ -3,8 +3,13 @@ "title": "Welcome! :P", "subtitle": "${name} v${version}", "sync": { + "useragent": "TheTownSyncer", "url": "http://46.4.195.36" }, + "social": { + "twitter": "https://twitter.com/TheTownServer", + "github": "https://github.com/Gurkengewuerz/arma3launcher" + }, "client": { "armaPath": "", "modPath": "", diff --git a/src/main/resources/disclaimer.html b/src/main/resources/disclaimer.html new file mode 100644 index 0000000..9ead04c --- /dev/null +++ b/src/main/resources/disclaimer.html @@ -0,0 +1,189 @@ + + + + + +

Haftungsausschluss

+Gemäß §1 Absatz 2 Satz 3 ProdHaftG haftet das Entwickler-Team nicht für Schäden, +die aus der Nutzung des Clients entstehen, da dieser unentgeltlich angeboten und ehrenamtlich entwickelt wird. Das +Entwickler-Team übernimmt darüber hinaus keine Garantie für die ordnungsgemäße Funktion des Clients.

+Darausfolgt auch, dass es keinen Anspruch darauf gibt, dass der Client auf jedem System funktioniert oder Programmfehler +durch +Updates behoben werden. Wir bemühen uns zwar, ein fehlerfreies Programm anzubieten, bitten aber um Verständnis, dass +nicht jeder Fehler unsererseits behoben werden kann. +
+
+

Icons

+
+ Icons made by Freepik from + www.flaticon.com +
+
+ Icons made by Pixel perfect from + www.flaticon.com +
+
+ Icons made by Roundicons from + www.flaticon.com +
+
+
+

Licenses

+

de.mc8051.arma3launcher

+ + MIT License
+
+ Copyright (c) 2020 Niklas Schütrumpf (Gurkengewuerz)
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+
+

com.formdev.flatlaf

+https://github.com/JFormDesigner/FlatLaf
+ + Copyright 2019 FormDev Software GmbH
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+

co.bitshfted.xapps.zsync

+https://github.com/bitshifted/zsyncer
+ + Copyright (c) 2015, Salesforce.com, Inc. All rights reserved.
+ Copyright (c) 2020, Bitshift (bitshifted.co), Inc. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+ following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+ disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided with the distribution.
+
+ Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+

com.github.RalleYTN.SimpleRegistry

+https://github.com/RalleYTN/SimpleRegistry
+ + MIT License
+
+ Copyright (c) 2017 Ralph Niemitz
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+
+

com.typesafe.config

+https://github.com/lightbend/config
+ + Copyright (C) 2011-2012 Typesafe Inc. http://typesafe.com
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+

org.ini4j.ini4j

+https://github.com/facebookarchive/ini4j
+ + Copyright 2005,2009 Ivan SZKIBA
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+

org.json.json

+https://json.org
+ + Copyright (c) 2018 JSON.org
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ The Software shall be used for Good, not Evil.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+ + \ No newline at end of file diff --git a/src/main/resources/icons/changelog_16.png b/src/main/resources/icons/changelog_16.png new file mode 100644 index 0000000000000000000000000000000000000000..61cb2cae89f58018aa252cb4f48c0b4ca8e7a0e4 GIT binary patch literal 536 zcmV+z0_XjSP)`<=&XU49II#wXON>yL)n69S>7#no zPTWnCw==B3eGEvyLOCm^KO{e3hXyEIK8!I@255wqqZfVO^$(r-bDxsw(N9we?r}ui8j4%Lw7mMhRAezI%9lc? z`6cJzEx<1IEpz6*bY*02YP|oHv++546dlWa6#L4V1bL%YuLn!FhYDBAC3(Y19BBv8 z)Xi$}B1m;XfG+H^1?Zu?cR>9o(4CRZ+W#QE@y9Vlo}=mvz@To#Ad><+z@p&aT^L2x a0sH|Ykg{7l!RuuJ0000*ogm literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/download_16.png b/src/main/resources/icons/download_16.png new file mode 100644 index 0000000000000000000000000000000000000000..1a4c6f57a272958e62df7da5021e180deb7f086e GIT binary patch literal 476 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>~Q?gt!9fS2d!q>%?By zioUKD1F>s?Y_T^rVz2AO-#`TG#Q!refW+%WAhJMRd-IPi0#bq{L4LsuxebpVdj0+r zkebuk!MND_NBi_W9zaRPByV>Yh7ML)5QnqCBeIx*f$s-pFvM&QW00 zOEKQT%3As0SfkJ5r;i`E7R{K@dt9-BX@<0|Ov=GA&*LMMG|WN@iLmZVfxEpDzJw wkObKfoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6i^X^r>mdKI;Vst02)Y=UjP6A literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/github_32.png b/src/main/resources/icons/github_32.png new file mode 100644 index 0000000000000000000000000000000000000000..628da97c70890c73e59204f5b140c4e67671e92d GIT binary patch literal 1571 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 N002ovPDHLkV1gJH&^iDB literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/logo_32.png b/src/main/resources/icons/logo_32.png new file mode 100644 index 0000000000000000000000000000000000000000..446dca9d79819e6f942b7b327f104e6e4fb60eb3 GIT binary patch literal 4691 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 Eg6r)4ivR!s literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/twitter_32.png b/src/main/resources/icons/twitter_32.png new file mode 100644 index 0000000000000000000000000000000000000000..2670097d6073cf0425ba0881853f29765e29c4e6 GIT binary patch literal 912 zcmV;B18@9^P)Px&N=ZaPR9Fe^R!wLVQ4pSaNo-QLDN4Omlh|4W6%YPU4@#+j2aDB$P@#|%EEG?I z3VM(p{5=Wv;z9A!G(piry+lPtZ&oT)5iD($wn?>8&>otkN!x73+3Kd*bvLG_pocsN z@6F6NKbf7G_W+8kn1O$s0bDNs?m%=i5S=5!9Y|0ENMHFJa@gqNaEtSs>Zd9gt)oh* zZ)`6n6`m#lk3YH(5VwcD(sflKpWWk+oh6`aM6^PWN5n}4y1>D>*xM1tyc=(Io=|a` z0NnoRE+X&=#E!79?1~D?wR-~bLupxllnbNE5aE^G3P~j)&xu-2eWjjnL^H000Qd7(KOgZ*&s2;y0EE}7;O9Qlo)A!^KMYJdynE&q0N0WP<59xvFh@%)R>1mLh+JGm-iM!-UOJcu3} z+0Xa#;IHZXhyLauqrrHE2re1d@mk0 z8U-PaWnVgGwsQth9f)mB$#ja9TiLnI%t$tW;Q@l8A?#ggIC!$L$~hANKSuWO^I#_- zt>z2I%>`J;-B6`hFr^V?{+_oTBy|k7;&)S=PLK&e;k&!yhxwv8LkRWUiyE0uZ;?cZ zbIXP-HoQ98BE4S}u$d!FiitdW>5&w9ZlX mw6_@(`ME2iiW&G1Gw=hv`xr6)<9I3n0000