Compare commits

..

137 Commits

Author SHA1 Message Date
Zlatin Balevsky
ff1df88601 Release 0.5.7 2019-11-03 12:35:04 +00:00
Zlatin Balevsky
4ed572ba51 clear search button 2019-11-03 12:03:12 +00:00
Zlatin Balevsky
fd3f55ab4d implement restore session 2019-11-03 10:06:55 +00:00
Zlatin Balevsky
1358e14467 add options for search history 2019-11-03 08:12:10 +00:00
Zlatin Balevsky
e22d5fea11 better search box 2019-11-03 01:50:55 +00:00
Zlatin Balevsky
7ade4aa10d set row height to trees 2019-11-02 19:06:26 +00:00
Zlatin Balevsky
a9f623a91a correct method name 2019-11-02 18:51:02 +00:00
Zlatin Balevsky
1ce410e943 wip on signing queries 2019-11-02 18:34:13 +00:00
Zlatin Balevsky
27aad9d75d do not collapse tree on updates pt2 2019-11-02 17:41:04 +00:00
Zlatin Balevsky
24591b10f2 change the griffon environment 2019-11-02 10:13:28 -07:00
Zlatin Balevsky
e4f1ea5c10 make table rows a bit larger 2019-11-02 15:58:48 +00:00
Zlatin Balevsky
c73c44c5f2 base table row height on the size of the font 2019-11-02 15:46:50 +00:00
Zlatin Balevsky
309cbcc580 UTF-8 in props of cli 2019-11-02 15:23:15 +00:00
Zlatin Balevsky
86894f242b support UTF-8 in persona names 2019-11-02 14:43:24 +00:00
Zlatin Balevsky
568255140f visualize the negative tree as well 2019-11-02 12:54:43 +00:00
Zlatin Balevsky
f6d2bac5bb show all watched directories 2019-11-02 12:26:19 +00:00
Zlatin Balevsky
1c396711ed Fix sidecar files larger than the limit from being shared 2019-11-02 11:15:08 +00:00
Zlatin Balevsky
c154d9538d only check negative tree for files, not directories 2019-11-02 10:28:04 +00:00
Zlatin Balevsky
8043782446 logging config with all logs turned off 2019-11-02 08:52:29 +00:00
Zlatin Balevsky
00c529cca1 toString() 2019-11-02 00:40:08 +00:00
Zlatin Balevsky
094b9ac2b0 restore behavior where watched directories get scanned on startup 2019-11-02 00:27:12 +00:00
Zlatin Balevsky
0dae0a561b more accurate speed measurement. Makes a difference if MW is minimized for a long time 2019-11-01 18:39:41 +00:00
Zlatin Balevsky
82eaafc2c3 Release 0.5.6 2019-10-31 23:22:13 +00:00
Zlatin Balevsky
a3fc1a62e7 format the I2P bandwidths 2019-10-31 21:52:22 +00:00
Zlatin Balevsky
2fd8f45107 update text in cli 2019-10-31 21:22:50 +00:00
Zlatin Balevsky
2429bbf59e Add update notification window 2019-10-31 20:51:09 +00:00
Zlatin Balevsky
f7e28e04f6 add a system status panel 2019-10-31 14:14:14 +00:00
Zlatin Balevsky
cc0188f20e show used memory, not free memory 2019-10-31 13:46:16 +00:00
Zlatin Balevsky
af9b4f4679 change package name for cli 2019-10-31 13:05:42 +00:00
Zlatin Balevsky
625a559d02 change package name 2019-10-31 13:02:44 +00:00
Zlatin Balevsky
6e20193d57 properly set Xmx 2019-10-31 07:15:54 +00:00
Zlatin Balevsky
88ac267f99 show java version and ram usage in cli 2019-10-31 07:14:52 +00:00
Zlatin Balevsky
9b3a7473d1 limit Xmx on cli-lanterna too 2019-10-31 06:52:56 +00:00
Zlatin Balevsky
5b0180280e fix changing font and size on metal lnf 2019-10-30 22:20:27 +00:00
Zlatin Balevsky
d0462034fc enforce comment length in cli as well 2019-10-30 21:51:16 +00:00
Zlatin Balevsky
f3e4098107 refresh gui when processing a sidecar file 2019-10-30 21:45:38 +00:00
Zlatin Balevsky
26e7ca0b21 enforce maximum comment length in the gui 2019-10-30 21:22:08 +00:00
Zlatin Balevsky
11007e5f19 allow up to exact max comment length 2019-10-30 21:20:09 +00:00
Zlatin Balevsky
ae651cb6bd implement sidecar files 2019-10-30 21:07:59 +00:00
Zlatin Balevsky
cad3a88517 Xmx256M by default 2019-10-30 21:06:33 +00:00
Zlatin Balevsky
29c81646af word-wrap the comment views 2019-10-30 19:52:37 +00:00
Zlatin Balevsky
8a0257927b Link to CLI configuration options 2019-10-30 19:43:51 +00:00
Zlatin Balevsky
3b882ae644 Release 0.5.5 2019-10-29 16:16:36 +00:00
Zlatin Balevsky
5b61738ca9 skip downloaders that can't start 2019-10-29 15:56:19 +00:00
Zlatin Balevsky
c77d79513e more long arithmetic fixes 2019-10-29 15:34:48 +00:00
Zlatin Balevsky
9f12442897 long arithmetic 2019-10-29 15:07:29 +00:00
Zlatin Balevsky
477b0a47ad more logging 2019-10-29 14:33:23 +00:00
Zlatin Balevsky
7f1041dd96 @Log 2019-10-29 14:22:28 +00:00
Zlatin Balevsky
99393c59bd log when skipping a download 2019-10-29 14:15:43 +00:00
Zlatin Balevsky
a78d8c84ca unmap before flushing 2019-10-29 13:12:59 +00:00
Zlatin Balevsky
fa9c697bfa do not flush the output stream on Endpoint.close(). This fixes the long shutdown time 2019-10-29 12:38:41 +00:00
Zlatin Balevsky
e5b12701f5 do not crash the core if the XHave in mesh.json fails to parse 2019-10-29 10:28:14 +00:00
Zlatin Balevsky
f69727ab43 wait less time for reset() 2019-10-29 09:35:57 +00:00
Zlatin Balevsky
d7c7afe2c0 move the connections closing to a separate threadpool and limit the time we wait for reset() to complete 2019-10-29 09:01:41 +00:00
Zlatin Balevsky
6c806c4441 fix display of uploader progress to reach 100% 2019-10-29 01:00:59 +00:00
Zlatin Balevsky
c4095abdb4 sanity-check the X-Have header 2019-10-29 00:15:00 +00:00
Zlatin Balevsky
8801546854 tighten piece size range 2019-10-28 23:36:40 +00:00
Zlatin Balevsky
f6ee49c0f5 add upper bounds to the file length and piece size 2019-10-28 23:25:32 +00:00
Zlatin Balevsky
2320d650f6 do not serialize meshes that have more downloaded pieces than total pieces. To be investigated further 2019-10-28 23:16:27 +00:00
Zlatin Balevsky
e9e6e6920a <= part 2 2019-10-28 23:12:32 +00:00
Zlatin Balevsky
87e5007f39 <= 2019-10-28 23:06:50 +00:00
Zlatin Balevsky
8df6715e24 guard mesh.json as well 2019-10-28 23:00:03 +00:00
Zlatin Balevsky
6d587bf228 guard against piece size or count of 0 2019-10-28 22:51:24 +00:00
Zlatin Balevsky
8684452848 Add ability to limit the total number of upload slots, as well as per user 2019-10-28 14:48:38 +00:00
Zlatin Balevsky
7d652fabcb add option to close warning dialog to exit app. Add config option for exit behavior in the options 2019-10-28 13:28:03 +00:00
Zlatin Balevsky
5eb8d75bba Show how many times we've been browsed and increment hit counter 2019-10-27 11:26:41 +00:00
Zlatin Balevsky
9ca8d1738c do not re-share watched directories from the cli 2019-10-27 10:42:26 +00:00
Zlatin Balevsky
2bb9480137 the filetree map gets accessed from the directory watcher thread 2019-10-27 09:54:16 +00:00
Zlatin Balevsky
7a6365f87a Implement a negative lookup structure to prevent explicitly unshared files in watched directories from being re-shared 2019-10-27 09:13:22 +00:00
Zlatin Balevsky
56540ca3ca delay initial persistence to give chance to events to reach FileManager 2019-10-27 09:08:57 +00:00
Zlatin Balevsky
eb5a5198b1 more efficient unsharing of nested dirs 2019-10-27 05:12:25 +00:00
Zlatin Balevsky
29562c42ea add toString() 2019-10-27 05:12:01 +00:00
Zlatin Balevsky
f5284f9483 add upload speed column to cli 2019-10-27 03:07:18 +00:00
Zlatin Balevsky
9bd3c4f141 add speed column to uploads table 2019-10-27 03:00:54 +00:00
Zlatin Balevsky
817dd68faf Add a cli settings file, automatic or manual clearing of downloads and uploads 2019-10-27 02:29:20 +00:00
Zlatin Balevsky
5954cdb342 remove requests column, reword option for consistency 2019-10-26 17:41:57 +01:00
Zlatin Balevsky
56d44e6458 Do not clear uploads by default 2019-10-26 16:45:21 +01:00
Zlatin Balevsky
c6fb76610d Add search hit and download count to shared file table in both UIs 2019-10-26 15:02:46 +01:00
Zlatin Balevsky
5e329dfa2c Release 0.5.4 2019-10-26 06:42:14 +01:00
Zlatin Balevsky
742f6da870 update notifications 2019-10-26 06:12:54 +01:00
Zlatin Balevsky
7f46347c0f retry failed downloads 2019-10-26 05:33:22 +01:00
Zlatin Balevsky
b308ac2f37 searches by hash 2019-10-26 05:14:04 +01:00
Zlatin Balevsky
9cdabb51d1 count shared files in dashboard 2019-10-25 22:51:26 +01:00
Zlatin Balevsky
45f0736a5e account for hashing errors 2019-10-25 22:51:15 +01:00
Zlatin Balevsky
fe753ff978 add a download details view 2019-10-25 22:36:25 +01:00
Zlatin Balevsky
ac717b5205 center things horizontally 2019-10-25 22:02:04 +01:00
Zlatin Balevsky
6f624e3afc add some stats to main window 2019-10-25 21:51:16 +01:00
Zlatin Balevsky
623d675ed9 Ability to view comments 2019-10-25 18:57:07 +01:00
Zlatin Balevsky
546b71b632 implement adding comments to shared files 2019-10-25 18:32:55 +01:00
Zlatin Balevsky
804113bb1b typo 2019-10-25 17:46:59 +01:00
Zlatin Balevsky
ab9e10f438 add a note about the CLI 2019-10-25 17:43:15 +01:00
Zlatin Balevsky
00520acdf0 implement browse host 2019-10-25 17:30:16 +01:00
Zlatin Balevsky
8c44d196a7 move gui result processing on gui thread 2019-10-25 13:14:38 +01:00
Zlatin Balevsky
9c5fa0a2ce hook up trust to results 2019-10-25 10:36:26 +01:00
Zlatin Balevsky
d7bca05725 implement trust list review window 2019-10-25 10:00:52 +01:00
Zlatin Balevsky
45fcb2209e Trust List actions 2019-10-25 08:48:07 +01:00
Zlatin Balevsky
7bf0373b80 trust and distrust actions 2019-10-25 08:24:07 +01:00
Zlatin Balevsky
5925b42597 wip on trust window 2019-10-25 07:39:01 +01:00
Zlatin Balevsky
13243b05ad center shutdown dialog 2019-10-25 06:14:14 +01:00
Zlatin Balevsky
43987be463 prevent RejectedExecutionExceptions on shutdown 2019-10-25 06:13:20 +01:00
Zlatin Balevsky
fcd3414e02 refresh number of connections automatically 2019-10-25 06:08:41 +01:00
Zlatin Balevsky
70913ea8fb correct startup sequence, add listeners for allFilesLoadedEvent 2019-10-25 06:01:16 +01:00
Zlatin Balevsky
b30e552498 share and unshare a directory 2019-10-24 22:35:29 +01:00
Zlatin Balevsky
bae66de4eb implement share file dialog 2019-10-24 22:03:20 +01:00
Zlatin Balevsky
626e145e25 properly set size of tables 2019-10-24 19:22:39 +01:00
Zlatin Balevsky
bf72c76f13 limit the size of the table based on the terminal size 2019-10-24 19:12:50 +01:00
Zlatin Balevsky
fce8bbfd97 wip on shared files window 2019-10-24 18:34:27 +01:00
Zlatin Balevsky
1cc7925155 uploads window 2019-10-24 17:24:01 +01:00
Zlatin Balevsky
12b51ceb02 add an ETA column 2019-10-24 16:51:11 +01:00
Zlatin Balevsky
62811861a4 working downloads window 2019-10-24 16:36:10 +01:00
Zlatin Balevsky
837aa6974b display search results in new window 2019-10-24 14:39:25 +01:00
Zlatin Balevsky
94e7c42d19 add option to specify I2CP host and port. Show failure message is I2CP connect fails 2019-10-24 08:15:02 +01:00
Zlatin Balevsky
877bf12a93 fixed progress dialog, wip on search view 2019-10-24 07:49:15 +01:00
Zlatin Balevsky
224266b2dd basic initialization of the core 2019-10-23 22:25:54 +01:00
Zlatin Balevsky
8f16614dc3 start a new project for an interactive cli 2019-10-23 19:38:16 +01:00
Zlatin Balevsky
b412f9fb0c Release 0.5.3 2019-10-23 09:01:19 +01:00
Zlatin Balevsky
b24d04811d set apple quit strategy 2019-10-23 08:55:10 +01:00
Zlatin Balevsky
771f645df0 proper close 2019-10-23 08:48:53 +01:00
Zlatin Balevsky
b6483ad0f4 add an exit menu 2019-10-23 08:45:03 +01:00
Zlatin Balevsky
decb72c8ef show a warning that MW will continue running 2019-10-23 08:31:23 +01:00
Zlatin Balevsky
439b3bf18b fixes 2019-10-23 06:46:20 +01:00
Zlatin Balevsky
06679ffee0 only show MW if the core has loaded 2019-10-23 06:39:25 +01:00
Zlatin Balevsky
1d5b12e2d7 if core is not initialized, just shutdown 2019-10-23 06:31:08 +01:00
Zlatin Balevsky
4e6e1b6f5b Do not show warnings if core is already shutting down 2019-10-23 06:15:34 +01:00
Zlatin Balevsky
f0b5361d7b smaller icon 2019-10-23 06:06:37 +01:00
Zlatin Balevsky
e0c6bfbf51 show the clsoing window if tray is disabled 2019-10-23 06:01:21 +01:00
Zlatin Balevsky
2a0ecd8a47 fix constructor 2019-10-23 05:48:14 +01:00
Zlatin Balevsky
fb1804e849 Use explicit event to shutdown the application. This fixes closing on Linux 2019-10-23 05:45:50 +01:00
Zlatin Balevsky
d4eaa0df8d do not shutdown core on awt thread 2019-10-22 23:37:44 +01:00
Zlatin Balevsky
ffde6ac86f show a window while MW is shutting down 2019-10-22 23:26:54 +01:00
Zlatin Balevsky
7ad677ead2 add an explicit menu to show MW 2019-10-22 21:48:51 +01:00
Zlatin Balevsky
ddb0568aab do not auto-shutdown 2019-10-22 21:40:47 +01:00
Zlatin Balevsky
ff50a84a48 try to get a tray icon working 2019-10-22 21:34:50 +01:00
Zlatin Balevsky
770396ba41 update test 2019-10-22 10:31:28 +01:00
Zlatin Balevsky
b55852e993 typo 2019-10-22 10:16:41 +01:00
Zlatin Balevsky
a6945275a4 i2p 0.9.43 2019-10-22 08:27:08 +01:00
Zlatin Balevsky
7241809e55 update readme 2019-10-22 00:42:18 +01:00
114 changed files with 4243 additions and 231 deletions

View File

@@ -4,7 +4,7 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.4.15 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
The current stable release - 0.5.5 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
### Building
@@ -21,7 +21,7 @@ If you want to run the unit tests, type
If you want to build binary bundles that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project
### Running
### Running the GUI
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z.jar` in a terminal or command prompt.
@@ -29,6 +29,12 @@ If you have an I2P router running on the same machine that is all you need to do
[Default I2CP port]\: `7654`
### Running the CLI
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
The CLI is under active development and doesn't have all the features of the GUI.
### GPG Fingerprint
```

27
cli-lanterna/build.gradle Normal file
View File

@@ -0,0 +1,27 @@
buildscript {
repositories {
jcenter()
mavenLocal()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
}
}
apply plugin : 'application'
application {
mainClassName = 'com.muwire.clilanterna.CliLanterna'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
applicationName = 'MuWire-cli'
}
apply plugin : 'com.github.johnrengelman.shadow'
dependencies {
compile project(":core")
compile 'com.googlecode.lanterna:lanterna:3.0.1'
}

View File

@@ -0,0 +1,73 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.SharedFile
import com.muwire.core.files.UICommentEvent
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class AddCommentView extends BasicWindow {
private final TextGUI textGUI
private final Core core
private final TextBox textBox
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
AddCommentView(TextGUI textGUI, Core core, SharedFile sharedFile, TerminalSize terminalSize) {
super("Add Comment To "+sharedFile.getFile().getName())
this.textGUI = textGUI
this.core = core
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
String oldComment = sharedFile.getComment()
if (oldComment == null)
oldComment = ""
else
oldComment = DataUtil.readi18nString(Base64.decode(oldComment))
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
textBox = new TextBox(boxSize,oldComment,TextBox.Style.MULTI_LINE)
contentPanel.addComponent(textBox, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
contentPanel.addComponent(buttonsPanel, layoutData)
Button saveButton = new Button("Save", {
String newComment = textBox.getText()
if (newComment.length() > Constants.MAX_COMMENT_LENGTH) {
String error = "Your comment is too long - ${newComment.length()} bytes. Maximum is $Constants.MAX_COMMENT_LENGTH bytes"
MessageDialog.showMessageDialog(textGUI, "Comment Too Long", error, MessageDialogButton.Close)
} else {
newComment = Base64.encode(DataUtil.encodei18nString(newComment))
String encodedOldComment = sharedFile.getComment()
sharedFile.setComment(newComment)
core.eventBus.publish(new UICommentEvent(sharedFile : sharedFile, oldComment : encodedOldComment))
close()
}
})
Button cancelButton = new Button("Cancel", {close()})
buttonsPanel.addComponent(saveButton, layoutData)
buttonsPanel.addComponent(cancelButton, layoutData)
setComponent(contentPanel)
}
}

View File

@@ -0,0 +1,75 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.search.BrowseStatus
import com.muwire.core.search.BrowseStatusEvent
import com.muwire.core.search.UIBrowseEvent
import com.muwire.core.search.UIResultEvent
import net.i2p.data.Base64
import net.i2p.data.DataHelper
class BrowseModel {
private final Persona persona
private final Core core
private final TextGUIThread guiThread
private final TableModel model = new TableModel("Name","Size","Hash","Comment")
private Map<String, UIResultEvent> rootToResult = new HashMap<>()
private int totalResults
private Label status
private Label percentage
BrowseModel(Persona persona, Core core, TextGUIThread guiThread) {
this.persona = persona
this.core = core
this.guiThread = guiThread
core.eventBus.register(BrowseStatusEvent.class, this)
core.eventBus.register(UIResultEvent.class, this)
core.eventBus.publish(new UIBrowseEvent(host : persona))
}
void unregister() {
core.eventBus.unregister(BrowseStatusEvent.class, this)
core.eventBus.unregister(UIResultEvent.class, this)
}
void onBrowseStatusEvent(BrowseStatusEvent e) {
guiThread.invokeLater {
status.setText(e.status.toString())
if (e.status == BrowseStatus.FETCHING)
totalResults = e.totalResults
}
}
void onUIResultEvent(UIResultEvent e) {
guiThread.invokeLater {
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
String infoHash = Base64.encode(e.infohash.getRoot())
String comment = String.valueOf(e.comment != null)
model.addRow(e.name, size, infoHash, comment)
rootToResult.put(infoHash, e)
String percentageString = ""
if (totalResults != 0) {
double percentage = Math.round( (model.getRowCount() * 100 / totalResults).toDouble() )
percentageString = String.valueOf(percentage)+"%"
}
percentage.setText(percentageString)
}
}
void setStatusLabel(Label status) {
this.status = status
}
void setPercentageLabel(Label percentage) {
this.percentage = percentage
}
}

View File

@@ -0,0 +1,111 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.UIResultEvent
class BrowseView extends BasicWindow {
private final BrowseModel model
private final TextGUI textGUI
private final Core core
private final Table table
private final TerminalSize terminalSize
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
BrowseView(BrowseModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
super("Browse "+model.persona.getHumanReadableName())
this.model = model
this.textGUI = textGUI
this.core = core
this.terminalSize = terminalSize
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
Label statusLabel = new Label("")
Label percentageLabel = new Label("")
model.setStatusLabel(statusLabel)
model.setPercentageLabel(percentageLabel)
Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2))
topPanel.addComponent(statusLabel, layoutData)
topPanel.addComponent(percentageLabel, layoutData)
contentPanel.addComponent(topPanel, layoutData)
table = new Table("Name","Size","Hash","Comment")
table.with {
setCellSelection(false)
setTableModel(model.model)
setVisibleRows(terminalSize.getRows())
setSelectAction({rowSelected()})
}
contentPanel.addComponent(table, layoutData)
Button closeButton = new Button("Close",{
model.unregister()
close()
})
contentPanel.addComponent(closeButton, layoutData)
setComponent(contentPanel)
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def row = model.model.getRow(selectedRow)
String infoHash = row[2]
boolean comment = Boolean.parseBoolean(row[3])
if (comment) {
Window prompt = new BasicWindow("Download Or View Comment")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
Button downloadButton = new Button("Download", {download(infoHash)})
Button viewButton = new Button("View Comment", {viewComment(infoHash)})
Button closeButton = new Button("Cancel", {prompt.close()})
contentPanel.with {
addComponent(downloadButton, layoutData)
addComponent(viewButton, layoutData)
addComponent(closeButton, layoutData)
}
prompt.setComponent(contentPanel)
downloadButton.takeFocus()
textGUI.addWindowAndWait(prompt)
} else {
download(infoHash)
}
}
private void download(String infoHash) {
UIResultEvent result = model.rootToResult[infoHash]
def file = new File(core.muOptions.downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : [result], sources : result.sources,
target : file, sequential : false))
MessageDialog.showMessageDialog(textGUI, "Download started", "Started download of "+result.name, MessageDialogButton.OK)
}
private void viewComment(String infoHash) {
UIResultEvent result = model.rootToResult[infoHash]
ViewCommentView view = new ViewCommentView(result, terminalSize)
textGUI.addWindowAndWait(view)
}
}

View File

@@ -0,0 +1,193 @@
package com.muwire.clilanterna
import java.nio.charset.StandardCharsets
import java.util.concurrent.CountDownLatch
import java.util.logging.Level
import java.util.logging.LogManager
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Border
import com.googlecode.lanterna.gui2.BorderLayout
import com.googlecode.lanterna.gui2.Borders
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.MultiWindowTextGUI
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.SeparateTextGUIThread
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.WindowBasedTextGUI
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder
import com.googlecode.lanterna.gui2.dialogs.WaitingDialog
import com.googlecode.lanterna.screen.Screen
import com.googlecode.lanterna.terminal.DefaultTerminalFactory
import com.googlecode.lanterna.terminal.Terminal
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.5.7"
private static volatile Core core
private static WindowBasedTextGUI textGUI
public static void main(String[] args) {
if (System.getProperty("java.util.logging.config.file") == null) {
def names = LogManager.getLogManager().getLoggerNames()
while(names.hasMoreElements()) {
def name = names.nextElement()
LogManager.getLogManager().getLogger(name).setLevel(Level.SEVERE)
}
}
def home = System.getProperty("user.home") + File.separator + ".MuWire"
home = new File(home)
if (!home.exists())
home.mkdirs()
def propsFile = new File(home,"MuWire.properties")
DefaultTerminalFactory terminalFactory = new DefaultTerminalFactory()
Screen screen = terminalFactory.createScreen()
textGUI = new MultiWindowTextGUI( new SeparateTextGUIThread.Factory(), screen)
textGUI.getGUIThread().start()
screen.startScreen()
def props
if (!propsFile.exists()) {
String nickname = TextInputDialog.showDialog(textGUI, "Select a nickname", "", "")
String defaultDownloadLocation = System.getProperty("user.home")+File.separator+"Downloads"
String downloadLocation = TextInputDialog.showDialog(textGUI, "Select download location", "", defaultDownloadLocation)
String defaultIncompletesLocation = System.getProperty("user.home")+File.separator+".MuWire"+File.separator+"incompletes"
String incompletesLocation = TextInputDialog.showDialog(textGUI, "Select incompletes location", "", defaultIncompletesLocation)
File downloadLocationFile = new File(downloadLocation)
if (!downloadLocationFile.exists())
downloadLocationFile.mkdirs()
File incompletesLocationFile = new File(incompletesLocation)
if (!incompletesLocationFile.exists())
incompletesLocationFile.mkdirs()
props = new MuWireSettings()
props.setNickname(nickname)
props.setDownloadLocation(downloadLocationFile)
props.incompleteLocation = incompletesLocationFile
propsFile.withPrintWriter("UTF-8", {
props.write(it)
})
} else {
props = new Properties()
propsFile.withReader("UTF-8", {
props.load(it)
})
props = new MuWireSettings(props)
}
props.updateType = "cli-lanterna"
def i2pPropsFile = new File(home, "i2p.properties")
if (!i2pPropsFile.exists()) {
String i2pHost = TextInputDialog.showDialog(textGUI, "I2P router host", "Specifiy the host I2P router is on", "127.0.0.1")
int i2pPort = TextInputDialog.showNumberDialog(textGUI, "I2CP port", "Specify the I2CP port", "7654").toInteger()
Properties i2pProps = new Properties()
i2pProps["i2cp.tcp.host"] = i2pHost
i2pProps["i2cp.tcp.port"] = String.valueOf(i2pPort)
i2pPropsFile.withOutputStream { i2pProps.store(it, "") }
}
def cliProps
def cliPropsFile = new File(home, "cli.properties")
if (cliPropsFile.exists()) {
Properties p = new Properties()
cliPropsFile.withInputStream {
p.load(it)
}
cliProps = new CliSettings(p)
} else
cliProps = new CliSettings(new Properties())
Window window = new BasicWindow("MuWire "+ MW_VERSION)
window.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.withBorder(Borders.doubleLine())
BorderLayout layout = new BorderLayout()
contentPanel.setLayoutManager(layout)
Panel welcomeNamePanel = new Panel()
contentPanel.addComponent(welcomeNamePanel, BorderLayout.Location.CENTER)
welcomeNamePanel.setLayoutManager(new GridLayout(1))
Label welcomeLabel = new Label("Welcome to MuWire "+ props.nickname)
welcomeNamePanel.addComponent(welcomeLabel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
Panel connectButtonPanel = new Panel()
contentPanel.addComponent(connectButtonPanel, BorderLayout.Location.BOTTOM)
connectButtonPanel.setLayoutManager(new GridLayout(1))
Button connectButton = new Button("Connect", {
WaitingDialog waiting = new WaitingDialog("Connecting", "Please wait")
waiting.showDialog(textGUI, false)
CountDownLatch latch = new CountDownLatch(1)
Thread connector = new Thread({
try {
core = new Core(props, home, MW_VERSION)
} finally {
latch.countDown()
}
})
connector.start()
while(latch.getCount() > 0) {
textGUI.updateScreen()
Thread.sleep(10)
}
waiting.close()
window.close()
} as Runnable)
welcomeNamePanel.addComponent(connectButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
window.setComponent(contentPanel)
textGUI.addWindowAndWait(window)
if (core == null) {
MessageDialog.showMessageDialog(textGUI, "Failed", "MuWire failed to load", MessageDialogButton.Close)
System.exit(1)
}
window = new MainWindowView("MuWire "+MW_VERSION, core, textGUI, screen, cliProps)
core.startServices()
core.eventBus.publish(new UILoadedEvent())
textGUI.addWindowAndWait(window)
CountDownLatch latch = new CountDownLatch(1)
Thread stopper = new Thread({
core.shutdown()
latch.countDown()
} as Runnable)
WaitingDialog waitingForShutdown = new WaitingDialog("MuWire is shutting down","Please wait")
waitingForShutdown.setHints([Window.Hint.CENTERED])
waitingForShutdown.showDialog(textGUI, false)
stopper.start()
while(latch.getCount() > 0) {
textGUI.updateScreen()
Thread.sleep(10)
}
waitingForShutdown.close()
screen.stopScreen()
System.exit(0)
}
}

View File

@@ -0,0 +1,25 @@
package com.muwire.clilanterna
class CliSettings {
boolean clearCancelledDownloads
boolean clearFinishedDownloads
boolean clearUploads
CliSettings(Properties props) {
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","true"))
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads", "false"))
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads", "false"))
}
void write(OutputStream os) {
Properties props = new Properties()
props.with {
setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
setProperty("clearUploads", String.valueOf(clearUploads))
store(os, "CLI Properties")
}
}
}

View File

@@ -0,0 +1,67 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.Window
import com.muwire.core.download.Downloader
class DownloadDetailsView extends BasicWindow {
private final Downloader downloader
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
private Label knownSources, activeSources, donePieces
DownloadDetailsView(Downloader downloader) {
super("Download details for "+downloader.file.getName())
this.downloader = downloader
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(2))
knownSources = new Label("0")
activeSources = new Label("0")
donePieces = new Label("0")
refresh()
Button refreshButton = new Button("Refresh",{refresh()})
Button closeButton = new Button("Close", {close()})
contentPanel.with {
addComponent(new Label("Target Location"), layoutData)
addComponent(new Label(downloader.file.getAbsolutePath()), layoutData)
addComponent(new Label("Piece Size"), layoutData)
addComponent(new Label(String.valueOf(downloader.pieceSize)), layoutData)
addComponent(new Label("Total Pieces"), layoutData)
addComponent(new Label(String.valueOf(downloader.nPieces)), layoutData)
addComponent(new Label("Done Pieces"), layoutData)
addComponent(donePieces, layoutData)
addComponent(new Label("Known Sources"), layoutData)
addComponent(knownSources, layoutData)
addComponent(new Label("Active Sources"), layoutData)
addComponent(activeSources, layoutData)
addComponent(refreshButton, layoutData)
addComponent(closeButton, layoutData)
}
setComponent(contentPanel)
}
private void refresh() {
int done = downloader.donePieces()
int known = downloader.activeWorkers.size()
int active = downloader.activeWorkers()
knownSources.setText(String.valueOf(known))
activeSources.setText(String.valueOf(active))
donePieces.setText(String.valueOf(done))
}
}

View File

@@ -0,0 +1,15 @@
package com.muwire.clilanterna
import com.muwire.core.download.Downloader
class DownloaderWrapper {
final Downloader downloader
DownloaderWrapper(Downloader downloader) {
this.downloader = downloader
}
@Override
public String toString() {
downloader.file.getName()
}
}

View File

@@ -0,0 +1,100 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileDownloadedEvent
import net.i2p.data.DataHelper
class DownloadsModel {
private final TextGUIThread guiThread
private final Core core
private final CliSettings props
private final List<Downloader> downloaders = new ArrayList<>()
private final TableModel model = new TableModel("Name", "Status", "Progress", "Speed", "ETA")
private long lastRetryTime
DownloadsModel(TextGUIThread guiThread, Core core, CliSettings props) {
this.guiThread = guiThread
this.core = core
this.props = props
core.eventBus.register(DownloadStartedEvent.class, this)
Timer timer = new Timer(true)
Runnable guiRunnable = {
refreshModel()
resumeDownloads()
}
timer.schedule({
if (core.shutdown.get())
return
guiThread.invokeLater(guiRunnable)
} as TimerTask, 1000,1000)
}
void onDownloadStartedEvent(DownloadStartedEvent e) {
guiThread.invokeLater({
downloaders.add(e.downloader)
refreshModel()
})
}
private void refreshModel() {
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
if (props.clearCancelledDownloads) {
downloaders.removeAll { it.cancelled }
}
if (props.clearFinishedDownloads) {
downloaders.removeAll { it.getCurrentState() == Downloader.DownloadState.FINISHED }
}
downloaders.each {
String status = it.getCurrentState().toString()
int speedInt = it.speed()
String speed = DataHelper.formatSize2Decimal(speedInt, false) + "B/sec"
int pieces = it.nPieces
int done = it.donePieces()
int percent = -1
if (pieces != 0)
percent = (done * 100 / pieces)
String totalSize = DataHelper.formatSize2Decimal(it.length, false) + "B"
String progress = (String.format("%2d", percent) + "% of ${totalSize}".toString())
String ETA
if (speedInt == 0)
ETA = "Unknown"
else {
long remaining = (pieces - done) * it.pieceSize / speedInt
ETA = DataHelper.formatDuration(remaining * 1000)
}
model.addRow([new DownloaderWrapper(it), status, progress, speed, ETA])
}
}
private void resumeDownloads() {
int retryInterval = core.muOptions.downloadRetryInterval
if (retryInterval == 0)
return
retryInterval *= 1000
long now = System.currentTimeMillis()
if (now - lastRetryTime > retryInterval) {
lastRetryTime = now
downloaders.each {
def state = it.getCurrentState()
if (state == Downloader.DownloadState.FAILED || state == Downloader.DownloadState.DOWNLOADING)
it.resume()
}
}
}
}

View File

@@ -0,0 +1,94 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.download.Downloader
import com.muwire.core.download.UIDownloadCancelledEvent
class DownloadsView extends BasicWindow {
private final Core core
private final DownloadsModel model
private final TextGUI textGUI
private final Table table
DownloadsView(Core core, DownloadsModel model, TextGUI textGUI, TerminalSize terminalSize) {
this.core = core
this.model = model
this.textGUI = textGUI
setHints([Window.Hint.EXPANDED])
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
table = new Table("Name","Status","Progress","Speed","ETA")
table.setCellSelection(false)
table.setSelectAction({rowSelected()})
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button clearButton = new Button("Clear Done",{clearDone()})
buttonsPanel.addComponent(clearButton, layoutData)
Button closeButton = new Button("Close",{close()})
buttonsPanel.addComponent(closeButton, layoutData)
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel)
closeButton.takeFocus()
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def row = model.model.getRow(selectedRow)
Downloader downloader = row[0].downloader
Window prompt = new BasicWindow("Kill Download?")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button killDownload = new Button("Kill Download", {
downloader.cancel()
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
MessageDialog.showMessageDialog(textGUI, "Download Killed", downloader.file.getName()+ " has been killed", MessageDialogButton.OK)
})
Button viewDetails = new Button("View Details", {
textGUI.addWindowAndWait(new DownloadDetailsView(downloader))
})
Button close = new Button("Close", {
prompt.close()
})
contentPanel.addComponent(killDownload,layoutData)
contentPanel.addComponent(viewDetails, layoutData)
contentPanel.addComponent(close, layoutData)
prompt.setComponent(contentPanel)
close.takeFocus()
textGUI.addWindowAndWait(prompt)
}
private void clearDone() {
model.downloaders.removeAll {
def state = it.getCurrentState()
state == Downloader.DownloadState.CANCELLED || state == Downloader.DownloadState.FINISHED
}
}
}

View File

@@ -0,0 +1,80 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.SharedFile
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryWatchedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.trust.TrustSubscriptionEvent
import net.i2p.data.DataHelper
class FilesModel {
private final TextGUIThread guiThread
private final Core core
private final List<SharedFile> sharedFiles = new ArrayList<>()
private final TableModel model = new TableModel("Name","Size","Comment","Search Hits","Downloaders")
FilesModel(TextGUIThread guiThread, Core core) {
this.guiThread = guiThread
this.core = core
core.eventBus.register(FileLoadedEvent.class, this)
core.eventBus.register(FileUnsharedEvent.class, this)
core.eventBus.register(FileHashedEvent.class, this)
core.eventBus.register(AllFilesLoadedEvent.class, this)
Runnable refreshModel = {refreshModel()}
Timer timer = new Timer(true)
timer.schedule({
guiThread.invokeLater(refreshModel)
} as TimerTask, 1000,1000)
}
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
def eventBus = core.eventBus
guiThread.invokeLater {
core.muOptions.watchedDirectories.each {
eventBus.publish(new DirectoryWatchedEvent(directory : new File(it)))
}
}
}
void onFileLoadedEvent(FileLoadedEvent e) {
guiThread.invokeLater {
sharedFiles.add(e.loadedFile)
}
}
void onFileHashedEvent(FileHashedEvent e) {
guiThread.invokeLater {
if (e.sharedFile != null)
sharedFiles.add(e.sharedFile)
}
}
void onFileUnsharedEvent(FileUnsharedEvent e) {
guiThread.invokeLater {
sharedFiles.remove(e.unsharedFile)
}
}
private void refreshModel() {
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
sharedFiles.each {
long size = it.getCachedLength()
boolean comment = it.comment != null
String hits = String.valueOf(it.getHits())
String downloaders = String.valueOf(it.getDownloaders().size())
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, hits, downloaders)
}
}
}

View File

@@ -0,0 +1,131 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.FileDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.SharedFile
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.UIPersistFilesEvent
class FilesView extends BasicWindow {
private final FilesModel model
private final TextGUI textGUI
private final Core core
private final Table table
private final TerminalSize terminalSize
FilesView(FilesModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
super("Shared Files")
this.model = model
this.core = core
this.textGUI = textGUI
this.terminalSize = terminalSize
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
table = new Table("Name","Size","Comment","Search Hits","Downloaders")
table.setCellSelection(false)
table.setTableModel(model.model)
table.setSelectAction({rowSelected()})
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(4))
Button shareFile = new Button("Share File", {shareFile()})
Button shareDirectory = new Button("Share Directory", {shareDirectory()})
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
Button close = new Button("Close", {close()})
buttonsPanel.with {
addComponent(shareFile, layoutData)
addComponent(shareDirectory, layoutData)
addComponent(unshareDirectory, layoutData)
addComponent(close, layoutData)
}
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel)
close.takeFocus()
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def row = model.model.getRow(selectedRow)
SharedFile sf = row[0].sharedFile
Window prompt = new BasicWindow("Unshare or add comment to "+sf.getFile().getName()+" ?")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
Button unshareButton = new Button("Unshare", {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
core.eventBus.publish(new UIPersistFilesEvent())
MessageDialog.showMessageDialog(textGUI, "File Unshared", "Unshared "+sf.getFile().getName(), MessageDialogButton.OK)
} )
Button addCommentButton = new Button("Add Comment", {
AddCommentView view = new AddCommentView(textGUI, core, sf, terminalSize)
textGUI.addWindowAndWait(view)
})
Button closeButton = new Button("Close", {prompt.close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
contentPanel.addComponent(unshareButton, layoutData)
contentPanel.addComponent(addCommentButton, layoutData)
contentPanel.addComponent(closeButton, layoutData)
prompt.setComponent(contentPanel)
textGUI.addWindowAndWait(prompt)
}
private void shareFile() {
TerminalSize terminalSize = new TerminalSize(terminalSize.getColumns() - 10, terminalSize.getRows() - 10)
FileDialog fileDialog = new FileDialog("Share File", "Select a file to share", "Share", terminalSize, false, null)
File f = fileDialog.showDialog(textGUI)
f = f.getCanonicalFile()
core.eventBus.publish(new FileSharedEvent(file : f))
MessageDialog.showMessageDialog(textGUI, "File Shared", f.getName()+" has been shared", MessageDialogButton.OK)
}
private void shareDirectory() {
String directoryName = TextInputDialog.showDialog(textGUI, "Share a directory", "Enter the directory to share", "")
if (directoryName == null)
return
File directory = new File(directoryName)
directory = directory.getCanonicalFile()
core.eventBus.publish(new FileSharedEvent(file : directory))
MessageDialog.showMessageDialog(textGUI, "Directory Shared", directory.getName()+" has been shared", MessageDialogButton.OK)
}
private void unshareDirectory() {
String directoryName = TextInputDialog.showDialog(textGUI, "Unshare a directory", "Enter the directory to unshare", "")
if (directoryName == null)
return
File directory = new File(directoryName)
directory = directory.getCanonicalFile()
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
MessageDialog.showMessageDialog(textGUI, "Directory Unshared", directory.getName()+" has been unshared", MessageDialogButton.OK)
}
}

View File

@@ -0,0 +1,311 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalPosition
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.BorderLayout
import com.googlecode.lanterna.gui2.Borders
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.Panels
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.screen.Screen
import com.googlecode.lanterna.gui2.TextBox
import com.muwire.core.Core
import com.muwire.core.DownloadedFile
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.update.UpdateAvailableEvent
import com.muwire.core.update.UpdateDownloadedEvent
import net.i2p.data.Base64
import net.i2p.data.DataHelper
class MainWindowView extends BasicWindow {
private final Core core
private final TextGUI textGUI
private final Screen screen
private final TextBox searchTextBox
private final DownloadsModel downloadsModel
private final UploadsModel uploadsModel
private final FilesModel filesModel
private final TrustModel trustModel
private final Label connectionCount, incoming, outgoing
private final Label known, failing, hopeless
private final Label sharedFiles
private final Label timesBrowsed
private final Label updateStatus
private final Label usedRam, totalRam, maxRam
public MainWindowView(String title, Core core, TextGUI textGUI, Screen screen, CliSettings props) {
super(title);
this.core = core
this.textGUI = textGUI
this.screen = screen
downloadsModel = new DownloadsModel(textGUI.getGUIThread(),core, props)
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
filesModel = new FilesModel(textGUI.getGUIThread(),core)
trustModel = new TrustModel(textGUI.getGUIThread(), core)
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
setComponent(contentPanel)
BorderLayout borderLayout = new BorderLayout()
contentPanel.setLayoutManager(borderLayout)
Panel buttonsPanel = new Panel()
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
GridLayout gridLayout = new GridLayout(7)
buttonsPanel.setLayoutManager(gridLayout)
searchTextBox = new TextBox(new TerminalSize(40, 1))
Button searchButton = new Button("Search", { search() })
Button downloadsButton = new Button("Downloads", {download()})
Button uploadsButton = new Button("Uploads", {upload()})
Button filesButton = new Button("Files", { files() })
Button trustButton = new Button("Trust", {trust()})
Button quitButton = new Button("Quit", {close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
buttonsPanel.with {
addComponent(searchTextBox, layoutData)
addComponent(searchButton, layoutData)
addComponent(downloadsButton, layoutData)
addComponent(uploadsButton, layoutData)
addComponent(filesButton, layoutData)
addComponent(trustButton, layoutData)
addComponent(quitButton, layoutData)
}
Panel bottomPanel = new Panel()
contentPanel.addComponent(bottomPanel, BorderLayout.Location.BOTTOM)
BorderLayout bottomLayout = new BorderLayout()
bottomPanel.setLayoutManager(bottomLayout)
Label persona = new Label(core.me.getHumanReadableName())
bottomPanel.addComponent(persona, BorderLayout.Location.LEFT)
Panel connectionsPanel = new Panel()
connectionsPanel.setLayoutManager(new GridLayout(2))
Label connections = new Label("Connections:")
connectionsPanel.addComponent(connections, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
connectionCount = new Label("0")
connectionsPanel.addComponent(connectionCount, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
bottomPanel.addComponent(connectionsPanel, BorderLayout.Location.RIGHT)
Panel centralPanel = new Panel()
centralPanel.setLayoutManager(new GridLayout(1))
contentPanel.addComponent(centralPanel, BorderLayout.Location.CENTER)
Panel statusPanel = new Panel()
statusPanel.setLayoutManager(new GridLayout(2))
statusPanel.withBorder(Borders.doubleLine("Stats"))
centralPanel.addComponent(statusPanel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, true))
incoming = new Label("0")
outgoing = new Label("0")
known = new Label("0")
failing = new Label("0")
hopeless = new Label("0")
sharedFiles = new Label("0")
timesBrowsed = new Label("0")
updateStatus = new Label("Unknown")
usedRam = new Label("0")
maxRam = new Label("0")
totalRam = new Label("0")
statusPanel.with {
addComponent(new Label("Incoming Connections: "), layoutData)
addComponent(incoming, layoutData)
addComponent(new Label("Outgoing Connections: "), layoutData)
addComponent(outgoing, layoutData)
addComponent(new Label("Known Hosts: "), layoutData)
addComponent(known, layoutData)
addComponent(new Label("Failing Hosts: "), layoutData)
addComponent(failing, layoutData)
addComponent(new Label("Hopeless Hosts: "), layoutData)
addComponent(hopeless, layoutData)
addComponent(new Label("Shared Files: "), layoutData)
addComponent(sharedFiles, layoutData)
addComponent(new Label("Times Browsed: "), layoutData)
addComponent(timesBrowsed, layoutData)
addComponent(new Label("Update Status: "), layoutData)
addComponent(updateStatus, layoutData)
addComponent(new Label("Java Version: "), layoutData)
addComponent(new Label(System.getProperty("java.vendor")+ " " + System.getProperty("java.version")), layoutData)
addComponent(new Label("Used Memory: "), layoutData)
addComponent(usedRam, layoutData)
addComponent(new Label("Total Memory: "), layoutData)
addComponent(totalRam, layoutData)
addComponent(new Label("Maximum Memory: "), layoutData)
addComponent(maxRam, layoutData)
}
refreshStats()
searchButton.takeFocus()
core.eventBus.register(ConnectionEvent.class, this)
core.eventBus.register(HostDiscoveredEvent.class, this)
core.eventBus.register(FileLoadedEvent.class, this)
core.eventBus.register(FileHashedEvent.class, this)
core.eventBus.register(FileUnsharedEvent.class, this)
core.eventBus.register(FileDownloadedEvent.class, this)
core.eventBus.register(UpdateAvailableEvent.class, this)
core.eventBus.register(UpdateDownloadedEvent.class, this)
}
void onConnectionEvent(ConnectionEvent e) {
textGUI.getGUIThread().invokeLater {
connectionCount.setText(String.valueOf(core.connectionManager.connections.size()))
refreshStats()
}
}
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
textGUI.getGUIThread().invokeLater {
refreshStats()
}
}
void onFileLoadedEvent(FileLoadedEvent e) {
textGUI.getGUIThread().invokeLater {
refreshStats()
}
}
void onFileHashedEvent(FileHashedEvent e) {
textGUI.getGUIThread().invokeLater {
refreshStats()
}
}
void onFileUnsharedEvent(FileUnsharedEvent e) {
textGUI.getGUIThread().invokeLater {
refreshStats()
}
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
textGUI.getGUIThread().invokeLater {
refreshStats()
}
}
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
textGUI.getGUIThread().invokeLater {
String label = "$e.version is available with hash $e.infoHash"
updateStatus.setText(label)
String message = "Version $e.version is available, with hash $e.infoHash . Show details?"
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
if (button == MessageDialogButton.No)
return
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
}
}
void onUpdateDownloadedEvent(UpdateDownloadedEvent e) {
textGUI.getGUIThread().invokeLater {
String label = "$e.version downloaded"
updateStatus.setText(label)
String message = "MuWire version $e.version has been downloaded. Show details?."
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
if (button == MessageDialogButton.No)
return
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
}
}
private TerminalSize sizeForTables() {
TerminalSize full = screen.getTerminalSize()
return new TerminalSize(full.getColumns(), full.getRows() - 10)
}
private void search() {
String query = searchTextBox.getText()
query = query.trim()
if (query.length() == 0)
return
if (query.length() > 128)
query = query.substring(0, 128)
SearchModel model = new SearchModel(query, core, textGUI.getGUIThread())
textGUI.addWindowAndWait(new SearchView(model,core, textGUI, sizeForTables()))
}
private void download() {
textGUI.addWindowAndWait(new DownloadsView(core, downloadsModel, textGUI, sizeForTables()))
}
private void upload() {
textGUI.addWindowAndWait(new UploadsView(uploadsModel, sizeForTables()))
}
private void files() {
textGUI.addWindowAndWait(new FilesView(filesModel, textGUI, core, sizeForTables()))
}
private void trust() {
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
}
private void refreshStats() {
int inCon = 0
int outCon = 0
core.connectionManager.getConnections().each {
if (it.isIncoming())
inCon++
else
outCon++
}
int knownHosts = core.hostCache.hosts.size()
int failingHosts = core.hostCache.countFailingHosts()
int hopelessHosts = core.hostCache.countHopelessHosts()
int shared = core.fileManager.fileToSharedFile.size()
int browsed = core.connectionAcceptor.browsed
long freeMemL = Runtime.getRuntime().freeMemory()
long totalMemL = Runtime.getRuntime().totalMemory()
String usedMem = DataHelper.formatSize2Decimal(freeMemL, false) + "B"
String totalMem = DataHelper.formatSize2Decimal(totalMemL, false)+"B"
String maxMem
long maxMemL = Runtime.getRuntime().maxMemory()
if (maxMemL >= Long.MAX_VALUE / 2)
maxMem = "Unlimited"
else
maxMem = DataHelper.formatSize2Decimal(maxMemL, false) + "B"
incoming.setText(String.valueOf(inCon))
outgoing.setText(String.valueOf(outCon))
known.setText(String.valueOf(knownHosts))
failing.setText(String.valueOf(failingHosts))
hopeless.setText(String.valueOf(hopelessHosts))
sharedFiles.setText(String.valueOf(shared))
timesBrowsed.setText(String.valueOf(browsed))
usedRam.setText(usedMem)
totalRam.setText(totalMem)
maxRam.setText(maxMem)
}
}

View File

@@ -0,0 +1,15 @@
package com.muwire.clilanterna
import com.muwire.core.Persona
class PersonaWrapper {
private final Persona persona
PersonaWrapper(Persona persona) {
this.persona = persona
}
@Override
public String toString() {
persona.getHumanReadableName()
}
}

View File

@@ -0,0 +1,28 @@
package com.muwire.clilanterna
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import net.i2p.data.Base64
import net.i2p.data.DataHelper
import com.googlecode.lanterna.gui2.table.TableModel
class ResultsModel {
private final UIResultBatchEvent results
final TableModel model
final Map<String, UIResultEvent> rootToResult = new HashMap<>()
ResultsModel(UIResultBatchEvent results) {
this.results = results
model = new TableModel("Name","Size","Hash","Sources","Comment")
results.results.each {
String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
String infoHash = Base64.encode(it.infohash.getRoot())
String sources = String.valueOf(it.sources.size())
String comment = String.valueOf(it.comment != null)
model.addRow(it.name, size, infoHash, sources, comment)
rootToResult.put(infoHash, it)
}
}
}

View File

@@ -0,0 +1,94 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.UIResultEvent
class ResultsView extends BasicWindow {
private final ResultsModel model
private final TextGUI textGUI
private final Core core
private final Table table
private final TerminalSize terminalSize
ResultsView(ResultsModel model, Core core, TextGUI textGUI, TerminalSize terminalSize) {
super(model.results.results[0].sender.getHumanReadableName() + " Results")
this.model = model
this.core = core
this.textGUI = textGUI
this.terminalSize = terminalSize
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
table = new Table("Name","Size","Hash","Sources","Comment")
table.setCellSelection(false)
table.setSelectAction({rowSelected()})
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
Button closeButton = new Button("Close", {close()})
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
setComponent(contentPanel)
closeButton.takeFocus()
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def rows = model.model.getRow(selectedRow)
boolean comment = Boolean.parseBoolean(rows[4])
if (comment) {
Window prompt = new BasicWindow("Download Or View Comment")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
Button downloadButton = new Button("Download", {download(rows[2])})
Button viewButton = new Button("View Comment", {viewComment(rows[2])})
Button closeButton = new Button("Cancel", {prompt.close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
contentPanel.addComponent(downloadButton, layoutData)
contentPanel.addComponent(viewButton, layoutData)
contentPanel.addComponent(closeButton, layoutData)
prompt.setComponent(contentPanel)
downloadButton.takeFocus()
textGUI.addWindowAndWait(prompt)
} else {
download(rows[2])
}
}
private void download(String infohash) {
UIResultEvent result = model.rootToResult[infohash]
def file = new File(core.muOptions.downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : [result], sources : result.sources,
target : file, sequential : false))
MessageDialog.showMessageDialog(textGUI, "Download Started", "Started download of "+result.name, MessageDialogButton.OK)
}
private void viewComment(String infohash) {
UIResultEvent result = model.rootToResult[infohash]
ViewCommentView view = new ViewCommentView(result, terminalSize)
textGUI.addWindowAndWait(view)
}
}

View File

@@ -0,0 +1,86 @@
package com.muwire.clilanterna
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.SplitPattern
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import java.nio.charset.StandardCharsets
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
class SearchModel {
private final TextGUIThread guiThread
private final String query
private final Core core
final TableModel model
private final Map<Persona, UIResultBatchEvent> resultsPerSender = new HashMap<>()
SearchModel(String query, Core core, TextGUIThread guiThread) {
this.query = query
this.core = core
this.guiThread = guiThread
this.model = new TableModel("Sender","Results","Browse","Trust")
core.eventBus.register(UIResultBatchEvent.class, this)
boolean hashSearch = false
byte [] root = null
if (query.length() == 44 && query.indexOf(" ") < 0) {
try {
root = Base64.decode(query)
hashSearch = true
} catch (Exception e) {
// not hash search
}
}
def searchEvent
byte [] payload
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
payload = root
} else {
def replaced = query.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
def terms = replaced.split(" ")
def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
}
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me, sig: sig.data))
}
void unregister() {
core.eventBus.unregister(UIResultBatchEvent.class, this)
}
void onUIResultBatchEvent(UIResultBatchEvent e) {
guiThread.invokeLater {
Persona sender = e.results[0].sender
resultsPerSender.put(sender, e)
String browse = String.valueOf(e.results[0].browse)
String results = String.valueOf(e.results.length)
String trust = core.trustService.getLevel(sender.destination).toString()
model.addRow([new PersonaWrapper(sender), results, browse, trust])
}
}
}

View File

@@ -0,0 +1,113 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
class SearchView extends BasicWindow {
private final Core core
private final SearchModel model
private final Table table
private final TextGUI textGUI
private final TerminalSize terminalSize
SearchView(SearchModel model, Core core, TextGUI textGUI, TerminalSize terminalSize) {
super(model.query)
this.core = core
this.model = model
this.textGUI = textGUI
this.terminalSize = terminalSize
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
table = new Table("Sender","Results","Browse","Trust")
table.setCellSelection(false)
table.setSelectAction({rowSelected()})
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
Button closeButton = new Button("Close", {
model.unregister()
close()
})
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
setComponent(contentPanel)
closeButton.takeFocus()
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def rows = model.model.getRow(selectedRow)
Persona persona = rows[0].persona
boolean browse = Boolean.parseBoolean(rows[2])
Window prompt = new BasicWindow("Show Or Browse "+rows[0]+"?")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(6))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button showResults = new Button("Show Results", {
showResults(persona)
})
Button browseHost = new Button("Browse Host", {
BrowseModel model = new BrowseModel(persona, core, textGUI.getGUIThread())
BrowseView view = new BrowseView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
})
Button trustHost = new Button("Trust",{
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + " has been marked trusted",
MessageDialogButton.OK)
})
Button neutralHost = new Button("Neutral",{
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + " has been marked neutral",
MessageDialogButton.OK)
})
Button distrustHost = new Button("Distrust", {
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + " has been marked distrusted",
MessageDialogButton.OK)
})
Button closePrompt = new Button("Close", {prompt.close()})
contentPanel.with {
addComponent(showResults, layoutData)
if (browse)
addComponent(browseHost, layoutData)
addComponent(trustHost, layoutData)
addComponent(neutralHost, layoutData)
addComponent(distrustHost, layoutData)
addComponent(closePrompt, layoutData)
}
prompt.setComponent(contentPanel)
showResults.takeFocus()
textGUI.addWindowAndWait(prompt)
}
private void showResults(Persona persona) {
def results = model.resultsPerSender.get(persona)
ResultsModel resultsModel = new ResultsModel(results)
ResultsView resultsView = new ResultsView(resultsModel, core, textGUI, terminalSize)
textGUI.addWindowAndWait(resultsView)
}
}

View File

@@ -0,0 +1,16 @@
package com.muwire.clilanterna
import com.muwire.core.SharedFile
class SharedFileWrapper {
private final SharedFile sharedFile
SharedFileWrapper(SharedFile sharedFile) {
this.sharedFile = sharedFile
}
@Override
public String toString() {
sharedFile.getCachedPath()
}
}

View File

@@ -0,0 +1,49 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.trust.RemoteTrustList
import com.muwire.core.trust.TrustEvent
class TrustListModel {
private final TextGUIThread guiThread
private final RemoteTrustList trustList
private final Core core
private final TableModel trustedTableModel, distrustedTableModel
TrustListModel(RemoteTrustList trustList, Core core) {
this.trustList = trustList
this.core = core
trustedTableModel = new TableModel("Trusted User","Your Trust")
distrustedTableModel = new TableModel("Distrusted User", "Your Trust")
refreshModels()
core.eventBus.register(TrustEvent.class, this)
}
void onTrustEvent(TrustEvent e) {
guiThread.invokeLater {
refreshModels()
}
}
private void refreshModels() {
int trustRows = trustedTableModel.getRowCount()
trustRows.times { trustedTableModel.removeRow(0) }
int distrustRows = distrustedTableModel.getRowCount()
distrustRows.times { distrustedTableModel.removeRow(0) }
trustList.good.each {
trustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
}
trustList.bad.each {
distrustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
}
}
void unregister() {
core.eventBus.unregister(TrustEvent.class, this)
}
}

View File

@@ -0,0 +1,118 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
class TrustListView extends BasicWindow {
private final TrustListModel model
private final TextGUI textGUI
private final Core core
private final TerminalSize terminalSize
private final Table trusted, distrusted
TrustListView(TrustListModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
this.model = model
this.textGUI = textGUI
this.core = core
this.terminalSize = terminalSize
int tableSize = terminalSize.getRows() - 10
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
Label nameLabel = new Label("Trust list for "+model.trustList.persona.getHumanReadableName())
Label lastUpdatedLabel = new Label("Last updated "+new Date(model.trustList.timestamp))
contentPanel.addComponent(nameLabel, layoutData)
contentPanel.addComponent(lastUpdatedLabel, layoutData)
Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2))
trusted = new Table("Trusted User","Your Trust")
trusted.with {
setCellSelection(false)
setTableModel(model.trustedTableModel)
setVisibleRows(tableSize)
}
trusted.setSelectAction({ actionsForUser(true) })
topPanel.addComponent(trusted, layoutData)
distrusted = new Table("Distrusted User", "Your Trust")
distrusted.with {
setCellSelection(false)
setTableModel(model.distrustedTableModel)
setVisibleRows(tableSize)
}
distrusted.setSelectAction({actionsForUser(false)})
topPanel.addComponent(distrusted, layoutData)
Button closeButton = new Button("Close",{close()})
contentPanel.addComponent(topPanel, layoutData)
contentPanel.addComponent(closeButton, layoutData)
setComponent(contentPanel)
}
private void actionsForUser(boolean trustedUser) {
def table = trustedUser ? trusted : distrusted
def model = trustedUser ? model.trustedTableModel : model.distrustedTableModel
int selectedRow = table.getSelectedRow()
def row = model.getRow(selectedRow)
Persona persona = row[0].persona
Window prompt = new BasicWindow("Actions for "+persona.getHumanReadableName())
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(4))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button trustButton = new Button("Trust",{
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
MessageDialogButton.OK)
})
Button neutralButton = new Button("Neutral",{
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
MessageDialogButton.OK)
})
Button distrustButton = new Button("Distrust",{
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
MessageDialogButton.OK)
})
Button closeButton = new Button("Close",{prompt.close()})
contentPanel.with {
addComponent(trustButton,layoutData)
addComponent(neutralButton, layoutData)
addComponent(distrustButton, layoutData)
addComponent(closeButton, layoutData)
}
prompt.setComponent(contentPanel)
textGUI.addWindowAndWait(prompt)
}
}

View File

@@ -0,0 +1,15 @@
package com.muwire.clilanterna
import com.muwire.core.trust.RemoteTrustList
class TrustListWrapper {
private final RemoteTrustList trustList
TrustListWrapper(RemoteTrustList trustList) {
this.trustList = trustList
}
@Override
public String toString() {
trustList.persona.getHumanReadableName()
}
}

View File

@@ -0,0 +1,78 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustSubscriptionEvent
import com.muwire.core.trust.TrustSubscriptionUpdatedEvent
class TrustModel {
private final TextGUIThread guiThread
private final Core core
private final TableModel modelTrusted, modelDistrusted, modelSubscriptions
TrustModel(TextGUIThread guiThread, Core core) {
this.guiThread = guiThread
this.core = core
modelTrusted = new TableModel("Trusted Users")
modelDistrusted = new TableModel("Distrusted Users")
modelSubscriptions = new TableModel("Name","Trusted","Distrusted","Status","Last Updated")
core.eventBus.register(TrustEvent.class, this)
core.eventBus.register(AllFilesLoadedEvent.class, this)
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
}
void onTrustEvent(TrustEvent e) {
guiThread.invokeLater {
refreshModels()
}
}
void onTrustSubscriptionUpdatedEvent(TrustSubscriptionUpdatedEvent e) {
guiThread.invokeLater {
refreshModels()
}
}
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
guiThread.invokeLater {
refreshModels()
}
core.muOptions.trustSubscriptions.each {
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
}
}
private void refreshModels() {
int trustedRows = modelTrusted.getRowCount()
trustedRows.times { modelTrusted.removeRow(0) }
int distrustedRows = modelDistrusted.getRowCount()
distrustedRows.times { modelDistrusted.removeRow(0) }
int subsRows = modelSubscriptions.getRowCount()
subsRows.times { modelSubscriptions.removeRow(0) }
core.trustService.good.values().each {
modelTrusted.addRow(new PersonaWrapper(it))
}
core.trustService.bad.values().each {
modelDistrusted.addRow(new PersonaWrapper(it))
}
core.trustSubscriber.remoteTrustLists.values().each {
def name = new TrustListWrapper(it)
String trusted = String.valueOf(it.good.size())
String distrusted = String.valueOf(it.bad.size())
String status = it.status
String lastUpdated = it.timestamp == 0 ? "Never" : new Date(it.timestamp)
modelSubscriptions.addRow(name, trusted, distrusted, status, lastUpdated)
}
}
}

View File

@@ -0,0 +1,204 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustSubscriptionEvent
class TrustView extends BasicWindow {
private final TrustModel model
private final TextGUI textGUI
private final Core core
private final TerminalSize terminalSize
private final Table trusted, distrusted, subscriptions
TrustView(TrustModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
this.model = model
this.textGUI = textGUI
this.core = core
this.terminalSize = terminalSize
int tableSize = (terminalSize.getRows() / 2 - 10).toInteger()
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2))
trusted = new Table("Trusted Users")
trusted.setCellSelection(false)
trusted.setSelectAction({trustedActions()})
trusted.setTableModel(model.modelTrusted)
trusted.setVisibleRows(tableSize)
topPanel.addComponent(trusted, layoutData)
distrusted = new Table("Distrusted users")
distrusted.setCellSelection(false)
distrusted.setSelectAction({distrustedActions()})
distrusted.setTableModel(model.modelDistrusted)
distrusted.setVisibleRows(tableSize)
topPanel.addComponent(distrusted, layoutData)
Panel bottomPanel = new Panel()
bottomPanel.setLayoutManager(new GridLayout(1))
Label tableName = new Label("Trust List Subscriptions")
bottomPanel.addComponent(tableName, layoutData)
subscriptions = new Table("Name","Trusted","Distrusted","Status","Last Updated")
subscriptions.setCellSelection(false)
subscriptions.setSelectAction({trustListActions()})
subscriptions.setTableModel(model.modelSubscriptions)
subscriptions.setVisibleRows(tableSize)
bottomPanel.addComponent(subscriptions, layoutData)
Button closeButton = new Button("Close", {close()})
contentPanel.addComponent(topPanel, layoutData)
contentPanel.addComponent(bottomPanel, layoutData)
contentPanel.addComponent(closeButton, layoutData)
setComponent(contentPanel)
}
private void trustedActions() {
int selectedRow = trusted.getSelectedRow()
def row = model.modelTrusted.getRow(selectedRow)
Persona persona = row[0].persona
Window prompt = new BasicWindow("Change Trust For "+persona.getHumanReadableName())
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(4))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button subscribe = new Button("Subscribe", {
core.muOptions.trustSubscriptions.add(persona)
saveMuSettings()
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : true))
MessageDialog.showMessageDialog(textGUI, "Subscribed", "Subscribed from trust list of " + persona.getHumanReadableName(),
MessageDialogButton.OK)
model.refreshModels()
})
Button markNeutral = new Button("Mark Neutral", {
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
MessageDialogButton.OK)
})
Button markDistrusted = new Button("Mark Distrusted", {
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
MessageDialogButton.OK)
})
Button closeButton = new Button("Close", {prompt.close()})
contentPanel.with {
addComponent(subscribe, layoutData)
addComponent(markNeutral, layoutData)
addComponent(markDistrusted, layoutData)
addComponent(closeButton, layoutData)
}
prompt.setComponent(contentPanel)
textGUI.addWindowAndWait(prompt)
}
private void distrustedActions() {
int selectedRow = trusted.getSelectedRow()
def row = model.modelDistrusted.getRow(selectedRow)
Persona persona = row[0].persona
Window prompt = new BasicWindow("Change Trust For "+persona.getHumanReadableName())
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button markNeutral = new Button("Mark Neutral", {
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
MessageDialogButton.OK)
})
Button markDistrusted = new Button("Mark Trusted", {
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
MessageDialogButton.OK)
})
Button closeButton = new Button("Close", {prompt.close()})
contentPanel.with {
addComponent(markDistrusted, layoutData)
addComponent(markNeutral, layoutData)
addComponent(closeButton, layoutData)
}
prompt.setComponent(contentPanel)
textGUI.addWindowAndWait(prompt)
}
private void trustListActions() {
int selectedRow = subscriptions.getSelectedRow()
def row = model.modelSubscriptions.getRow(selectedRow)
def trustList = row[0].trustList
Persona persona = trustList.persona
Window prompt = new BasicWindow("Trust List Actions")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(4))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button reviewButton = new Button("Review",{review(trustList)})
Button updateButton = new Button("Update",{
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : true))
MessageDialog.showMessageDialog(textGUI, "Updating...", "Trust list will update soon", MessageDialogButton.OK)
})
Button unsubscribeButton = new Button("Unsubscribe", {
core.muOptions.trustSubscriptions.remove(persona)
saveMuSettings()
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : false))
MessageDialog.showMessageDialog(textGUI, "Unsubscribed", "Unsubscribed from trust list of " + persona.getHumanReadableName(),
MessageDialogButton.OK)
model.refreshModels()
})
Button closeButton = new Button("Close", {prompt.close()})
contentPanel.with {
addComponent(reviewButton, layoutData)
addComponent(updateButton, layoutData)
addComponent(unsubscribeButton, layoutData)
addComponent(closeButton, layoutData)
}
prompt.setComponent(contentPanel)
textGUI.addWindowAndWait(prompt)
}
private void review(def trustList) {
TrustListModel model = new TrustListModel(trustList, core)
TrustListView view = new TrustListView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
model.unregister()
}
private void saveMuSettings() {
File settingsFile = new File(core.home,"MuWire.properties")
settingsFile.withPrintWriter("UTF-8",{ core.muOptions.write(it) })
}
}

View File

@@ -0,0 +1,34 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.Window
class UpdateTextView extends BasicWindow {
private final TextBox textBox
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
UpdateTextView(String text, TerminalSize terminalSize) {
super("Update Details")
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
textBox = new TextBox(boxSize, text, TextBox.Style.MULTI_LINE)
contentPanel.addComponent(textBox, layoutData)
Button closeButton = new Button("Close", {close()})
contentPanel.addComponent(closeButton, layoutData)
setComponent(contentPanel)
}
}

View File

@@ -0,0 +1,107 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.upload.UploadEvent
import com.muwire.core.upload.UploadFinishedEvent
import com.muwire.core.upload.Uploader
import net.i2p.data.DataHelper
class UploadsModel {
private final TextGUIThread guiThread
private final Core core
private CliSettings props
private final List<UploaderWrapper> uploaders = new ArrayList<>()
private final TableModel model = new TableModel("Name","Progress","Downloader","Remote Pieces", "Speed")
UploadsModel(TextGUIThread guiThread, Core core, CliSettings props) {
this.guiThread = guiThread
this.core = core
this.props = props
core.eventBus.register(UploadEvent.class, this)
core.eventBus.register(UploadFinishedEvent.class, this)
Timer timer = new Timer(true)
Runnable refreshModel = {refreshModel()}
timer.schedule({
guiThread.invokeLater(refreshModel)
} as TimerTask, 1000, 1000)
}
void onUploadEvent(UploadEvent e) {
guiThread.invokeLater {
UploaderWrapper found = null
uploaders.each {
if (it.uploader == e.uploader) {
found = it
return
}
}
if (found != null) {
found.uploader = e.uploader
found.finished = false
} else
uploaders << new UploaderWrapper(uploader : e.uploader)
}
}
void onUploadFinishedEvent(UploadFinishedEvent e) {
guiThread.invokeLater {
uploaders.each {
if (it.uploader == e.uploader) {
it.finished = true
return
}
}
}
}
private void refreshModel() {
int uploadersSize = model.getRowCount()
uploadersSize.times { model.removeRow(0) }
if (props.clearUploads) {
uploaders.removeAll { it.finished }
}
uploaders.each {
String name = it.uploader.getName()
int percent = it.uploader.getProgress()
String percentString = "$percent% of piece".toString()
String downloader = it.uploader.getDownloader()
int pieces = it.uploader.getTotalPieces()
int done = it.uploader.getDonePieces()
if (percent == 100)
done++
int percentTotal = -1
if (pieces != 0)
percentTotal = (done * 100) / pieces
long size = it.uploader.getTotalSize()
String totalSize = ""
if (size > 0)
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
String remotePieces = String.format("%02d", percentTotal) + "% ${totalSize} ($done/$pieces) pcs".toString()
String speed = DataHelper.formatSize2Decimal(it.uploader.speed(), false) + "B/sec"
model.addRow([name, percentString, downloader, remotePieces, speed])
}
}
private static class UploaderWrapper {
Uploader uploader
boolean finished
@Override
public String toString() {
uploader.getName()
}
}
}

View File

@@ -0,0 +1,49 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.table.Table
class UploadsView extends BasicWindow {
private final UploadsModel model
private final Table table
UploadsView(UploadsModel model, TerminalSize terminalSize) {
this.model = model
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
table = new Table("Name","Progress","Downloader","Remote Pieces","Speed")
table.setCellSelection(false)
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button clearDoneButton = new Button("Clear Finished",{
model.uploaders.removeAll { it.finished }
})
Button closeButton = new Button("Close",{close()})
buttonsPanel.addComponent(clearDoneButton, layoutData)
buttonsPanel.addComponent(closeButton, layoutData)
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel)
closeButton.takeFocus()
}
}

View File

@@ -0,0 +1,39 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.Window
import com.muwire.core.SharedFile
import com.muwire.core.search.UIResultEvent
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class ViewCommentView extends BasicWindow {
private final TextBox textBox
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
ViewCommentView(UIResultEvent result, TerminalSize terminalSize) {
super("View Comments For "+result.getName())
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
textBox = new TextBox(boxSize, result.comment, TextBox.Style.MULTI_LINE)
contentPanel.addComponent(textBox, layoutData)
Button closeButton = new Button("Close", {close()})
contentPanel.addComponent(closeButton, layoutData)
setComponent(contentPanel)
}
}

View File

@@ -35,7 +35,7 @@ class Cli {
Core core
try {
core = new Core(props, home, "0.5.2")
core = new Core(props, home, "0.5.3")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core
try {
core = new Core(props, home, "0.5.2")
core = new Core(props, home, "0.5.3")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@@ -28,10 +28,12 @@ import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService
import com.muwire.core.files.SideCarFileEvent
import com.muwire.core.files.UICommentEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatchedEvent
import com.muwire.core.files.DirectoryWatcher
import com.muwire.core.hostcache.CacheClient
import com.muwire.core.hostcache.HostCache
@@ -101,6 +103,8 @@ public class Core {
private final Router router
final AtomicBoolean shutdown = new AtomicBoolean()
final SigningPrivateKey spk
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
@@ -178,7 +182,7 @@ public class Core {
i2pSession = socketManager.getSession()
def destination = new Destination()
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
spk = new SigningPrivateKey(Constants.SIG_TYPE)
keyDat.withInputStream {
destination.readBytes(it)
def privateKey = new PrivateKey()
@@ -189,8 +193,9 @@ public class Core {
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.write(Constants.PERSONA_VERSION)
daos.writeShort((short)props.getNickname().length())
daos.write(props.getNickname().getBytes(StandardCharsets.UTF_8))
byte [] name = props.getNickname().getBytes(StandardCharsets.UTF_8)
daos.writeShort((short)name.length)
daos.write(name)
destination.writeBytes(daos)
daos.flush()
byte [] payload = baos.toByteArray()
@@ -220,6 +225,7 @@ public class Core {
eventBus.register(SearchEvent.class, fileManager)
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
eventBus.register(UICommentEvent.class, fileManager)
eventBus.register(SideCarFileEvent.class, fileManager)
log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager, home, props)
@@ -275,7 +281,7 @@ public class Core {
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
log.info("initializing upload manager")
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager)
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, props)
log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
@@ -287,7 +293,7 @@ public class Core {
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
eventBus.register(FileSharedEvent.class, directoryWatcher)
eventBus.register(DirectoryWatchedEvent.class, directoryWatcher)
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
@@ -331,6 +337,8 @@ public class Core {
log.info("already shutting down")
return
}
log.info("saving settings")
saveMuSettings()
log.info("shutting down trust subscriber")
trustSubscriber.stop()
log.info("shutting down download manageer")
@@ -349,6 +357,12 @@ public class Core {
log.info("shutting down embedded router")
router.shutdown(0)
}
log.info("shutdown complete")
}
public void saveMuSettings() {
File f = new File(home, "MuWire.properties")
f.withPrintWriter("UTF-8", { muOptions.write(it) })
}
static main(args) {
@@ -375,7 +389,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.5.2")
Core core = new Core(props, home, "0.5.7")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -17,6 +17,8 @@ class MuWireSettings {
int trustListInterval
Set<Persona> trustSubscriptions
int downloadRetryInterval
int totalUploadSlots
int uploadSlotsPerUser
int updateCheckInterval
boolean autoDownloadUpdate
String updateType
@@ -37,6 +39,7 @@ class MuWireSettings {
int inBw, outBw
Set<String> watchedKeywords
Set<String> watchedRegexes
Set<String> negativeFileTree
MuWireSettings() {
this(new Properties())
@@ -72,10 +75,13 @@ class MuWireSettings {
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
watchedDirectories = readEncodedSet(props, "watchedDirectories")
watchedKeywords = readEncodedSet(props, "watchedKeywords")
watchedRegexes = readEncodedSet(props, "watchedRegexes")
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
watchedRegexes = DataUtil.readEncodedSet(props, "watchedRegexes")
negativeFileTree = DataUtil.readEncodedSet(props, "negativeFileTree")
trustSubscriptions = new HashSet<>()
if (props.containsKey("trustSubscriptions")) {
@@ -87,7 +93,7 @@ class MuWireSettings {
}
void write(OutputStream out) throws IOException {
void write(Writer out) throws IOException {
Properties props = new Properties()
props.setProperty("leaf", isLeaf.toString())
props.setProperty("allowUntrusted", allowUntrusted.toString())
@@ -116,10 +122,13 @@ class MuWireSettings {
props.setProperty("searchComments", String.valueOf(searchComments))
props.setProperty("browseFiles", String.valueOf(browseFiles))
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
DataUtil.writeEncodedSet(watchedRegexes, "watchedRegexes", props)
DataUtil.writeEncodedSet(negativeFileTree, "negativeFileTree", props)
if (!trustSubscriptions.isEmpty()) {
String encoded = trustSubscriptions.stream().
@@ -128,25 +137,7 @@ class MuWireSettings {
props.setProperty("trustSubscriptions", encoded)
}
props.store(out, "")
}
private static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new ConcurrentHashSet<>()
if (props.containsKey(property)) {
String[] encoded = props.getProperty(property).split(",")
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
}
rv
}
private static void writeEncodedSet(Set<String> set, String property, Properties props) {
if (set.isEmpty())
return
String encoded = set.stream().
map({Base64.encode(DataUtil.encodei18nString(it))}).
collect(Collectors.joining(","))
props.setProperty(property, encoded)
props.store(out, "This file is UTF-8")
}
boolean isLeaf() {

View File

@@ -22,8 +22,9 @@ public class Name {
public void write(OutputStream out) throws IOException {
DataOutputStream dos = new DataOutputStream(out)
dos.writeShort(name.length())
dos.write(name.getBytes(StandardCharsets.UTF_8))
byte [] bytes = name.getBytes(StandardCharsets.UTF_8)
dos.writeShort(bytes.length)
dos.write(bytes)
}
public getName() {

View File

@@ -1,10 +1,17 @@
package com.muwire.core.connection
import java.nio.charset.StandardCharsets
import java.util.concurrent.BlockingQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
@@ -16,12 +23,14 @@ import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.data.Signature
@Log
abstract class Connection implements Closeable {
private static final int SEARCHES = 10
private static final long INTERVAL = 1000
@@ -83,6 +92,7 @@ abstract class Connection implements Closeable {
reader.interrupt()
writer.interrupt()
endpoint.close()
log.info("closed $name")
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
}
@@ -91,6 +101,7 @@ abstract class Connection implements Closeable {
while(running.get()) {
read()
}
} catch (InterruptedException ok) {
} catch (SocketTimeoutException e) {
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in reader",e)
@@ -107,6 +118,7 @@ abstract class Connection implements Closeable {
def message = messages.take()
write(message)
}
} catch (InterruptedException ok) {
} catch (Exception e) {
log.log(Level.WARNING, "unhandled exception in writer",e)
} finally {
@@ -139,6 +151,8 @@ abstract class Connection implements Closeable {
query.replyTo = e.replyTo.toBase64()
if (e.originator != null)
query.originator = e.originator.toBase64()
if (e.sig != null)
query.sig = Base64.encode(e.sig)
messages.put(query)
}
@@ -217,6 +231,24 @@ abstract class Connection implements Closeable {
boolean compressedResults = false
if (search.compressedResults != null)
compressedResults = search.compressedResults
byte[] sig = null
// TODO: make this mandatory at some point
if (search.sig != null) {
sig = Base64.decode(search.sig)
byte [] payload
if (infohash != null)
payload = infohash
else
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig)
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
log.info("signature didn't match keywords")
return
} else
log.info("query signature verified")
} else
log.info("no signature in query")
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash,
@@ -228,7 +260,8 @@ abstract class Connection implements Closeable {
replyTo : replyTo,
originator : originator,
receivedOn : endpoint.destination,
firstHop : search.firstHop )
firstHop : search.firstHop,
sig : sig )
eventBus.publish(event)
}

View File

@@ -50,6 +50,8 @@ class ConnectionAcceptor {
final ExecutorService handshakerThreads
private volatile shutdown
private volatile int browsed
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
@@ -339,7 +341,8 @@ class ConnectionAcceptor {
return
}
browsed++
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
def sharedFiles = fileManager.getSharedFiles().values()
@@ -349,6 +352,7 @@ class ConnectionAcceptor {
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each {
it.hit()
def obj = ResultsSender.sharedFileToObj(it, false)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())

View File

@@ -62,7 +62,7 @@ class ConnectionEstablisher {
void stop() {
timer.cancel()
executor.shutdownNow()
closer.shutdown()
closer.shutdownNow()
}
private void connectIfNeeded() {
@@ -123,10 +123,12 @@ class ConnectionEstablisher {
}
private void fail(Endpoint endpoint) {
closer.execute {
endpoint.close()
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
} as Runnable
if (!closer.isShutdown()) {
closer.execute {
endpoint.close()
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
} as Runnable
}
}
private void readK(Endpoint e) {

View File

@@ -36,9 +36,8 @@ abstract class ConnectionManager {
timer.schedule({sendPings()} as TimerTask, 1000,1000)
}
void stop() {
void shutdown() {
timer.cancel()
getConnections().each { it.close() }
}
void onTrustEvent(TrustEvent e) {
@@ -62,8 +61,6 @@ abstract class ConnectionManager {
abstract void onDisconnectionEvent(DisconnectionEvent e)
abstract void shutdown()
protected void sendPings() {
final long now = System.currentTimeMillis()
getConnections().each {

View File

@@ -31,9 +31,6 @@ class Endpoint implements Closeable {
if (inputStream != null) {
try {inputStream.close()} catch (Exception ignore) {}
}
if (outputStream != null) {
try {outputStream.close()} catch (Exception ignore) {}
}
if (toClose != null) {
try {toClose.reset()} catch (Exception ignore) {}
}

View File

@@ -104,6 +104,7 @@ class UltrapeerConnectionManager extends ConnectionManager {
@Override
void shutdown() {
super.shutdown()
peerConnections.values().stream().parallel().forEach({v -> v.close()})
leafConnections.values().stream().parallel().forEach({v -> v.close()})
peerConnections.clear()

View File

@@ -12,6 +12,7 @@ import com.muwire.core.util.DataUtil
import groovy.json.JsonBuilder
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
@@ -25,7 +26,9 @@ import com.muwire.core.UILoadedEvent
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.logging.Level
@Log
public class DownloadManager {
private final EventBus eventBus
@@ -135,22 +138,33 @@ public class DownloadManager {
else
incompletes = new File(home, "incompletes")
if (json.pieceSizePow2 == null || json.pieceSizePow2 == 0) {
log.warning("Skipping $file because pieceSizePow2=$json.pieceSizePow2")
return // skip this download as it's corrupt anyway
}
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
if (json.paused != null)
downloader.paused = json.paused
downloaders.put(infoHash, downloader)
downloader.readPieces()
if (!downloader.paused)
downloader.download()
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
try {
downloader.readPieces()
if (!downloader.paused)
downloader.download()
downloaders.put(infoHash, downloader)
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
} catch (IllegalArgumentException bad) {
log.log(Level.WARNING,"cannot start downloader, skipping", bad)
return
}
}
}
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
int pieceSize = 0x1 << pieceSizePow2
long pieceSize = 0x1L << pieceSizePow2
int nPieces = (int)(length / pieceSize)
if (length % pieceSize != 0)
nPieces++

View File

@@ -21,6 +21,7 @@ import java.nio.file.Files
import java.nio.file.StandardOpenOption
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Level
@Log
@@ -37,13 +38,12 @@ class DownloadSession {
private final Set<Integer> available
private final MessageDigest digest
private long lastSpeedRead = System.currentTimeMillis()
private long dataSinceLastRead
private final AtomicLong dataSinceLastRead
private MappedByteBuffer mapped
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength, Set<Integer> available) {
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead) {
this.eventBus = eventBus
this.meB64 = meB64
this.pieces = pieces
@@ -53,6 +53,7 @@ class DownloadSession {
this.pieceSize = pieceSize
this.fileLength = fileLength
this.available = available
this.dataSinceLastRead = dataSinceLastRead
try {
digest = MessageDigest.getInstance("SHA-256")
} catch (NoSuchAlgorithmException impossible) {
@@ -141,6 +142,8 @@ class DownloadSession {
// parse X-Have if present
if (headers.containsKey("X-Have")) {
DataUtil.decodeXHave(headers["X-Have"]).each {
if (it >= pieces.nPieces)
throw new IOException("Invalid X-Have header, available piece $it/$pieces.nPieces")
available.add(it)
}
if (!available.contains(piece))
@@ -188,7 +191,7 @@ class DownloadSession {
throw new IOException()
synchronized(this) {
mapped.put(tmp, 0, read)
dataSinceLastRead += read
dataSinceLastRead.addAndGet(read)
pieces.markPartial(piece, mapped.position())
}
}
@@ -220,13 +223,4 @@ class DownloadSession {
return 0
mapped.position()
}
synchronized int speed() {
final long now = System.currentTimeMillis()
long interval = Math.max(1000, now - lastSpeedRead)
lastSpeedRead = now;
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
dataSinceLastRead = 0
rv
}
}

View File

@@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Level
import com.muwire.core.Constants
@@ -61,10 +62,11 @@ public class Downloader {
private final AtomicBoolean eventFired = new AtomicBoolean()
private boolean piecesFileClosed
private final AtomicLong dataSinceLastRead = new AtomicLong(0)
private volatile long lastSpeedRead = System.currentTimeMillis()
private ArrayList speedArr = new ArrayList<Integer>()
private int speedPos = 0
private int speedAvg = 0
private long timestamp = Instant.now().toEpochMilli()
public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash,
@@ -139,10 +141,11 @@ public class Downloader {
public int speed() {
int currSpeed = 0
if (getCurrentState() == DownloadState.DOWNLOADING) {
activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING)
currSpeed += it.speed()
}
long dataRead = dataSinceLastRead.getAndSet(0)
long now = System.currentTimeMillis()
if (now > lastSpeedRead)
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
lastSpeedRead = now
}
if (speedArr.size() != downloadManager.muSettings.speedSmoothSeconds) {
@@ -302,7 +305,7 @@ public class Downloader {
boolean requestPerformed
while(!pieces.isComplete()) {
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
endpoint, incompleteFile, pieceSize, length, available)
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead)
requestPerformed = currentSession.request()
if (!requestPerformed)
break
@@ -339,12 +342,6 @@ public class Downloader {
}
}
int speed() {
if (currentSession == null)
return 0
currentSession.speed()
}
void cancel() {
downloadThread?.interrupt()
}

View File

@@ -75,6 +75,8 @@ class Pieces {
}
synchronized void markDownloaded(int piece) {
if (piece >= nPieces)
throw new IllegalArgumentException("invalid piece marked as downloaded? $piece/$nPieces")
done.set(piece)
claimed.set(piece)
partials.remove(piece)

View File

@@ -4,4 +4,8 @@ import com.muwire.core.Event
class DirectoryUnsharedEvent extends Event {
File directory
public String toString() {
super.toString() + " unshared directory "+ directory.toString()
}
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.files
import com.muwire.core.Event
class DirectoryWatchedEvent extends Event {
File directory
}

View File

@@ -66,10 +66,8 @@ class DirectoryWatcher {
watchService?.close()
}
void onFileSharedEvent(FileSharedEvent e) {
if (!e.file.isDirectory())
return
File canonical = e.file.getCanonicalFile()
void onDirectoryWatchedEvent(DirectoryWatchedEvent e) {
File canonical = e.directory.getCanonicalFile()
Path path = canonical.toPath()
WatchKey wk = path.register(watchService, kinds)
watchedDirectories.put(canonical, wk)
@@ -88,9 +86,9 @@ class DirectoryWatcher {
private void saveMuSettings() {
File muSettingsFile = new File(home, "MuWire.properties")
muSettingsFile.withOutputStream {
muSettingsFile.withPrintWriter("UTF-8", {
muOptions.write(it)
}
})
}
private void watch() {
@@ -125,7 +123,8 @@ class DirectoryWatcher {
private void processModified(Path parent, Path path) {
File f = join(parent, path)
log.fine("modified entry $f")
waitingFiles.put(f, System.currentTimeMillis())
if (!fileManager.getNegativeTree().fileToNode.containsKey(f))
waitingFiles.put(f, System.currentTimeMillis())
}
private void processDeleted(Path parent, Path path) {
@@ -133,7 +132,7 @@ class DirectoryWatcher {
log.fine("deleted entry $f")
SharedFile sf = fileManager.fileToSharedFile.get(f)
if (sf != null)
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
}
private static File join(Path parent, Path path) {

View File

@@ -13,8 +13,10 @@ import java.security.NoSuchAlgorithmException
class FileHasher {
public static final int MIN_PIECE_SIZE_POW2 = 17
public static final int MAX_PIECE_SIZE_POW2 = 37
/** max size of shared file is 128 GB */
public static final long MAX_SIZE = 0x1L << 37
public static final long MAX_SIZE = 0x1L << MAX_PIECE_SIZE_POW2
/**
* @param size of the file to be shared
@@ -24,9 +26,9 @@ class FileHasher {
*/
static int getPieceSize(long size) {
if (size <= 0x1 << 30)
return 17
return MIN_PIECE_SIZE_POW2
for (int i = 31; i <= 37; i++) {
for (int i = 31; i <= MAX_PIECE_SIZE_POW2; i++) {
if (size <= 0x1L << i) {
return i-13
}
@@ -48,27 +50,28 @@ class FileHasher {
InfoHash hashFile(File file) {
final long length = file.length()
final int size = 0x1 << getPieceSize(length)
int numPieces = (int) (length / size)
final long size = 0x1L << getPieceSize(length)
int numPieces = (length / size).toInteger()
if (numPieces * size < length)
numPieces++
def output = new ByteArrayOutputStream()
RandomAccessFile raf = new RandomAccessFile(file, "r")
MappedByteBuffer buf = null
try {
MappedByteBuffer buf
for (int i = 0; i < numPieces - 1; i++) {
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
buf = raf.getChannel().map(MapMode.READ_ONLY, size * i, size.toInteger())
digest.update buf
DataUtil.tryUnmap(buf)
output.write(digest.digest(), 0, 32)
}
def lastPieceLength = length - (numPieces - 1) * ((long)size)
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
long lastPieceLength = length - (numPieces - 1) * size
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength.toInteger())
digest.update buf
output.write(digest.digest(), 0, 32)
} finally {
raf.close()
DataUtil.tryUnmap(buf)
}
byte [] hashList = output.toByteArray()

View File

@@ -24,15 +24,28 @@ class FileManager {
final Map<String, Set<File>> nameToFiles = new HashMap<>()
final Map<String, Set<File>> commentToFile = new HashMap<>()
final SearchIndex index = new SearchIndex()
final FileTree negativeTree = new FileTree()
final Set<File> sideCarFiles = new HashSet<>()
FileManager(EventBus eventBus, MuWireSettings settings) {
this.settings = settings
this.eventBus = eventBus
for (String negative : settings.negativeFileTree) {
negativeTree.add(new File(negative))
}
}
void onFileHashedEvent(FileHashedEvent e) {
if (e.sharedFile != null)
addToIndex(e.sharedFile)
if (e.sharedFile == null)
return
File f = e.sharedFile.getFile()
if (sideCarFiles.remove(f)) {
File sideCar = new File(f.getParentFile(), f.getName() + ".mwcomment")
if (sideCar.exists())
e.sharedFile.setComment(Base64.encode(DataUtil.encodei18nString(sideCar.text)))
}
addToIndex(e.sharedFile)
}
void onFileLoadedEvent(FileLoadedEvent e) {
@@ -44,6 +57,21 @@ class FileManager {
addToIndex(e.downloadedFile)
}
}
void onSideCarFileEvent(SideCarFileEvent e) {
String name = e.file.getName()
name = name.substring(0, name.length() - ".mwcomment".length())
File target = new File(e.file.getParentFile(), name)
SharedFile existing = fileToSharedFile.get(target)
if (existing == null) {
sideCarFiles.add(target)
return
}
String comment = Base64.encode(DataUtil.encodei18nString(e.file.text))
String oldComment = existing.getComment()
existing.setComment(comment)
eventBus.publish(new UICommentEvent(oldComment : oldComment, sharedFile : existing))
}
private void addToIndex(SharedFile sf) {
log.info("Adding shared file " + sf.getFile())
@@ -56,6 +84,13 @@ class FileManager {
}
existing.add(sf)
fileToSharedFile.put(sf.file, sf)
negativeTree.remove(sf.file)
String parent = sf.getFile().getParent()
if (parent != null && settings.watchedDirectories.contains(parent)) {
negativeTree.add(sf.file.getParentFile())
}
saveNegativeTree()
String name = sf.getFile().getName()
Set<File> existingFiles = nameToFiles.get(name)
@@ -92,6 +127,10 @@ class FileManager {
}
fileToSharedFile.remove(sf.file)
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
negativeTree.add(sf.file)
saveNegativeTree()
}
String name = sf.getFile().getName()
Set<File> existingFiles = nameToFiles.get(name)
@@ -158,8 +197,10 @@ class FileManager {
Set<SharedFile> found
found = rootToFiles.get new InfoHash(e.searchHash)
found = filter(found, e.oobInfohash)
if (found != null && !found.isEmpty())
if (found != null && !found.isEmpty()) {
found.each { it.hit() }
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
}
} else {
def names = index.search e.searchTerms
Set<File> files = new HashSet<>()
@@ -172,8 +213,10 @@ class FileManager {
files.each { sharedFiles.add fileToSharedFile[it] }
files = filter(sharedFiles, e.oobInfohash)
if (!sharedFiles.isEmpty())
if (!sharedFiles.isEmpty()) {
sharedFiles.each { it.hit() }
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
}
}
@@ -191,8 +234,10 @@ class FileManager {
}
rv
}
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
negativeTree.remove(e.directory)
saveNegativeTree()
e.directory.listFiles().each {
if (it.isDirectory())
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
@@ -203,4 +248,9 @@ class FileManager {
}
}
}
private void saveNegativeTree() {
settings.negativeFileTree.clear()
settings.negativeFileTree.addAll(negativeTree.fileToNode.keySet().collect { it.getAbsolutePath() })
}
}

View File

@@ -0,0 +1,64 @@
package com.muwire.core.files
import java.util.concurrent.ConcurrentHashMap
class FileTree {
private final TreeNode root = new TreeNode()
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
void add(File file) {
List<File> path = new ArrayList<>()
path.add(file)
while (file.getParentFile() != null) {
path.add(file.getParentFile())
file = file.getParentFile()
}
Collections.reverse(path)
TreeNode current = root
for (File element : path) {
TreeNode existing = fileToNode.get(element)
if (existing == null) {
existing = new TreeNode()
existing.file = element
existing.parent = current
fileToNode.put(element, existing)
current.children.add(existing)
}
current = existing
}
}
boolean remove(File file) {
TreeNode node = fileToNode.remove(file)
if (node == null) {
return false
}
node.parent.children.remove(node)
if (node.parent.children.isEmpty() && node.parent != root)
remove(node.parent.file)
def copy = new ArrayList(node.children)
for (TreeNode child : copy)
remove(child.file)
true
}
public static class TreeNode {
TreeNode parent
File file
final Set<TreeNode> children = new HashSet<>()
public int hashCode() {
file.hashCode()
}
public boolean equals(Object o) {
if (!(o instanceof TreeNode))
return false
TreeNode other = (TreeNode)o
file == other.file
}
}
}

View File

@@ -5,4 +5,5 @@ import com.muwire.core.SharedFile
class FileUnsharedEvent extends Event {
SharedFile unsharedFile
boolean deleted
}

View File

@@ -3,6 +3,7 @@ package com.muwire.core.files
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
@@ -33,7 +34,12 @@ class HasherService {
return
if (fileManager.fileToSharedFile.containsKey(canonical))
return
if (hashed.add(canonical))
if (canonical.isFile() && fileManager.negativeTree.fileToNode.containsKey(canonical))
return
if (canonical.getName().endsWith(".mwcomment")) {
if (canonical.length() <= Constants.MAX_COMMENT_LENGTH)
eventBus.publish(new SideCarFileEvent(file : canonical))
} else if (hashed.add(canonical))
executor.execute( { -> process(canonical) } as Runnable)
}
@@ -47,7 +53,10 @@ class HasherService {
private void process(File f) {
if (f.isDirectory()) {
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
eventBus.publish(new DirectoryWatchedEvent(directory : f))
f.listFiles().each {
eventBus.publish new FileSharedEvent(file: it)
}
} else {
if (f.length() == 0) {
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")

View File

@@ -82,7 +82,7 @@ class PersisterService extends Service {
} else {
listener.publish(new AllFilesLoadedEvent())
}
timer.schedule({persistFiles()} as TimerTask, 0, interval)
timer.schedule({persistFiles()} as TimerTask, 1000, interval)
loaded = true
}
@@ -129,9 +129,15 @@ class PersisterService extends Service {
return new FileLoadedEvent(loadedFile : df)
}
int hits = 0
if (json.hits != null)
hits = json.hits
SharedFile sf = new SharedFile(file, ih, pieceSize)
sf.setComment(json.comment)
sf.hits = hits
if (json.downloaders != null)
sf.getDownloaders().addAll(json.downloaders)
return new FileLoadedEvent(loadedFile: sf)
}
@@ -163,6 +169,8 @@ class PersisterService extends Service {
json.pieceSize = sf.getPieceSize()
json.hashList = sf.getB64EncodedHashList()
json.comment = sf.getComment()
json.hits = sf.getHits()
json.downloaders = sf.getDownloaders()
if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())

View File

@@ -0,0 +1,12 @@
package com.muwire.core.files
import com.muwire.core.Event
class SideCarFileEvent extends Event {
File file
@Override
public String toString() {
return super.toString() + " file: "+file.getAbsolutePath()
}
}

View File

@@ -1,5 +1,6 @@
package com.muwire.core.mesh
import java.util.logging.Level
import java.util.stream.Collectors
import com.muwire.core.Constants
@@ -13,8 +14,10 @@ import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.data.Base64
@Log
class MeshManager {
private final Map<InfoHash, Mesh> meshes = Collections.synchronizedMap(new HashMap<>())
@@ -67,7 +70,10 @@ class MeshManager {
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
json.nPieces = mesh.pieces.nPieces
json.xHave = DataUtil.encodeXHave(mesh.pieces.downloaded, mesh.pieces.nPieces)
List<Integer> downloaded = mesh.pieces.getDownloaded()
if( downloaded.size() > mesh.pieces.nPieces)
return
json.xHave = DataUtil.encodeXHave(downloaded, mesh.pieces.nPieces)
writer.println(JsonOutput.toJson(json))
}
}
@@ -82,6 +88,9 @@ class MeshManager {
JsonSlurper slurper = new JsonSlurper()
meshFile.eachLine {
def json = slurper.parseText(it)
if (json.nPieces == null || json.nPieces == 0)
return // skip it, invalid
if (now - json.timestamp > settings.meshExpiration * 60 * 1000)
return
InfoHash infoHash = new InfoHash(Base64.decode(json.infoHash))
@@ -93,8 +102,13 @@ class MeshManager {
mesh.sources.add(persona)
}
if (json.xHave != null)
DataUtil.decodeXHave(json.xHave).each { pieces.markDownloaded(it) }
if (json.xHave != null) {
try {
DataUtil.decodeXHave(json.xHave).each { pieces.markDownloaded(it) }
} catch (IllegalArgumentException bad) {
log.log(Level.WARNING, "couldn't parse XHave", bad)
}
}
if (!mesh.sources.isEmpty())
meshes.put(infoHash, mesh)

View File

@@ -12,6 +12,7 @@ class QueryEvent extends Event {
Destination replyTo
Persona originator
Destination receivedOn
byte[] sig
String toString() {
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +

View File

@@ -6,6 +6,7 @@ import javax.naming.directory.InvalidSearchControlsException
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
@@ -30,12 +31,12 @@ class ResultsParser {
private static parseV1(Persona p, UUID uuid, def json) {
if (json.name == null)
throw new InvalidSearchResultException("name missing")
if (json.size == null)
throw new InvalidSearchResultException("length missing")
if (json.size == null || json.size <= 0 || json.size > FileHasher.MAX_SIZE)
throw new InvalidSearchResultException("length missing or invalid, $json.size")
if (json.infohash == null)
throw new InvalidSearchResultException("infohash missing")
if (json.pieceSize == null)
throw new InvalidSearchResultException("pieceSize missing")
if (json.pieceSize == null || json.pieceSize < FileHasher.MIN_PIECE_SIZE_POW2 || json.pieceSize > FileHasher.MAX_PIECE_SIZE_POW2)
throw new InvalidSearchResultException("pieceSize missing or invalid, $json.pieceSize")
if (!(json.hashList instanceof List))
throw new InvalidSearchResultException("hashlist not a list")
try {
@@ -71,12 +72,12 @@ class ResultsParser {
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
if (json.name == null)
throw new InvalidSearchResultException("name missing")
if (json.size == null)
throw new InvalidSearchResultException("length missing")
if (json.size == null || json.size <= 0 || json.size > FileHasher.MAX_SIZE)
throw new InvalidSearchResultException("length missing or invalid $json.size")
if (json.infohash == null)
throw new InvalidSearchResultException("infohash missing")
if (json.pieceSize == null)
throw new InvalidSearchResultException("pieceSize missing")
if (json.pieceSize == null || json.pieceSize < FileHasher.MIN_PIECE_SIZE_POW2 || json.pieceSize > FileHasher.MAX_PIECE_SIZE_POW2)
throw new InvalidSearchResultException("pieceSize missing or invalid, $json.pieceSize")
if (json.hashList != null)
throw new InvalidSearchResultException("V2 result with hashlist")
try {

View File

@@ -7,4 +7,5 @@ class UpdateAvailableEvent extends Event {
String version
String signer
String infoHash
String text
}

View File

@@ -40,6 +40,8 @@ class UpdateClient {
private volatile InfoHash updateInfoHash
private volatile String version, signer
private volatile boolean updateDownloading
private volatile String text
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings, FileManager fileManager, Persona me) {
this.eventBus = eventBus
@@ -75,7 +77,7 @@ class UpdateClient {
if (e.downloadedFile.infoHash != updateInfoHash)
return
updateDownloading = false
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer))
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))
}
private void checkUpdate() {
@@ -147,15 +149,16 @@ class UpdateClient {
} else
infoHash = payload[settings.updateType]
text = payload.text
if (!settings.autoDownloadUpdate) {
log.info("new version $payload.version available, publishing event")
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : infoHash))
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : infoHash, text : text))
} else {
log.info("new version $payload.version available")
updateInfoHash = new InfoHash(Base64.decode(infoHash))
if (fileManager.rootToFiles.containsKey(updateInfoHash))
eventBus.publish(new UpdateDownloadedEvent(version : payload.version, signer : payload.signer))
eventBus.publish(new UpdateDownloadedEvent(version : payload.version, signer : payload.signer, text : text))
else {
updateDownloading = false
version = payload.version

View File

@@ -5,4 +5,5 @@ import com.muwire.core.Event
class UpdateDownloadedEvent extends Event {
String version
String signer
String text
}

View File

@@ -20,6 +20,8 @@ class ContentUploader extends Uploader {
private final ContentRequest request
private final Mesh mesh
private final int pieceSize
private volatile boolean done
ContentUploader(File file, ContentRequest request, Endpoint endpoint, Mesh mesh, int pieceSize) {
super(endpoint)
@@ -62,20 +64,23 @@ class ContentUploader extends Uploader {
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
byte [] tmp = new byte[0x1 << 13]
while(mapped.hasRemaining()) {
int start = mapped.position()
int read
synchronized(this) {
int start = mapped.position()
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
read = mapped.position() - start
dataSinceLastRead += read
}
int read = mapped.position() - start
endpoint.getOutputStream().write(tmp, 0, read)
}
done = true
} finally {
try {channel?.close() } catch (IOException ignored) {}
endpoint.getOutputStream().flush()
synchronized(this) {
DataUtil.tryUnmap(mapped)
mapped = null
}
endpoint.getOutputStream().flush()
}
}
@@ -98,7 +103,7 @@ class ContentUploader extends Uploader {
@Override
public synchronized int getProgress() {
if (mapped == null)
return 0
return done ? 100 : 0
int position = mapped.position()
int total = request.getRange().end - request.getRange().start
(int)(position * 100.0 / total)
@@ -123,4 +128,13 @@ class ContentUploader extends Uploader {
public long getTotalSize() {
return file.length();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ContentUploader))
return false
ContentUploader other = (ContentUploader)o
request.infoHash == other.request.infoHash &&
request.getDownloader() == other.request.getDownloader()
}
}

View File

@@ -26,11 +26,13 @@ class HashListUploader extends Uploader {
byte[]tmp = new byte[0x1 << 13]
while(mapped.hasRemaining()) {
int start = mapped.position()
int read
synchronized(this) {
int start = mapped.position()
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
read = mapped.position() - start
dataSinceLastRead += read
}
int read = mapped.position() - start
endpoint.getOutputStream().write(tmp, 0, read)
}
endpoint.getOutputStream().flush()
@@ -65,4 +67,12 @@ class HashListUploader extends Uploader {
public long getTotalSize() {
return -1;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof HashListUploader))
return false
HashListUploader other = (HashListUploader)o
infoHash == other.infoHash && request.downloader == other.request.downloader
}
}

View File

@@ -4,6 +4,8 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.SharedFile
import com.muwire.core.connection.Endpoint
import com.muwire.core.download.DownloadManager
@@ -22,15 +24,22 @@ public class UploadManager {
private final FileManager fileManager
private final MeshManager meshManager
private final DownloadManager downloadManager
private final MuWireSettings props
/** LOCKING: this on both structures */
private int totalUploads
private final Map<Persona, Integer> uploadsPerUser = new HashMap<>()
public UploadManager() {}
public UploadManager(EventBus eventBus, FileManager fileManager,
MeshManager meshManager, DownloadManager downloadManager) {
MeshManager meshManager, DownloadManager downloadManager,
MuWireSettings props) {
this.eventBus = eventBus
this.fileManager = fileManager
this.meshManager = meshManager
this.downloadManager = downloadManager
this.props = props
}
public void processGET(Endpoint e) throws IOException {
@@ -82,7 +91,15 @@ public class UploadManager {
if (request.have > 0)
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
if (!incrementUploads(request.downloader)) {
log.info("rejecting due to slot limit")
e.getOutputStream().write("429 Too Many Requests\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
e.close()
return
}
Mesh mesh
File file
int pieceSize
@@ -91,6 +108,7 @@ public class UploadManager {
file = downloader.incompleteFile
pieceSize = downloader.pieceSizePow2
} else {
sharedFiles.each { it.getDownloaders().add(request.downloader.getHumanReadableName()) }
SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
file = sharedFile.file
@@ -102,6 +120,7 @@ public class UploadManager {
try {
uploader.respond()
} finally {
decrementUploads(request.downloader)
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
}
}
@@ -156,12 +175,21 @@ public class UploadManager {
return
}
}
if (!incrementUploads(request.downloader)) {
log.info("rejecting due to slot limit")
e.getOutputStream().write("429 Too Many Requests\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
e.close()
return
}
Uploader uploader = new HashListUploader(e, fullInfoHash, request)
eventBus.publish(new UploadEvent(uploader : uploader))
try {
uploader.respond()
} finally {
decrementUploads(request.downloader)
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
}
@@ -216,6 +244,7 @@ public class UploadManager {
file = downloader.incompleteFile
pieceSize = downloader.pieceSizePow2
} else {
sharedFiles.each { it.getDownloaders().add(request.downloader.getHumanReadableName()) }
SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
file = sharedFile.file
@@ -231,5 +260,37 @@ public class UploadManager {
}
}
}
/**
* @param p downloader
* @return true if this upload hasn't hit any slot limits
*/
private synchronized boolean incrementUploads(Persona p) {
if (props.totalUploadSlots >= 0 && totalUploads >= props.totalUploadSlots)
return false
if (props.uploadSlotsPerUser == 0)
return false
Integer currentUploads = uploadsPerUser.get(p)
if (currentUploads == null)
currentUploads = 0
if (props.uploadSlotsPerUser > 0 && currentUploads >= props.uploadSlotsPerUser)
return false
uploadsPerUser.put(p, ++currentUploads)
totalUploads++
true
}
private synchronized void decrementUploads(Persona p) {
totalUploads--
Integer currentUploads = uploadsPerUser.get(p)
if (currentUploads == null || currentUploads == 0)
throw new IllegalStateException()
currentUploads--
if (currentUploads == 0)
uploadsPerUser.remove(p)
else
uploadsPerUser.put(p, currentUploads)
}
}

View File

@@ -11,7 +11,13 @@ import com.muwire.core.connection.Endpoint
abstract class Uploader {
protected final Endpoint endpoint
protected ByteBuffer mapped
private long lastSpeedRead
protected int dataSinceLastRead
private final ArrayList<Integer> speedArr = [0,0,0,0,0]
private int speedPos, speedAvg
Uploader(Endpoint endpoint) {
this.endpoint = endpoint
}
@@ -38,4 +44,34 @@ abstract class Uploader {
abstract int getTotalPieces();
abstract long getTotalSize();
synchronized int speed() {
final long now = System.currentTimeMillis()
long interval = Math.max(1000, now - lastSpeedRead)
lastSpeedRead = now;
int currSpeed = (int) (dataSinceLastRead * 1000.0 / interval)
dataSinceLastRead = 0
// normalize to speedArr.size
currSpeed /= speedArr.size()
// compute new speedAvg and update speedArr
if ( speedArr[speedPos] > speedAvg ) {
speedAvg = 0
} else {
speedAvg -= speedArr[speedPos]
}
speedAvg += currSpeed
speedArr[speedPos] = currSpeed
// this might be necessary due to rounding errors
if (speedAvg < 0)
speedAvg = 0
// rolling index over the speedArr
speedPos++
if (speedPos >= speedArr.size())
speedPos=0
speedAvg
}
}

View File

@@ -10,4 +10,6 @@ public class Constants {
public static final int MAX_HEADERS = 16;
public static final int MAX_RESULTS = 0x1 << 16;
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
}

View File

@@ -3,7 +3,10 @@ package com.muwire.core;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.muwire.core.util.DataUtil;
@@ -23,6 +26,8 @@ public class SharedFile {
private final List<String> b64EncodedHashList;
private volatile String comment;
private volatile int hits;
private final Set<String> downloaders = Collections.synchronizedSet(new HashSet<>());
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
this.file = file;
@@ -90,6 +95,22 @@ public class SharedFile {
public String getComment() {
return comment;
}
public int getHits() {
return hits;
}
public void hit() {
hits++;
}
public Set<String> getDownloaders() {
return downloaders;
}
public void addDownloader(String name) {
downloaders.add(name);
}
@Override
public int hashCode() {

View File

@@ -11,10 +11,14 @@ import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import com.muwire.core.Constants;
import net.i2p.data.Base64;
import net.i2p.util.ConcurrentHashSet;
public class DataUtil {
@@ -165,4 +169,22 @@ public class DataUtil {
} catch(Exception ex) { }
cb = null;
}
public static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new ConcurrentHashSet<>();
if (props.containsKey(property)) {
String [] encoded = props.getProperty(property).split(",");
for(String s : encoded)
rv.add(readi18nString(Base64.decode(s)));
}
return rv;
}
public static void writeEncodedSet(Set<String> set, String property, Properties props) {
if (set.isEmpty())
return;
String encoded = set.stream().map(s -> Base64.encode(encodei18nString(s)))
.collect(Collectors.joining(","));
props.setProperty(property, encoded);
}
}

View File

@@ -95,7 +95,7 @@ class ConnectionAcceptorTest {
connectionEstablisher = connectionEstablisherMock.proxyInstance()
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher)
acceptor.start()
Thread.sleep(100)
}

View File

@@ -2,6 +2,8 @@ package com.muwire.core.download
import static org.junit.Assert.fail
import java.util.concurrent.atomic.AtomicLong
import org.junit.After
import org.junit.Before
import org.junit.Ignore
@@ -76,7 +78,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available)
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available, new AtomicLong())
downloadThread = new Thread( { perform() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()

View File

@@ -0,0 +1,42 @@
package com.muwire.core.files
import org.junit.Test
class FileTreeTest {
@Test
public void testRemoveEmtpyDirs() {
File a = new File("a")
File b = new File(a, "b")
File c = new File(b, "c")
FileTree tree = new FileTree()
tree.add(c)
assert tree.root.children.size() == 1
assert tree.fileToNode.size() == 3
tree.remove(b)
assert tree.root.children.size() == 0
assert tree.fileToNode.isEmpty()
}
@Test
public void testRemoveFileFromNonEmptyDir() {
File a = new File("a")
File b = new File(a,"b")
File c = new File(b, "c")
File d = new File(b, "d")
FileTree tree = new FileTree()
tree.add(c)
assert tree.fileToNode.size() == 3
tree.add(d)
assert tree.fileToNode.size() == 4
tree.remove(d)
assert tree.fileToNode.size() == 3
}
}

View File

@@ -1,11 +1,12 @@
group = com.muwire
version = 0.5.2
i2pVersion = 0.9.42
version = 0.5.7
i2pVersion = 0.9.43
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4
grailsVersion=4.0.0
gorm.version=7.0.2.RELEASE
griffonEnv=prod
sourceCompatibility=1.8
targetCompatibility=1.8

View File

@@ -40,8 +40,11 @@ griffon {
]
}
mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
application {
mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
applicationName = 'MuWire'
}
apply from: 'gradle/publishing.gradle'
// apply from: 'gradle/code-coverage.gradle'

View File

@@ -1,8 +1,8 @@
application {
title = 'MuWire'
startupGroups = ['EventList', 'MainFrame']
autoShutdown = true
startupGroups = ['EventList', 'MainFrame', 'ShutdownWindow']
autoShutdown = false
}
mvcGroups {
@@ -16,6 +16,11 @@ mvcGroups {
view = 'com.muwire.gui.MainFrameView'
controller = 'com.muwire.gui.MainFrameController'
}
'ShutdownWindow' {
model = 'com.muwire.gui.ShutdownWindowModel'
view = 'com.muwire.gui.ShutdownWindowView'
controller = 'com.muwire.gui.ShutdownWindowController'
}
'SearchTab' {
model = 'com.muwire.gui.SearchTabModel'
view = 'com.muwire.gui.SearchTabView'
@@ -36,6 +41,11 @@ mvcGroups {
view = 'com.muwire.gui.I2PStatusView'
controller = 'com.muwire.gui.I2PStatusController'
}
'system-status' {
model = 'com.muwire.gui.SystemStatusModel'
view = 'com.muwire.gui.SystemStatusView'
controller = 'com.muwire.gui.SystemStatusController'
}
'trust-list' {
model = 'com.muwire.gui.TrustListModel'
view = 'com.muwire.gui.TrustListView'
@@ -61,4 +71,19 @@ mvcGroups {
view = 'com.muwire.gui.BrowseView'
controller = 'com.muwire.gui.BrowseController'
}
'close-warning' {
model = 'com.muwire.gui.CloseWarningModel'
view = 'com.muwire.gui.CloseWarningView'
controller = 'com.muwire.gui.CloseWarningController'
}
'update' {
model = 'com.muwire.gui.UpdateModel'
view = 'com.muwire.gui.UpdateView'
controller = 'com.muwire.gui.UpdateController'
}
'advanced-sharing' {
model = 'com.muwire.gui.AdvancedSharingModel'
view = 'com.muwire.gui.AdvancedSharingView'
controller = 'com.muwire.gui.AdvancedSharingController'
}
}

View File

@@ -7,7 +7,9 @@ import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull
import javax.swing.JOptionPane
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.files.UICommentEvent
import com.muwire.core.util.DataUtil
@@ -24,6 +26,11 @@ class AddCommentController {
@ControllerAction
void save() {
String comment = view.textarea.getText()
if (comment.length() > Constants.MAX_COMMENT_LENGTH ) {
JOptionPane.showMessageDialog(null, "Your comment is too long - ${comment.length()} bytes. The maximum size is $Constants.MAX_COMMENT_LENGTH bytes",
"Comment Too Long", JOptionPane.WARNING_MESSAGE)
return
}
if (comment.trim().length() == 0)
comment = null
else

View File

@@ -0,0 +1,17 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.Core
@ArtifactProviderFor(GriffonController)
class AdvancedSharingController {
@MVCMember @Nonnull
AdvancedSharingModel model
@MVCMember @Nonnull
AdvancedSharingView view
}

View File

@@ -0,0 +1,60 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonController)
class CloseWarningController {
@MVCMember @Nonnull
CloseWarningModel model
@MVCMember @Nonnull
CloseWarningView view
UISettings settings
File home
void mvcGroupInit(Map<String, String> args) {
model.closeWarning = settings.closeWarning
}
@ControllerAction
void close() {
boolean rememberDecision = view.checkbox.model.isSelected()
if (rememberDecision) {
settings.exitOnClose = false
settings.closeWarning = false
saveMuSettings()
}
view.dialog.setVisible(false)
view.mainFrame.setVisible(false)
mvcGroup.destroy()
}
@ControllerAction
void exit() {
boolean rememberDecision = view.checkbox.model.isSelected()
if (rememberDecision) {
settings.exitOnClose = true
settings.closeWarning = false
saveMuSettings()
}
view.dialog.setVisible(false)
view.mainFrame.setVisible(false)
def parentView = mvcGroup.parentGroup.view
mvcGroup.destroy()
parentView.closeApplication()
}
private void saveMuSettings() {
File props = new File(home, "gui.properties")
props.withOutputStream {
settings.write(it)
}
}
}

View File

@@ -97,9 +97,6 @@ class ContentPanelController {
}
void saveMuWireSettings() {
File f = new File(core.home, "MuWire.properties")
f.withOutputStream {
core.muOptions.write(it)
}
core.saveMuSettings()
}
}

View File

@@ -7,7 +7,12 @@ import griffon.core.mvc.MVCGroup
import griffon.core.mvc.MVCGroupConfiguration
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import java.awt.event.ActionEvent
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
import javax.inject.Inject
@@ -47,11 +52,29 @@ class MainFrameController {
private volatile Core core
@ControllerAction
void search() {
void clearSearch() {
def searchField = builder.getVariable("search-field")
searchField.setSelectedItem(null)
searchField.requestFocus()
}
@ControllerAction
void search(ActionEvent evt) {
if (evt?.getActionCommand() == null)
return
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
def search = builder.getVariable("search-field").text
def searchField = builder.getVariable("search-field")
def search = searchField.getSelectedItem()
searchField.model.addElement(search)
performSearch(search)
}
private void performSearch(String search) {
model.sessionRestored = true
search = search.trim()
if (search.length() == 0)
return
@@ -77,21 +100,28 @@ class MainFrameController {
}
def searchEvent
byte [] payload
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true)
payload = root
} else {
// this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
def terms = replaced.split(" ")
def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
payload = String.join(" ",nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
}
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me, sig : sig.data))
}
void search(String infoHash, String tabTitle) {
@@ -292,12 +322,22 @@ class MainFrameController {
params['core'] = core
mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
}
@ControllerAction
void clearUploads() {
model.uploads.removeAll { it.finished }
}
@ControllerAction
void restoreSession() {
model.sessionRestored = true
view.settings.openTabs.each {
performSearch(it)
}
}
void saveMuWireSettings() {
File f = new File(core.home, "MuWire.properties")
f.withOutputStream {
core.muOptions.write(it)
}
core.saveMuSettings()
}
void mvcGroupInit(Map<String, String> args) {

View File

@@ -36,8 +36,8 @@ class MuWireStatusController {
model.sharedFiles = core.fileManager.fileToSharedFile.size()
model.downloads = core.downloadManager.downloaders.size()
model.browsed = core.connectionAcceptor.browsed
}
@ControllerAction

View File

@@ -10,6 +10,7 @@ import java.util.logging.Level
import javax.annotation.Nonnull
import javax.swing.JFileChooser
import javax.swing.JOptionPane
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
@@ -20,12 +21,14 @@ class OptionsController {
OptionsModel model
@MVCMember @Nonnull
OptionsView view
Core core
MuWireSettings settings
UISettings uiSettings
@ControllerAction
void save() {
String text
Core core = application.context.get("core")
MuWireSettings settings = application.context.get("muwire-settings")
def i2pProps = core.i2pOptions
@@ -69,6 +72,16 @@ class OptionsController {
text = view.updateField.text
model.updateCheckInterval = text
settings.updateCheckInterval = Integer.valueOf(text)
text = view.totalUploadSlotsField.text
int totalUploadSlots = Integer.valueOf(text)
model.totalUploadSlots = totalUploadSlots
settings.totalUploadSlots = totalUploadSlots
text = view.uploadSlotsPerUserField.text
int uploadSlotsPerUser = Integer.valueOf(text)
model.uploadSlotsPerUser = uploadSlotsPerUser
settings.uploadSlotsPerUser = uploadSlotsPerUser
boolean searchComments = view.searchCommentsCheckbox.model.isSelected()
model.searchComments = searchComments
@@ -127,14 +140,10 @@ class OptionsController {
model.trustListInterval = trustListInterval
settings.trustListInterval = Integer.parseInt(trustListInterval)
File settingsFile = new File(core.home, "MuWire.properties")
settingsFile.withOutputStream {
settings.write(it)
}
core.saveMuSettings()
// UI Setttings
UISettings uiSettings = application.context.get("ui-settings")
text = view.lnfField.text
model.lnf = text
uiSettings.lnf = text
@@ -157,13 +166,29 @@ class OptionsController {
boolean excludeLocalResult = view.excludeLocalResultCheckbox.model.isSelected()
model.excludeLocalResult = excludeLocalResult
uiSettings.excludeLocalResult = excludeLocalResult
boolean clearUploads = view.clearUploadsCheckbox.model.isSelected()
model.clearUploads = clearUploads
uiSettings.clearUploads = clearUploads
boolean storeSearchHistory = view.storeSearchHistoryCheckbox.model.isSelected()
model.storeSearchHistory = storeSearchHistory
uiSettings.storeSearchHistory = storeSearchHistory
uiSettings.exitOnClose = model.exitOnClose
if (model.closeDecisionMade)
uiSettings.closeWarning = false
saveUISettings()
cancel()
}
private void saveUISettings() {
File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream {
uiSettings.write(it)
}
cancel()
}
@ControllerAction
@@ -195,13 +220,32 @@ class OptionsController {
}
@ControllerAction
void automaticFontAction() {
void automaticFont() {
model.automaticFontSize = true
model.customFontSize = 12
}
@ControllerAction
void customFontAction() {
void customFont() {
model.automaticFontSize = false
}
@ControllerAction
void exitOnClose() {
model.exitOnClose = true
model.closeDecisionMade = true
}
@ControllerAction
void minimizeOnClose() {
model.exitOnClose = false
model.closeDecisionMade = true
}
@ControllerAction
void clearHistory() {
uiSettings.searchHistory.clear()
saveUISettings()
JOptionPane.showMessageDialog(null, "Search history has been cleared")
}
}

View File

@@ -0,0 +1,11 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonController)
class ShutdownWindowController {
}

View File

@@ -0,0 +1,34 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonController)
class SystemStatusController {
@MVCMember @Nonnull
SystemStatusModel model
@MVCMember @Nonnull
SystemStatusView view
@ControllerAction
void refresh() {
long totalRam = Runtime.getRuntime().totalMemory()
long usedRam = totalRam - Runtime.getRuntime().freeMemory()
model.usedRam = usedRam
model.totalRam = totalRam
model.maxRam = Runtime.getRuntime().maxMemory()
model.javaVendor = System.getProperty("java.vendor")
model.javaVersion = System.getProperty("java.version")
}
@ControllerAction
void close() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -0,0 +1,27 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonController)
class UpdateController {
@MVCMember @Nonnull
UpdateView view
@MVCMember @Nonnull
UpdateModel model
@ControllerAction
void close() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
@ControllerAction
void search() {
mvcGroup.parentGroup.controller.search(model.available.infoHash, "MuWire update")
close()
}
}

View File

@@ -10,7 +10,9 @@ import com.muwire.gui.UISettings
import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.ImageIcon
import javax.swing.JLabel
import javax.swing.JPopupMenu
import javax.swing.JTable
import javax.swing.LookAndFeel
import javax.swing.UIManager
@@ -20,7 +22,11 @@ import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel
import java.awt.Font
import java.awt.MenuItem
import java.awt.PopupMenu
import java.awt.SystemTray
import java.awt.Toolkit
import java.awt.TrayIcon
import java.util.logging.Level
import java.util.logging.LogManager
@@ -43,6 +49,56 @@ class Initialize extends AbstractLifecycleHandler {
}
}
System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
if (SystemTray.isSupported() && (SystemVersion.isMac() || SystemVersion.isWindows())) {
try {
def tray = SystemTray.getSystemTray()
def url = Initialize.class.getResource("/MuWire-16x16.png")
def image = new ImageIcon(url, "tray icon").getImage()
def popupMenu = new PopupMenu()
def trayIcon = new TrayIcon(image, "MuWire", popupMenu)
def exit = new MenuItem("Exit")
exit.addActionListener({
application.getWindowManager().findWindow("main-frame").setVisible(false)
application.getWindowManager().findWindow("shutdown-window").setVisible(true)
Core core = application.getContext().get("core")
if (core != null) {
Thread t = new Thread({
core.shutdown()
application.shutdown()
}as Runnable)
t.start()
} else
application.shutdown()
tray.remove(trayIcon)
})
def showMW = {e ->
def mainFrame = application.getWindowManager().findWindow("main-frame")
if (mainFrame != null) {
Core core = application.getContext().get("core")
if (core != null)
mainFrame.setVisible(true)
}
}
def show = new MenuItem("Open MuWire")
show.addActionListener(showMW)
popupMenu.add(show)
popupMenu.add(exit)
tray.add(trayIcon)
trayIcon.addActionListener(showMW)
application.getContext().put("tray-icon", true)
} catch (Exception bad) {
log.log(Level.WARNING,"couldn't set tray icon",bad)
}
}
log.info "Loading home dir"
def portableHome = System.getProperty("portable.home")
def home = portableHome == null ?
@@ -61,6 +117,7 @@ class Initialize extends AbstractLifecycleHandler {
def guiPropsFile = new File(home, "gui.properties")
UISettings uiSettings
int rowHeight = 15
if (guiPropsFile.exists()) {
Properties props = new Properties()
guiPropsFile.withInputStream { props.load(it) }
@@ -93,15 +150,17 @@ class Initialize extends AbstractLifecycleHandler {
} else {
fontSize = uiSettings.fontSize
}
rowHeight = fontSize + 3
FontUIResource font = new FontUIResource(fontName, Font.PLAIN, fontSize)
def keys = lnf.getDefaults().keys()
while(keys.hasMoreElements()) {
def key = keys.nextElement()
def value = lnf.getDefaults().get(key)
if (value instanceof FontUIResource)
if (value instanceof FontUIResource) {
lnf.getDefaults().put(key, font)
UIManager.put(key, font)
}
}
}
} else {
@@ -125,6 +184,7 @@ class Initialize extends AbstractLifecycleHandler {
}
}
application.context.put("row-height", rowHeight)
application.context.put("ui-settings", uiSettings)
}

View File

@@ -41,9 +41,9 @@ class Ready extends AbstractLifecycleHandler {
def propsFile = new File(home, "MuWire.properties")
if (propsFile.exists()) {
log.info("loading existing props file")
propsFile.withInputStream {
propsFile.withReader("UTF-8", {
props.load(it)
}
})
props = new MuWireSettings(props)
if (props.incompleteLocation == null)
props.incompleteLocation = new File(home, "incompletes")
@@ -90,9 +90,9 @@ class Ready extends AbstractLifecycleHandler {
props.downloadLocation = chooser.getSelectedFile()
}
propsFile.withOutputStream {
propsFile.withPrintWriter("UTF-8", {
props.write(it)
}
})
}
Core core

View File

@@ -18,8 +18,8 @@ class Shutdown extends AbstractLifecycleHandler {
@Override
void execute() {
log.info("shutting down")
log.info("shutting down from lifecycle")
Core core = application.context.get("core")
core.shutdown()
core?.shutdown()
}
}

View File

@@ -0,0 +1,39 @@
package com.muwire.gui
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.MutableTreeNode
import com.muwire.core.Core
import com.muwire.core.files.FileTree
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class AdvancedSharingModel {
def watchedDirectories = []
def treeRoot
def negativeTree
Core core
void mvcGroupInit(Map<String,String> args) {
watchedDirectories.addAll(core.muOptions.watchedDirectories)
treeRoot = new DefaultMutableTreeNode()
negativeTree = new DefaultTreeModel(treeRoot)
copyTree(treeRoot, core.fileManager.negativeTree.root)
}
private void copyTree(DefaultMutableTreeNode jtreeNode, FileTree.TreeNode fileTreeNode) {
jtreeNode.setUserObject(fileTreeNode.file?.getName())
fileTreeNode.children.each {
MutableTreeNode newChild = new DefaultMutableTreeNode()
jtreeNode.add(newChild)
copyTree(newChild, it)
}
}
}

View File

@@ -0,0 +1,18 @@
package com.muwire.gui
import javax.annotation.Nonnull
import griffon.core.artifact.GriffonController
import griffon.core.artifact.GriffonModel
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import griffon.transform.Observable
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonModel)
class CloseWarningModel {
@Observable boolean closeWarning
@Observable boolean exitOnClose
}

View File

@@ -1,3 +1,4 @@
package com.muwire.gui
import java.util.concurrent.ConcurrentHashMap
@@ -28,13 +29,16 @@ import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatchedEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.SideCarFileEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent
@@ -45,6 +49,7 @@ import com.muwire.core.update.UpdateAvailableEvent
import com.muwire.core.update.UpdateDownloadedEvent
import com.muwire.core.upload.UploadEvent
import com.muwire.core.upload.UploadFinishedEvent
import com.muwire.core.upload.Uploader
import griffon.core.GriffonApplication
import griffon.core.artifact.GriffonModel
@@ -83,6 +88,8 @@ class MainFrameModel {
def trusted = []
def distrusted = []
def subscriptions = []
boolean sessionRestored
@Observable int connections
@Observable String me
@@ -186,6 +193,7 @@ class MainFrameModel {
core.eventBus.register(ConnectionEvent.class, this)
core.eventBus.register(DisconnectionEvent.class, this)
core.eventBus.register(FileHashedEvent.class, this)
core.eventBus.register(SideCarFileEvent.class, this)
core.eventBus.register(FileHashingEvent.class, this)
core.eventBus.register(FileLoadedEvent.class, this)
core.eventBus.register(UploadEvent.class, this)
@@ -199,6 +207,7 @@ class MainFrameModel {
core.eventBus.register(AllFilesLoadedEvent.class, this)
core.eventBus.register(UpdateDownloadedEvent.class, this)
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
core.eventBus.register(SearchEvent.class, this)
core.muOptions.watchedKeywords.each {
core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true))
@@ -258,8 +267,10 @@ class MainFrameModel {
void onUpdateDownloadedEvent(UpdateDownloadedEvent e) {
runInsideUIAsync {
JOptionPane.showMessageDialog(null, "MuWire $e.version has been downloaded. You can update now",
"Update Downloaded", JOptionPane.INFORMATION_MESSAGE)
Map<String, Object> args = new HashMap<>()
args['available'] = null
args['downloaded'] = e
mvcGroup.createMVCGroup("update", "update", args)
}
}
@@ -286,6 +297,8 @@ class MainFrameModel {
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
view.showRestoreOrEmpty()
if (connections > 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-search-panel")
@@ -355,6 +368,7 @@ class MainFrameModel {
def dmtn = fileToNode.remove(e.unsharedFile)
if (dmtn != null) {
loadedFiles = fileToNode.size()
List<File> unshared = new ArrayList<>()
while (true) {
def parent = dmtn.getParent()
parent.remove(dmtn)
@@ -363,12 +377,16 @@ class MainFrameModel {
if (parent.getChildCount() == 0) {
File file = parent.getUserObject().file
if (core.muOptions.watchedDirectories.contains(file.toString()))
core.eventBus.publish(new DirectoryUnsharedEvent(directory : parent.getUserObject().file))
unshared.add(file)
dmtn = parent
continue
}
break
}
if (!unshared.isEmpty()) {
File unsharedRoot = unshared.get( unshared.size() -1 )
core.eventBus.publish(new DirectoryUnsharedEvent(directory : unsharedRoot))
}
}
view.refreshSharedFiles()
}
@@ -376,15 +394,39 @@ class MainFrameModel {
void onUploadEvent(UploadEvent e) {
runInsideUIAsync {
uploads << e.uploader
UploaderWrapper wrapper = null
uploads.each {
if (it.uploader == e.uploader) {
wrapper = it
return
}
}
if (wrapper != null) {
wrapper.uploader = e.uploader
wrapper.requests++
wrapper.finished = false
} else
uploads << new UploaderWrapper(uploader : e.uploader)
JTable table = builder.getVariable("uploads-table")
table.model.fireTableDataChanged()
view.refreshSharedFiles()
}
}
void onUploadFinishedEvent(UploadFinishedEvent e) {
runInsideUIAsync {
uploads.remove(e.uploader)
UploaderWrapper wrapper = null
uploads.each {
if (it.uploader == e.uploader) {
wrapper = it
return
}
}
if (uiSettings.clearUploads) {
uploads.remove(wrapper)
} else {
wrapper.finished = true
}
JTable table = builder.getVariable("uploads-table")
table.model.fireTableDataChanged()
}
@@ -416,6 +458,12 @@ class MainFrameModel {
updateTablePreservingSelection("subscription-table")
}
}
void onSearchEvent(SearchEvent e) {
runInsideUIAsync {
view.refreshSharedFiles()
}
}
void onQueryEvent(QueryEvent e) {
if (e.replyTo == core.me.destination)
@@ -488,17 +536,16 @@ class MainFrameModel {
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
runInsideUIAsync {
int option = JOptionPane.showConfirmDialog(null,
"MuWire $e.version is available from $e.signer. You have "+ metadata["application.version"]+" Update?",
"New MuWire version availble", JOptionPane.OK_CANCEL_OPTION)
if (option == JOptionPane.CANCEL_OPTION)
return
controller.search(e.infoHash,"MuWire update")
Map<String, Object> args = new HashMap<>()
args['available'] = e
args['downloaded'] = null
mvcGroup.createMVCGroup("update", "update", args)
}
}
void onRouterDisconnectedEvent(RouterDisconnectedEvent e) {
if (core.getShutdown().get())
return
runInsideUIAsync {
JOptionPane.showMessageDialog(null, "MuWire lost connection to the I2P router and will now exit.",
"Connection to I2P router lost", JOptionPane.WARNING_MESSAGE)
@@ -518,6 +565,12 @@ class MainFrameModel {
}
}
void onSideCarFileEvent(SideCarFileEvent e) {
runInsideUIAsync {
view.refreshSharedFiles()
}
}
private void insertIntoTree(SharedFile file) {
List<File> parents = new ArrayList<>()
File tmp = file.file.getParentFile()
@@ -573,4 +626,10 @@ class MainFrameModel {
boolean canDownload(InfoHash hash) {
!downloadInfoHashes.contains(hash)
}
class UploaderWrapper {
Uploader uploader
int requests
boolean finished
}
}

View File

@@ -20,6 +20,7 @@ class MuWireStatusModel {
@Observable int hopelessHosts
@Observable int sharedFiles
@Observable int downloads
@Observable int browsed
void mvcGroupInit(Map<String,String> args) {
controller.refresh()

View File

@@ -19,6 +19,9 @@ class OptionsModel {
@Observable boolean searchComments
@Observable boolean browseFiles
@Observable int speedSmoothSeconds
@Observable int totalUploadSlots
@Observable int uploadSlotsPerUser
@Observable boolean storeSearchHistory
// i2p options
@Observable String inboundLength
@@ -38,6 +41,9 @@ class OptionsModel {
@Observable boolean clearFinishedDownloads
@Observable boolean excludeLocalResult
@Observable boolean showSearchHashes
@Observable boolean clearUploads
@Observable boolean exitOnClose
@Observable boolean closeDecisionMade
// bw options
@Observable String inBw
@@ -62,6 +68,8 @@ class OptionsModel {
searchComments = settings.searchComments
browseFiles = settings.browseFiles
speedSmoothSeconds = settings.speedSmoothSeconds
totalUploadSlots = settings.totalUploadSlots
uploadSlotsPerUser = settings.uploadSlotsPerUser
Core core = application.context.get("core")
inboundLength = core.i2pOptions["inbound.length"]
@@ -81,6 +89,9 @@ class OptionsModel {
clearFinishedDownloads = uiSettings.clearFinishedDownloads
excludeLocalResult = uiSettings.excludeLocalResult
showSearchHashes = uiSettings.showSearchHashes
clearUploads = uiSettings.clearUploads
exitOnClose = uiSettings.exitOnClose
storeSearchHistory = uiSettings.storeSearchHistory
if (core.router != null) {
inBw = String.valueOf(settings.inBw)

View File

@@ -0,0 +1,9 @@
package com.muwire.gui
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class ShutdownWindowModel {
}

View File

@@ -0,0 +1,24 @@
package com.muwire.gui
import javax.annotation.Nonnull
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class SystemStatusModel {
@MVCMember @Nonnull
SystemStatusController controller
@Observable String javaVendor
@Observable String javaVersion
@Observable long usedRam
@Observable long totalRam
@Observable long maxRam
void mvcGroupInit(Map<String,String> args) {
controller.refresh()
}
}

View File

@@ -0,0 +1,14 @@
package com.muwire.gui
import com.muwire.core.update.UpdateAvailableEvent
import com.muwire.core.update.UpdateDownloadedEvent
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class UpdateModel {
UpdateAvailableEvent available
UpdateDownloadedEvent downloaded
}

View File

@@ -43,7 +43,7 @@ class AddCommentView {
borderLayout()
panel (constraints : BorderLayout.CENTER) {
scrollPane {
textarea = textArea(text : comment, rows : 20, columns : 100, editable : true)
textarea = textArea(text : comment, rows : 20, columns : 100, editable : true, lineWrap:true, wrapStyleWord:true)
}
}
panel (constraints : BorderLayout.SOUTH) {

View File

@@ -0,0 +1,82 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JTabbedPane
import javax.swing.JTree
import javax.swing.SwingConstants
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class AdvancedSharingView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
AdvancedSharingModel model
def mainFrame
def dialog
def watchedDirsPanel
def negativeTreePanel
def watchedDirsTable
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
int rowHeight = application.context.get("row-height")
dialog = new JDialog(mainFrame,"Advanced Sharing",true)
dialog.setResizable(true)
watchedDirsPanel = builder.panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label(text : "Directories watched for file changes")
}
scrollPane( constraints : BorderLayout.CENTER ) {
watchedDirsTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.watchedDirectories) {
closureColumn(header : "Directory", type : String, read : {it})
}
}
}
}
negativeTreePanel = builder.panel {
borderLayout()
panel(constraints : BorderLayout.NORTH) {
label(text : "Files which are explicitly not shared")
}
scrollPane( constraints : BorderLayout.CENTER ) {
def jtree = new JTree(model.negativeTree)
tree(rootVisible : false, rowHeight : rowHeight,jtree)
}
}
}
void mvcGroupInit(Map<String,String> args) {
def tabbedPane = new JTabbedPane()
tabbedPane.addTab("Watched Directories", watchedDirsPanel)
tabbedPane.addTab("Negative Tree", negativeTreePanel)
dialog.with {
getContentPane().add(tabbedPane)
pack()
setLocationRelativeTo(mainFrame)
setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
show()
}
}
}

View File

@@ -0,0 +1,59 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import java.awt.GridBagConstraints
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class CloseWarningView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
CloseWarningModel model
def mainFrame
def dialog
def panel
def checkbox
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, "Close MuWire?", true)
panel = builder.panel {
gridBagLayout()
label(text : "Would you like to minimize to system tray or exit immediately?", constraints : gbc(gridx: 0, gridy: 0, gridwidth : 2))
label(text : "\n", constraints : gbc(gridx : 0, gridy : 1)) // TODO: real padding
label(text : "Remember my decision", constraints : gbc(gridx: 0, gridy : 2, weightx: 100, anchor : GridBagConstraints.LINE_END))
checkbox = checkBox(selected : bind {model.closeWarning}, constraints : gbc(gridx: 1, gridy :2))
panel (constraints : gbc(gridx: 0, gridy : 3, gridwidth : 2)) {
button(text : "Minimize To Tray", closeAction)
button(text : "Exit MuWire", exitAction)
}
}
dialog.getContentPane().add(panel)
dialog.pack()
dialog.setResizable(false)
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mainFrame.setVisible(false)
mvcGroup.destroy()
}
})
}
void mvcGroupInit(Map<String,String> args) {
dialog.show()
}
}

View File

@@ -38,6 +38,7 @@ class ContentPanelView {
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
int rowHeight = application.context.get("row-height")
dialog = new JDialog(mainFrame, "Content Control Panel", true)
mainPanel = builder.panel {
@@ -48,7 +49,7 @@ class ContentPanelView {
label(text : "Rules")
}
scrollPane (constraints : BorderLayout.CENTER) {
rulesTable = table(id : "rules-table", autoCreateRowSorter : true) {
rulesTable = table(id : "rules-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.rules) {
closureColumn(header: "Term", type:String, read: {row -> row.getTerm()})
closureColumn(header: "Regex?", type:Boolean, read: {row -> row instanceof RegexMatcher})
@@ -74,7 +75,7 @@ class ContentPanelView {
label(text : "Hits")
}
scrollPane(constraints : BorderLayout.CENTER) {
hitsTable = table(id : "hits-table", autoCreateRowSorter : true) {
hitsTable = table(id : "hits-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.hits) {
closureColumn(header : "Searcher", type : String, read : {row -> row.persona.getHumanReadableName()})
closureColumn(header : "Keywords", type : String, read : {row -> row.keywords.join(" ")})

View File

@@ -3,6 +3,7 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.DataHelper
import javax.swing.JDialog
import javax.swing.JPanel
@@ -64,10 +65,10 @@ class I2PStatusView {
panel(border : titledBorder(title : "Bandwidth", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx: 0, gridy: 3, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
gridBagLayout()
label(text : "Receive Bps (15 seconds)", constraints : gbc(gridx:0, gridy:0, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {model.receiveBps}, constraints : gbc(gridx: 1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "Send Bps (15 seconds)", constraints : gbc(gridx:0, gridy:1, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {model.sendBps}, constraints : gbc(gridx: 1, gridy:1, anchor : GridBagConstraints.LINE_END))
label(text : "Receive (15 seconds)", constraints : gbc(gridx:0, gridy:0, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {DataHelper.formatSize2Decimal(model.receiveBps,false)+"B"}, constraints : gbc(gridx: 1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "Send (15 seconds)", constraints : gbc(gridx:0, gridy:1, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {DataHelper.formatSize2Decimal(model.sendBps, false)+"B"}, constraints : gbc(gridx: 1, gridy:1, anchor : GridBagConstraints.LINE_END))
}
}

View File

@@ -1,5 +1,6 @@
package com.muwire.gui
import griffon.core.GriffonApplication
import griffon.core.artifact.GriffonView
import griffon.core.env.Metadata
import griffon.inject.MVCMember
@@ -10,7 +11,9 @@ import net.i2p.data.DataHelper
import javax.swing.BorderFactory
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JComboBox
import javax.swing.JFileChooser
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
@@ -19,13 +22,19 @@ import javax.swing.JTable
import javax.swing.JTree
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.SwingUtilities
import javax.swing.TransferHandler
import javax.swing.border.Border
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import javax.swing.event.TreeExpansionEvent
import javax.swing.event.TreeExpansionListener
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.tree.TreeNode
import javax.swing.tree.TreePath
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.download.Downloader
@@ -42,6 +51,8 @@ import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
@@ -54,20 +65,25 @@ class MainFrameView {
@MVCMember @Nonnull
MainFrameModel model
@Inject @Nonnull GriffonApplication application
@Inject Metadata metadata
def downloadsTable
def lastDownloadSortEvent
def lastSharedSortEvent
def trustTablesSortEvents = [:]
def expansionListener = new TreeExpansions()
UISettings settings
void initUI() {
settings = application.context.get("ui-settings")
int rowHeight = application.context.get("row-height")
builder.with {
application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null,
defaultCloseOperation : JFrame.DO_NOTHING_ON_CLOSE,
title: application.configuration['application.title'] + " " +
metadata["application.version"] + " revision " + metadata["build.revision"],
iconImage: imageIcon('/MuWire-48x48.png').image,
@@ -77,18 +93,35 @@ class MainFrameView {
pack : false,
visible : bind { model.coreInitialized }) {
menuBar {
menu (text : "File") {
menuItem("Exit", actionPerformed : {closeApplication()})
}
menu (text : "Options") {
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
menuItem("Content Control", actionPerformed : {
def env = [:]
env["core"] = model.core
mvcGroup.createMVCGroup("content-panel", env)
menuItem("Configuration", actionPerformed : {
def params = [:]
params['core'] = application.context.get("core")
params['settings'] = params['core'].muOptions
params['uiSettings'] = settings
mvcGroup.createMVCGroup("Options", params)
})
}
menu (text : "Status") {
menuItem("MuWire", actionPerformed : {mvcGroup.createMVCGroup("mu-wire-status")})
MuWireSettings muSettings = application.context.get("muwire-settings")
menuItem("I2P", enabled : bind {model.routerPresent}, actionPerformed: {mvcGroup.createMVCGroup("i-2-p-status")})
menuItem("System", actionPerformed : {mvcGroup.createMVCGroup("system-status")})
}
menu (text : "Tools") {
menuItem("Content Control", actionPerformed : {
def env = [:]
env["core"] = model.core
mvcGroup.createMVCGroup("content-panel", env)
})
menuItem("Advanced Sharing", actionPerformed : {
def env = [:]
env["core"] = model.core
mvcGroup.createMVCGroup("advanced-sharing",env)
})
}
}
borderLayout()
@@ -112,20 +145,39 @@ class MainFrameView {
panel(constraints: BorderLayout.CENTER) {
borderLayout()
label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
def searchFieldModel = new SearchFieldModel(settings, new File(application.context.get("muwire-home")))
JComboBox myComboBox = new SearchField(searchFieldModel)
myComboBox.setAction(searchAction)
widget(id: "search-field", constraints: BorderLayout.CENTER, myComboBox)
}
panel( constraints: BorderLayout.EAST) {
button(text: "Search", searchAction)
button(text : "", icon : imageIcon("/close_tab.png"), clearSearchAction)
}
}
}
}
panel (id: "cards-panel", constraints : BorderLayout.CENTER) {
cardLayout()
panel (constraints : "search window") {
borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
panel (id : "search window", constraints : "search window") {
cardLayout()
panel (constraints : "tabs-panel") {
borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
}
panel(constraints : "restore session") {
borderLayout()
panel (constraints : BorderLayout.CENTER) {
gridBagLayout()
label(text : "Saved Tabs:", constraints : gbc(gridx : 0, gridy : 0))
scrollPane (constraints : gbc(gridx : 0, gridy : 1)) {
list(items : new ArrayList(settings.openTabs))
}
button(text : "Restore Session", constraints : gbc(gridx :0, gridy : 2), restoreSessionAction)
}
}
}
panel (constraints: "downloads window") {
gridLayout(rows : 1, cols : 1)
@@ -133,7 +185,7 @@ class MainFrameView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true) {
downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list: model.downloads) {
closureColumn(header: "Name", preferredWidth: 300, type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState().toString()})
@@ -212,11 +264,13 @@ class MainFrameView {
panel (constraints : "shared files table") {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "shared-files-table", autoCreateRowSorter: true) {
table(id : "shared-files-table", autoCreateRowSorter: true, rowHeight : rowHeight) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() })
closureColumn(header : "Comments", preferredWidth : 100, type : Boolean, read : {it.getComment() != null})
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.getCachedLength() })
closureColumn(header : "Comments", preferredWidth : 50, type : Boolean, read : {it.getComment() != null})
closureColumn(header : "Search Hits", preferredWidth: 50, type : Integer, read : {it.getHits()})
closureColumn(header : "Downloaders", preferredWidth: 50, type : Integer, read : {it.getDownloaders().size()})
}
}
}
@@ -226,7 +280,7 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
def jtree = new JTree(model.sharedTree)
jtree.setCellRenderer(new SharedTreeRenderer())
tree(id : "shared-files-tree", rootVisible : false, expandsSelectedPaths: true, jtree)
tree(id : "shared-files-tree", rowHeight : rowHeight, rootVisible : false, expandsSelectedPaths: true, jtree)
}
}
}
@@ -256,33 +310,42 @@ class MainFrameView {
label("Uploads")
}
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "uploads-table") {
table(id : "uploads-table", rowHeight : rowHeight) {
tableModel(list : model.uploads) {
closureColumn(header : "Name", type : String, read : {row -> row.getName() })
closureColumn(header : "Name", type : String, read : {row -> row.uploader.getName() })
closureColumn(header : "Progress", type : String, read : { row ->
int percent = row.getProgress()
int percent = row.uploader.getProgress()
"$percent% of piece".toString()
})
closureColumn(header : "Downloader", type : String, read : { row ->
row.getDownloader()
row.uploader.getDownloader()
})
closureColumn(header : "Remote Pieces", type : String, read : { row ->
int pieces = row.getTotalPieces()
int done = row.getDonePieces()
int pieces = row.uploader.getTotalPieces()
int done = row.uploader.getDonePieces()
if (row.uploader.getProgress() == 100)
done++
int percent = -1
if ( pieces != 0 ) {
percent = (done * 100) / pieces
}
long size = row.getTotalSize()
long size = row.uploader.getTotalSize()
String totalSize = ""
if (size >= 0 ) {
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
}
String.format("%02d", percent) + "% ${totalSize} ($done/$pieces pcs)".toString()
})
closureColumn(header : "Speed", type : String, read : { row ->
int speed = row.uploader.speed()
DataHelper.formatSize2Decimal(speed, false) + "B/sec"
})
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Clear Finished Uploads", clearUploadsAction)
}
}
}
panel (constraints: "monitor window") {
@@ -293,7 +356,7 @@ class MainFrameView {
label("Connections")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "connections-table") {
table(id : "connections-table", rowHeight : rowHeight) {
tableModel(list : model.connectionList) {
closureColumn(header : "Destination", preferredWidth: 250, type: String, read : { row -> row.destination.toBase32() })
closureColumn(header : "Direction", preferredWidth: 20, type: String, read : { row ->
@@ -312,7 +375,7 @@ class MainFrameView {
label("Incoming searches")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") {
table(id : "searches-table", rowHeight : rowHeight) {
tableModel(list : model.searches) {
closureColumn(header : "Keywords", type : String, read : {
sanitized = it.search.replace('<', ' ')
@@ -345,7 +408,7 @@ class MainFrameView {
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table", autoCreateRowSorter : true) {
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
}
@@ -361,7 +424,7 @@ class MainFrameView {
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table", autoCreateRowSorter : true) {
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
}
@@ -380,7 +443,7 @@ class MainFrameView {
label(text : "Trust List Subscriptions")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "subscription-table", autoCreateRowSorter : true) {
table(id : "subscription-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.subscriptions) {
closureColumn(header : "Name", preferredWidth: 200, type: String, read : {it.persona.getHumanReadableName()})
closureColumn(header : "Trusted", preferredWidth : 20, type: Integer, read : {it.good.size()})
@@ -432,7 +495,28 @@ class MainFrameView {
true
}
})
mainFrame.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e) {
if (application.getContext().get("tray-icon")) {
if (settings.closeWarning) {
runInsideUIAsync {
Map<String, Object> args2 = new HashMap<>()
args2.put("settings", settings)
args2.put("home", model.core.home)
mvcGroup.createMVCGroup("close-warning", "Close Warning", args2)
}
} else if (settings.exitOnClose)
closeApplication()
} else {
closeApplication()
}
}})
// search field
def searchField = builder.getVariable("search-field")
// downloads table
def downloadsTable = builder.getVariable("downloads-table")
def selectionModel = downloadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
@@ -550,6 +634,8 @@ class MainFrameView {
model.addCommentButtonEnabled = selectedNode != null
})
sharedFilesTree.addTreeExpansionListener(expansionListener)
// searches table
def searchesTable = builder.getVariable("searches-table")
@@ -655,6 +741,9 @@ class MainFrameView {
// show tree by default
showSharedFilesTree.call()
// show search panel by default
showSearchWindow.call()
}
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
@@ -799,10 +888,23 @@ class MainFrameView {
showPopupMenu(menu, e)
}
void showRestoreOrEmpty() {
def searchWindow = builder.getVariable("search window")
String id
if (!model.sessionRestored && !settings.openTabs.isEmpty())
id = model.connections > 0 ? "restore session" : "tabs-panel"
else
id = "tabs-panel"
searchWindow.getLayout().show(searchWindow, id)
}
def showSearchWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
showRestoreOrEmpty()
model.searchesPaneButtonEnabled = false
model.downloadsPaneButtonEnabled = true
model.uploadsPaneButtonEnabled = true
@@ -890,8 +992,52 @@ class MainFrameView {
public void refreshSharedFiles() {
def tree = builder.getVariable("shared-files-tree")
TreePath[] selectedPaths = tree.getSelectionPaths()
Set<TreePath> expanded = new HashSet<>(expansionListener.expandedPaths)
model.sharedTree.nodeStructureChanged(model.treeRoot)
expanded.each { tree.expandPath(it) }
tree.setSelectionPaths(selectedPaths)
builder.getVariable("shared-files-table").model.fireTableDataChanged()
}
private void closeApplication() {
Core core = application.getContext().get("core")
def tabbedPane = builder.getVariable("result-tabs")
settings.openTabs.clear()
int count = tabbedPane.getTabCount()
for (int i = 0; i < count; i++)
settings.openTabs.add(tabbedPane.getTitleAt(i))
File uiPropsFile = new File(core.home, "gui.properties")
uiPropsFile.withOutputStream { settings.write(it) }
def mainFrame = builder.getVariable("main-frame")
mainFrame.setVisible(false)
application.getWindowManager().findWindow("shutdown-window").setVisible(true)
if (core != null) {
Thread t = new Thread({
core.shutdown()
application.shutdown()
}as Runnable)
t.start()
}
}
private static class TreeExpansions implements TreeExpansionListener {
private final Set<TreePath> expandedPaths = new HashSet<>()
@Override
public void treeExpanded(TreeExpansionEvent event) {
expandedPaths.add(event.getPath())
}
@Override
public void treeCollapsed(TreeExpansionEvent event) {
expandedPaths.remove(event.getPath())
}
}
}

Some files were not shown because too many files have changed in this diff Show More