diff --git a/README.md b/README.md index 2dc2ebd..cb9f2ce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -## VitaOrganizer 0.3 +## VitaOrganizer 0.4 -![](extra/screenshot-0.3.png) +![](extra/screenshot-0.4.png) Desktop tool for listing and uploading games and homebrew applications to PSVITA without the size requirements of uploading the whole VPK and extracting it later. @@ -10,13 +10,25 @@ It is written in Kotlin/Java. It should work on Windows, Linux and MacOS. It is a Java desktop application, packed in an executable .JAR, that can be executed directly with double click on most cases. -In other cases, you can run it with `java -jar vitaorganizer-0.3.jar` +In other cases, you can run it with `java -jar vitaorganizer-0.4.jar` You can download a prebuild binary here, or just build from source: -[Download VitaOrganizer 0.3 here](https://github.com/soywiz/vitaorganizer/releases/download/0.3/vitaorganizer-0.3.jar) +[Download VitaOrganizer 0.4 here](https://github.com/soywiz/vitaorganizer/releases/download/0.4/vitaorganizer-0.4.jar) ## CHANGELOG +**0.4** + +* Improved row selection +* Supported translations (please, go to github if you want to translate to your own language) +* Show file in explorer/finder +* Show PSF dialog +* Version column useful for homebrew +* Repack : Compression 9 + Remove duplicates + Make it safe (for backups done with older versions of vitamin, or homebrew done with older versions of vitasdk or without -s but that do not require special permissions) +* Queue tasks (not displaying yet, but already allows to queue) +* Added menus that will provide more features in future versions +* Lots of internal improvements + **0.3** * Fixes size of games in psvita (please delete vitaorganizer/cache folder) diff --git a/build.gradle b/build.gradle index 5edb841..40df528 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ group 'com.soywiz' -version new File('resources/currentVersion.txt').text +version new File('resources/com/soywiz/vitaorganizer/currentVersion.txt').text buildscript { - ext.version = new File('resources/currentVersion.txt').text + ext.version = new File('resources/com/soywiz/vitaorganizer/currentVersion.txt').text ext.kotlin_version = '1.0.3' ext.proguard_version = '5.2.1' ext.launch4j_version = '1.6.2' @@ -76,6 +76,11 @@ task minimizedJar(type: proguard.gradle.ProGuardTask) { name: 'main', parameters: 'java.lang.String[]' } + keep access: 'public', + name: 'kotlin.text.RegexOption', { + method access: 'public' + method access: 'private' + } } minimizedJar.dependsOn jar diff --git a/extra/screenshot-0.4.png b/extra/screenshot-0.4.png new file mode 100644 index 0000000..5dc4cde Binary files /dev/null and b/extra/screenshot-0.4.png differ diff --git a/lastVersion.txt b/lastVersion.txt index 8efd03d..650e001 100644 --- a/lastVersion.txt +++ b/lastVersion.txt @@ -1,2 +1,2 @@ -0.3 +0.4 https://github.com/soywiz/vitaorganizer \ No newline at end of file diff --git a/resources/com/soywiz/vitaorganizer/currentVersion.txt b/resources/com/soywiz/vitaorganizer/currentVersion.txt index 1d71ef9..e6adf3f 100644 --- a/resources/com/soywiz/vitaorganizer/currentVersion.txt +++ b/resources/com/soywiz/vitaorganizer/currentVersion.txt @@ -1 +1 @@ -0.3 \ No newline at end of file +0.4 \ No newline at end of file diff --git a/src/com/soywiz/util/OS.kt b/src/com/soywiz/util/OS.kt new file mode 100644 index 0000000..7b165cd --- /dev/null +++ b/src/com/soywiz/util/OS.kt @@ -0,0 +1,16 @@ +package com.soywiz.util + +object OS { + private val OS = System.getProperty("os.name").toLowerCase() + + val isWindows: Boolean get() = OS.indexOf("win") >= 0 + val isMac: Boolean get() = OS.indexOf("mac") >= 0 + val isUnix: Boolean get() = OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0 + val isSolaris: Boolean get() = OS.indexOf("sunos") >= 0 + val os: String + get() = if (isWindows) "win" + else if (isMac) "osx" + else if (isUnix) "uni" + else if (isSolaris) "sol" + else "unknown" +} \ No newline at end of file diff --git a/src/com/soywiz/vitaorganizer/GameEntry.kt b/src/com/soywiz/vitaorganizer/GameEntry.kt index 4e29923..9d1948d 100644 --- a/src/com/soywiz/vitaorganizer/GameEntry.kt +++ b/src/com/soywiz/vitaorganizer/GameEntry.kt @@ -1,6 +1,7 @@ package com.soywiz.vitaorganizer import com.soywiz.util.stream +import java.io.File class GameEntry(val gameId: String) { val entry = VitaOrganizerCache.entry(gameId) @@ -22,7 +23,8 @@ class GameEntry(val gameId: String) { val title by lazy { psf["TITLE"].toString() } var inVita = false var inPC = false - val vpkFile: String? get() = entry.pathFile.readText(Charsets.UTF_8) + val vpkLocalPath: String? get() = entry.pathFile.readText(Charsets.UTF_8) + val vpkLocalFile: File? get() = if (vpkLocalPath != null) File(vpkLocalPath!!) else null val size: Long by lazy { try { entry.sizeFile.readText().toLong() diff --git a/src/com/soywiz/vitaorganizer/VitaOrganizer.kt b/src/com/soywiz/vitaorganizer/VitaOrganizer.kt index de624df..36f18e8 100644 --- a/src/com/soywiz/vitaorganizer/VitaOrganizer.kt +++ b/src/com/soywiz/vitaorganizer/VitaOrganizer.kt @@ -1,5 +1,6 @@ package com.soywiz.vitaorganizer +import com.soywiz.util.OS import com.soywiz.util.open2 import com.soywiz.vitaorganizer.ext.action import com.soywiz.vitaorganizer.ext.getResourceString @@ -82,6 +83,14 @@ object VitaOrganizer : JPanel(BorderLayout()), StatusUpdater { table.setEntries(ALL_GAME_IDS.values.toList()) } + fun showFileInExplorerOrFinder(file: File) { + if (OS.isWindows) { + ProcessBuilder("explorer.exe", "/select," + file.absolutePath).start().waitFor() + } else { + ProcessBuilder("open", "-R", file.absolutePath).start().waitFor() + } + } + val table = object : GameListTable() { val dialog = this@VitaOrganizer val gameTitlePopup = JMenuItem("").apply { @@ -120,11 +129,20 @@ object VitaOrganizer : JPanel(BorderLayout()), StatusUpdater { init { add(gameTitlePopup) add(JSeparator()) + add(JMenuItem(if (OS.isWindows) "Show file in explorer" else "Show file in finder").action { + if (entry != null) { + showFileInExplorerOrFinder(entry!!.vpkLocalFile!!) + } + }) add(JMenuItem(Texts.format("MENU_SHOW_PSF")).action { if (entry != null) { frame.showDialog(KeyValueViewerFrame(Texts.format("PSF_VIEWER_TITLE", "id" to entry!!.id, "title" to entry!!.title), entry!!.psf)) } }) + add(JMenuItem("Repack : Compression 9 + Remove duplicates + Make is safe").action { + if (entry != null) remoteTasks.queue(RepackVpkTask(entry!!, setSecure = true)) + }) + add(JSeparator()) //add(deleteFromVita) add(sendVpkToVita) @@ -373,16 +391,8 @@ object VitaOrganizer : JPanel(BorderLayout()), StatusUpdater { } } - val checkUpdatesButton = JButton(Texts.format("MENU_CHECK_FOR_UPDATES")).apply { - addMouseListener(object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent) { - checkForUpdates() - } - }) - } add(connectButton) add(connectAddress) - add(checkUpdatesButton) } add(header, SpringLayout.NORTH) diff --git a/src/com/soywiz/vitaorganizer/VitaOrganizerCache.kt b/src/com/soywiz/vitaorganizer/VitaOrganizerCache.kt index 80808d6..520dc58 100644 --- a/src/com/soywiz/vitaorganizer/VitaOrganizerCache.kt +++ b/src/com/soywiz/vitaorganizer/VitaOrganizerCache.kt @@ -16,6 +16,14 @@ object VitaOrganizerCache { val pathFile = cacheFolder["$gameId.path"] val sizeFile = cacheFolder["$gameId.size"] val permissionsFile = cacheFolder["$gameId.extperm"] + + fun delete() { + icon0File.delete() + paramSfoFile.delete() + pathFile.delete() + sizeFile.delete() + permissionsFile.delete() + } } fun entry(gameId: String) = Entry(gameId) diff --git a/src/com/soywiz/vitaorganizer/tasks/OneStepToVitaTask.kt b/src/com/soywiz/vitaorganizer/tasks/OneStepToVitaTask.kt index f4dd886..1d10dc7 100644 --- a/src/com/soywiz/vitaorganizer/tasks/OneStepToVitaTask.kt +++ b/src/com/soywiz/vitaorganizer/tasks/OneStepToVitaTask.kt @@ -16,7 +16,7 @@ class OneStepToVitaTask(val entry: GameEntry) : VitaTask() { override fun perform() { sendPromotingVpkTask.perform() - updateStatus("Promoting VPK (this could take a while)...") + status("Promoting VPK (this could take a while)...") PsvitaDevice.promoteVpk(sendPromotingVpkTask.vpkPath) PsvitaDevice.removeFile(sendPromotingVpkTask.vpkPath) diff --git a/src/com/soywiz/vitaorganizer/tasks/RepackVpkTask.kt b/src/com/soywiz/vitaorganizer/tasks/RepackVpkTask.kt new file mode 100644 index 0000000..1e5bd16 --- /dev/null +++ b/src/com/soywiz/vitaorganizer/tasks/RepackVpkTask.kt @@ -0,0 +1,79 @@ +package com.soywiz.vitaorganizer.tasks + +import com.soywiz.vitaorganizer.FileSize +import com.soywiz.vitaorganizer.GameEntry +import com.soywiz.vitaorganizer.VitaOrganizer +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.zip.* + +class RepackVpkTask(val entry: GameEntry, val compression: Int = Deflater.BEST_COMPRESSION, val setSecure: Boolean? = null) : VitaTask() { + override fun perform() { + status("Repacking vpk...") + val file = entry.vpkLocalFile!! + val tempFile = File("${file.absolutePath}.temp") + val tempFile2 = File("${file.absolutePath}.temp2") + + val temp = ByteArray(10 * 1024 * 1024) + + ZipFile(file).use { zip -> + FileOutputStream(tempFile).use { out -> + ZipOutputStream(out).use { zout -> + zout.setLevel(compression) + val entries = zip.entries().toList().distinctBy { it.name } + var currentSize = 0L + val totalSize = entries.map { it.size }.sum() + + for ((index, e) in entries.withIndex()) { + + fun updateStatus() { + status("Repacking ${index}/${entries.size} :: ${FileSize.toString(currentSize)}/${FileSize.toString(totalSize)}") + progress(index, entries.size) + } + + updateStatus() + zout.putNextEntry(ZipEntry(e.name)) + if (e.name == "eboot.bin" && setSecure != null) { + val full = zip.getInputStream(e).readBytes() + if (setSecure) { + full[0x80] = (full[0x80].toInt() and 1.inv()).toByte() // Remove bit 0 + } else { + full[0x80] = (full[0x80].toInt() or 1).toByte() // Set bit 0 + } + zout.write(full) + currentSize += full.size + } else { + zip.getInputStream(e).use { + var localSize = 0L + while (it.available() > 0) { + val bytes = it.read(temp) + if (bytes <= 0) break + zout.write(temp, 0, bytes) + currentSize += bytes + localSize += bytes + if (localSize >= 1 * 1024 * 1024) { + localSize -= 1 * 1024 * 1024 + updateStatus() + } + } + } + updateStatus() + } + + zout.closeEntry() + } + } + } + } + status("Done...") + + file.renameTo(tempFile2) + tempFile.renameTo(file) + tempFile2.delete() + entry.entry.delete() // flush this info! + + VitaOrganizer.updateFileList() + } +} \ No newline at end of file diff --git a/src/com/soywiz/vitaorganizer/tasks/SendDataToVitaTask.kt b/src/com/soywiz/vitaorganizer/tasks/SendDataToVitaTask.kt index 279f8c5..4686538 100644 --- a/src/com/soywiz/vitaorganizer/tasks/SendDataToVitaTask.kt +++ b/src/com/soywiz/vitaorganizer/tasks/SendDataToVitaTask.kt @@ -8,10 +8,10 @@ import javax.swing.JOptionPane class SendDataToVitaTask(val entry: GameEntry) : VitaTask() { override fun perform() { - updateStatus("Sending game ${entry.id}...") + status("Sending game ${entry.id}...") //val zip = ZipFile(entry.vpkFile) try { - PsvitaDevice.uploadGame(entry.id, ZipFile(entry.vpkFile), filter = { path -> + PsvitaDevice.uploadGame(entry.id, ZipFile(entry.vpkLocalPath), filter = { path -> // Skip files already installed in the VPK if (path == "eboot.bin" || path.startsWith("sce_sys/")) { false @@ -20,13 +20,13 @@ class SendDataToVitaTask(val entry: GameEntry) : VitaTask() { } }) { status -> //println("$status") - updateStatus("Uploading ${entry.id} :: ${status.fileRange} :: ${status.sizeRange}") + status("Uploading ${entry.id} :: ${status.fileRange} :: ${status.sizeRange}") } //statusLabel.text = "Processing game ${vitaGameCount + 1}/${vitaGameIds.size} ($gameId)..." } catch (e: Throwable) { error(e.toString()) } - updateStatus("Sent game data ${entry.id}") + status("Sent game data ${entry.id}") info("Game ${entry.id} sent successfully") } } \ No newline at end of file diff --git a/src/com/soywiz/vitaorganizer/tasks/SendPromotingVpkToVitaTask.kt b/src/com/soywiz/vitaorganizer/tasks/SendPromotingVpkToVitaTask.kt index e1f0a5e..b1d1f48 100644 --- a/src/com/soywiz/vitaorganizer/tasks/SendPromotingVpkToVitaTask.kt +++ b/src/com/soywiz/vitaorganizer/tasks/SendPromotingVpkToVitaTask.kt @@ -5,7 +5,7 @@ import java.util.zip.ZipFile import javax.swing.JOptionPane class SendPromotingVpkToVitaTask(val entry: GameEntry) : VitaTask() { - val zip = ZipFile(entry.vpkFile) + val zip = ZipFile(entry.vpkLocalPath) val vpkPath = "ux0:/organizer/${entry.id}.VPK" override fun checkBeforeQueue() { @@ -18,23 +18,23 @@ class SendPromotingVpkToVitaTask(val entry: GameEntry) : VitaTask() { } override fun perform() { - updateStatus(Texts.format("STEP_GENERATING_SMALL_VPK_FOR_PROMOTING")) + status(Texts.format("STEP_GENERATING_SMALL_VPK_FOR_PROMOTING")) //val zip = ZipFile(entry.vpkFile) try { val vpkData = createSmallVpk(zip) - updateStatus(Texts.format("STEP_GENERATED_SMALL_VPK_FOR_PROMOTING")) + status(Texts.format("STEP_GENERATED_SMALL_VPK_FOR_PROMOTING")) PsvitaDevice.uploadFile("/$vpkPath", vpkData) { status -> progress(status.currentSize, status.totalSize) - updateStatus(Texts.format("STEP_UPLOADING_VPK_FOR_PROMOTING", "current" to status.currentSize, "total" to status.totalSize)) + status(Texts.format("STEP_UPLOADING_VPK_FOR_PROMOTING", "current" to status.currentSize, "total" to status.totalSize)) } } catch (e: Throwable) { e.printStackTrace() JOptionPane.showMessageDialog(VitaOrganizer, "${e.toString()}", "${e.message}", JOptionPane.ERROR_MESSAGE); } - updateStatus("Sent game vpk ${entry.id}") + status("Sent game vpk ${entry.id}") info("Now use VitaShell to install\n$vpkPath\n\nAfter that active ftp again and use this program to Send Data to PSVita") zip.close() diff --git a/src/com/soywiz/vitaorganizer/tasks/UpdateFileListTask.kt b/src/com/soywiz/vitaorganizer/tasks/UpdateFileListTask.kt index 938cfef..4feb9f7 100644 --- a/src/com/soywiz/vitaorganizer/tasks/UpdateFileListTask.kt +++ b/src/com/soywiz/vitaorganizer/tasks/UpdateFileListTask.kt @@ -12,11 +12,11 @@ class UpdateFileListTask : VitaTask() { VitaOrganizer.VPK_GAME_IDS.clear() } val vpkFiles = File(VitaOrganizerSettings.vpkFolder).listFiles().filter { it.name.toLowerCase().endsWith(".vpk") } - updateStatus(Texts.format("STEP_ANALYZING_FILES", "folder" to VitaOrganizerSettings.vpkFolder)) + status(Texts.format("STEP_ANALYZING_FILES", "folder" to VitaOrganizerSettings.vpkFolder)) var count = 0 for (vpkFile in File(VitaOrganizerSettings.vpkFolder).listFiles().filter { it.name.toLowerCase().endsWith(".vpk") }) { //println(vpkFile) - updateStatus(Texts.format("STEP_ANALYZING_ITEM", "name" to vpkFile.name, "current" to count + 1, "total" to vpkFiles.size)) + status(Texts.format("STEP_ANALYZING_ITEM", "name" to vpkFile.name, "current" to count + 1, "total" to vpkFiles.size)) try { val zip = ZipFile(vpkFile) val paramSfoData = zip.getBytes("sce_sys/param.sfo") @@ -50,7 +50,7 @@ class UpdateFileListTask : VitaTask() { e.printStackTrace() } } - updateStatus(Texts.format("STEP_DONE")) + status(Texts.format("STEP_DONE")) VitaOrganizer.updateEntries() } } \ No newline at end of file diff --git a/src/com/soywiz/vitaorganizer/tasks/VitaTask.kt b/src/com/soywiz/vitaorganizer/tasks/VitaTask.kt index 8228933..d1a12e1 100644 --- a/src/com/soywiz/vitaorganizer/tasks/VitaTask.kt +++ b/src/com/soywiz/vitaorganizer/tasks/VitaTask.kt @@ -16,7 +16,7 @@ open class VitaTask { val globalProgress = Progress(0L, 0L) val localProgress = Progress(0L, 0L) - fun updateStatus(status: String) { + fun status(status: String) { SwingUtilities.invokeLater { VitaOrganizer.updateStatus(status) } @@ -43,6 +43,10 @@ open class VitaTask { localProgress.set(current, max) } + fun progress(current: Int, max: Int) { + localProgress.set(current.toLong(), max.toLong()) + } + open fun checkBeforeQueue() { } diff --git a/src/com/soywiz/vitaorganizer/tasks/createSmallVpk.kt b/src/com/soywiz/vitaorganizer/tasks/createSmallVpk.kt index 06d4c18..64fe33a 100644 --- a/src/com/soywiz/vitaorganizer/tasks/createSmallVpk.kt +++ b/src/com/soywiz/vitaorganizer/tasks/createSmallVpk.kt @@ -17,6 +17,7 @@ fun createSmallVpk(zip: ZipFile): ByteArray { if (e.name == "eboot.bin" || e.name.startsWith("sce_sys/")) { out.putNextEntry(ZipEntry(e.name)) out.write(zip.getInputStream(e).readBytes()) + out.closeEntry() } }