Compare commits
95 Commits
muwire-0.5
...
muwire-0.5
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3b882ae644 | ||
![]() |
5b61738ca9 | ||
![]() |
c77d79513e | ||
![]() |
9f12442897 | ||
![]() |
477b0a47ad | ||
![]() |
7f1041dd96 | ||
![]() |
99393c59bd | ||
![]() |
a78d8c84ca | ||
![]() |
fa9c697bfa | ||
![]() |
e5b12701f5 | ||
![]() |
f69727ab43 | ||
![]() |
d7c7afe2c0 | ||
![]() |
6c806c4441 | ||
![]() |
c4095abdb4 | ||
![]() |
8801546854 | ||
![]() |
f6ee49c0f5 | ||
![]() |
2320d650f6 | ||
![]() |
e9e6e6920a | ||
![]() |
87e5007f39 | ||
![]() |
8df6715e24 | ||
![]() |
6d587bf228 | ||
![]() |
8684452848 | ||
![]() |
7d652fabcb | ||
![]() |
5eb8d75bba | ||
![]() |
9ca8d1738c | ||
![]() |
2bb9480137 | ||
![]() |
7a6365f87a | ||
![]() |
56540ca3ca | ||
![]() |
eb5a5198b1 | ||
![]() |
29562c42ea | ||
![]() |
f5284f9483 | ||
![]() |
9bd3c4f141 | ||
![]() |
817dd68faf | ||
![]() |
5954cdb342 | ||
![]() |
56d44e6458 | ||
![]() |
c6fb76610d | ||
![]() |
5e329dfa2c | ||
![]() |
742f6da870 | ||
![]() |
7f46347c0f | ||
![]() |
b308ac2f37 | ||
![]() |
9cdabb51d1 | ||
![]() |
45f0736a5e | ||
![]() |
fe753ff978 | ||
![]() |
ac717b5205 | ||
![]() |
6f624e3afc | ||
![]() |
623d675ed9 | ||
![]() |
546b71b632 | ||
![]() |
804113bb1b | ||
![]() |
ab9e10f438 | ||
![]() |
00520acdf0 | ||
![]() |
8c44d196a7 | ||
![]() |
9c5fa0a2ce | ||
![]() |
d7bca05725 | ||
![]() |
45fcb2209e | ||
![]() |
7bf0373b80 | ||
![]() |
5925b42597 | ||
![]() |
13243b05ad | ||
![]() |
43987be463 | ||
![]() |
fcd3414e02 | ||
![]() |
70913ea8fb | ||
![]() |
b30e552498 | ||
![]() |
bae66de4eb | ||
![]() |
626e145e25 | ||
![]() |
bf72c76f13 | ||
![]() |
fce8bbfd97 | ||
![]() |
1cc7925155 | ||
![]() |
12b51ceb02 | ||
![]() |
62811861a4 | ||
![]() |
837aa6974b | ||
![]() |
94e7c42d19 | ||
![]() |
877bf12a93 | ||
![]() |
224266b2dd | ||
![]() |
8f16614dc3 | ||
![]() |
b412f9fb0c | ||
![]() |
b24d04811d | ||
![]() |
771f645df0 | ||
![]() |
b6483ad0f4 | ||
![]() |
decb72c8ef | ||
![]() |
439b3bf18b | ||
![]() |
06679ffee0 | ||
![]() |
1d5b12e2d7 | ||
![]() |
4e6e1b6f5b | ||
![]() |
f0b5361d7b | ||
![]() |
e0c6bfbf51 | ||
![]() |
2a0ecd8a47 | ||
![]() |
fb1804e849 | ||
![]() |
d4eaa0df8d | ||
![]() |
ffde6ac86f | ||
![]() |
7ad677ead2 | ||
![]() |
ddb0568aab | ||
![]() |
ff50a84a48 | ||
![]() |
770396ba41 | ||
![]() |
b55852e993 | ||
![]() |
a6945275a4 | ||
![]() |
7241809e55 |
10
README.md
10
README.md
@@ -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.
|
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.3 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||||
|
|
||||||
### Building
|
### 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
|
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.
|
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`
|
[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.
|
||||||
|
|
||||||
|
The CLI is under active development and doesn't have all the features of the GUI.
|
||||||
|
|
||||||
### GPG Fingerprint
|
### GPG Fingerprint
|
||||||
|
|
||||||
```
|
```
|
||||||
|
23
cli-lanterna/build.gradle
Normal file
23
cli-lanterna/build.gradle
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
buildscript {
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin : 'application'
|
||||||
|
mainClassName = 'com.muwire.clilanterna.CliLanterna'
|
||||||
|
apply plugin : 'com.github.johnrengelman.shadow'
|
||||||
|
|
||||||
|
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile project(":core")
|
||||||
|
compile 'com.googlecode.lanterna:lanterna:3.0.1'
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,65 @@
|
|||||||
|
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.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.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()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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.5"
|
||||||
|
|
||||||
|
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.withOutputStream {
|
||||||
|
props.write(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
props = new Properties()
|
||||||
|
propsFile.withInputStream {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,278 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 from $e.signer, search for $e.infoHash"
|
||||||
|
MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.OK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onUpdateDownloadedEvent(UpdateDownloadedEvent e) {
|
||||||
|
textGUI.getGUIThread().invokeLater {
|
||||||
|
String label = "$e.version downloaded"
|
||||||
|
updateStatus.setText(label)
|
||||||
|
String message = "Version $e.version from $e.signer has been downloaded. You can update now."
|
||||||
|
MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.OK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,75 @@
|
|||||||
|
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.data.Base64
|
||||||
|
|
||||||
|
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
|
||||||
|
if (hashSearch) {
|
||||||
|
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
|
||||||
|
} else {
|
||||||
|
def replaced = query.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.randomUUID(), oobInfohash: true,
|
||||||
|
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||||
|
}
|
||||||
|
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
|
||||||
|
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||||
|
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||||
|
originator : core.me))
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -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.withOutputStream { core.muOptions.write(it) }
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -35,7 +35,7 @@ class Cli {
|
|||||||
|
|
||||||
Core core
|
Core core
|
||||||
try {
|
try {
|
||||||
core = new Core(props, home, "0.5.2")
|
core = new Core(props, home, "0.5.3")
|
||||||
} catch (Exception bad) {
|
} catch (Exception bad) {
|
||||||
bad.printStackTrace(System.out)
|
bad.printStackTrace(System.out)
|
||||||
println "Failed to initialize core, exiting"
|
println "Failed to initialize core, exiting"
|
||||||
|
@@ -53,7 +53,7 @@ class CliDownloader {
|
|||||||
|
|
||||||
Core core
|
Core core
|
||||||
try {
|
try {
|
||||||
core = new Core(props, home, "0.5.2")
|
core = new Core(props, home, "0.5.3")
|
||||||
} catch (Exception bad) {
|
} catch (Exception bad) {
|
||||||
bad.printStackTrace(System.out)
|
bad.printStackTrace(System.out)
|
||||||
println "Failed to initialize core, exiting"
|
println "Failed to initialize core, exiting"
|
||||||
|
@@ -32,6 +32,7 @@ import com.muwire.core.files.UICommentEvent
|
|||||||
import com.muwire.core.files.UIPersistFilesEvent
|
import com.muwire.core.files.UIPersistFilesEvent
|
||||||
import com.muwire.core.files.AllFilesLoadedEvent
|
import com.muwire.core.files.AllFilesLoadedEvent
|
||||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||||
|
import com.muwire.core.files.DirectoryWatchedEvent
|
||||||
import com.muwire.core.files.DirectoryWatcher
|
import com.muwire.core.files.DirectoryWatcher
|
||||||
import com.muwire.core.hostcache.CacheClient
|
import com.muwire.core.hostcache.CacheClient
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
@@ -275,7 +276,7 @@ public class Core {
|
|||||||
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
||||||
|
|
||||||
log.info("initializing upload manager")
|
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")
|
log.info("initializing connection establisher")
|
||||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||||
@@ -287,7 +288,7 @@ public class Core {
|
|||||||
|
|
||||||
log.info("initializing directory watcher")
|
log.info("initializing directory watcher")
|
||||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
||||||
eventBus.register(FileSharedEvent.class, directoryWatcher)
|
eventBus.register(DirectoryWatchedEvent.class, directoryWatcher)
|
||||||
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||||
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||||
|
|
||||||
@@ -331,6 +332,9 @@ public class Core {
|
|||||||
log.info("already shutting down")
|
log.info("already shutting down")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.info("saving settings")
|
||||||
|
File f = new File(home, "MuWire.properties")
|
||||||
|
f.withOutputStream { muOptions.write(it) }
|
||||||
log.info("shutting down trust subscriber")
|
log.info("shutting down trust subscriber")
|
||||||
trustSubscriber.stop()
|
trustSubscriber.stop()
|
||||||
log.info("shutting down download manageer")
|
log.info("shutting down download manageer")
|
||||||
@@ -349,6 +353,7 @@ public class Core {
|
|||||||
log.info("shutting down embedded router")
|
log.info("shutting down embedded router")
|
||||||
router.shutdown(0)
|
router.shutdown(0)
|
||||||
}
|
}
|
||||||
|
log.info("shutdown complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
static main(args) {
|
static main(args) {
|
||||||
@@ -375,7 +380,7 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Core core = new Core(props, home, "0.5.2")
|
Core core = new Core(props, home, "0.5.5")
|
||||||
core.startServices()
|
core.startServices()
|
||||||
|
|
||||||
// ... at the end, sleep or execute script
|
// ... at the end, sleep or execute script
|
||||||
|
@@ -17,6 +17,8 @@ class MuWireSettings {
|
|||||||
int trustListInterval
|
int trustListInterval
|
||||||
Set<Persona> trustSubscriptions
|
Set<Persona> trustSubscriptions
|
||||||
int downloadRetryInterval
|
int downloadRetryInterval
|
||||||
|
int totalUploadSlots
|
||||||
|
int uploadSlotsPerUser
|
||||||
int updateCheckInterval
|
int updateCheckInterval
|
||||||
boolean autoDownloadUpdate
|
boolean autoDownloadUpdate
|
||||||
String updateType
|
String updateType
|
||||||
@@ -37,6 +39,7 @@ class MuWireSettings {
|
|||||||
int inBw, outBw
|
int inBw, outBw
|
||||||
Set<String> watchedKeywords
|
Set<String> watchedKeywords
|
||||||
Set<String> watchedRegexes
|
Set<String> watchedRegexes
|
||||||
|
Set<String> negativeFileTree
|
||||||
|
|
||||||
MuWireSettings() {
|
MuWireSettings() {
|
||||||
this(new Properties())
|
this(new Properties())
|
||||||
@@ -72,10 +75,13 @@ class MuWireSettings {
|
|||||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||||
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
||||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
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")
|
watchedDirectories = readEncodedSet(props, "watchedDirectories")
|
||||||
watchedKeywords = readEncodedSet(props, "watchedKeywords")
|
watchedKeywords = readEncodedSet(props, "watchedKeywords")
|
||||||
watchedRegexes = readEncodedSet(props, "watchedRegexes")
|
watchedRegexes = readEncodedSet(props, "watchedRegexes")
|
||||||
|
negativeFileTree = readEncodedSet(props, "negativeFileTree")
|
||||||
|
|
||||||
trustSubscriptions = new HashSet<>()
|
trustSubscriptions = new HashSet<>()
|
||||||
if (props.containsKey("trustSubscriptions")) {
|
if (props.containsKey("trustSubscriptions")) {
|
||||||
@@ -116,10 +122,13 @@ class MuWireSettings {
|
|||||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||||
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
||||||
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
|
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
|
||||||
|
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
|
||||||
|
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
|
||||||
|
|
||||||
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||||
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||||
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
|
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
|
||||||
|
writeEncodedSet(negativeFileTree, "negativeFileTree", props)
|
||||||
|
|
||||||
if (!trustSubscriptions.isEmpty()) {
|
if (!trustSubscriptions.isEmpty()) {
|
||||||
String encoded = trustSubscriptions.stream().
|
String encoded = trustSubscriptions.stream().
|
||||||
|
@@ -1,7 +1,12 @@
|
|||||||
package com.muwire.core.connection
|
package com.muwire.core.connection
|
||||||
|
|
||||||
import java.util.concurrent.BlockingQueue
|
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.LinkedBlockingQueue
|
||||||
|
import java.util.concurrent.ThreadFactory
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
@@ -21,7 +26,7 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
@Log
|
@Log
|
||||||
abstract class Connection implements Closeable {
|
abstract class Connection implements Closeable {
|
||||||
|
|
||||||
private static final int SEARCHES = 10
|
private static final int SEARCHES = 10
|
||||||
private static final long INTERVAL = 1000
|
private static final long INTERVAL = 1000
|
||||||
|
|
||||||
@@ -83,6 +88,7 @@ abstract class Connection implements Closeable {
|
|||||||
reader.interrupt()
|
reader.interrupt()
|
||||||
writer.interrupt()
|
writer.interrupt()
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
|
log.info("closed $name")
|
||||||
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +97,7 @@ abstract class Connection implements Closeable {
|
|||||||
while(running.get()) {
|
while(running.get()) {
|
||||||
read()
|
read()
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException ok) {
|
||||||
} catch (SocketTimeoutException e) {
|
} catch (SocketTimeoutException e) {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.log(Level.WARNING,"unhandled exception in reader",e)
|
log.log(Level.WARNING,"unhandled exception in reader",e)
|
||||||
@@ -107,6 +114,7 @@ abstract class Connection implements Closeable {
|
|||||||
def message = messages.take()
|
def message = messages.take()
|
||||||
write(message)
|
write(message)
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException ok) {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.log(Level.WARNING, "unhandled exception in writer",e)
|
log.log(Level.WARNING, "unhandled exception in writer",e)
|
||||||
} finally {
|
} finally {
|
||||||
|
@@ -50,6 +50,8 @@ class ConnectionAcceptor {
|
|||||||
final ExecutorService handshakerThreads
|
final ExecutorService handshakerThreads
|
||||||
|
|
||||||
private volatile shutdown
|
private volatile shutdown
|
||||||
|
|
||||||
|
private volatile int browsed
|
||||||
|
|
||||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||||
@@ -339,7 +341,8 @@ class ConnectionAcceptor {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
browsed++
|
||||||
|
|
||||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
def sharedFiles = fileManager.getSharedFiles().values()
|
def sharedFiles = fileManager.getSharedFiles().values()
|
||||||
@@ -349,6 +352,7 @@ class ConnectionAcceptor {
|
|||||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||||
JsonOutput jsonOutput = new JsonOutput()
|
JsonOutput jsonOutput = new JsonOutput()
|
||||||
sharedFiles.each {
|
sharedFiles.each {
|
||||||
|
it.hit()
|
||||||
def obj = ResultsSender.sharedFileToObj(it, false)
|
def obj = ResultsSender.sharedFileToObj(it, false)
|
||||||
def json = jsonOutput.toJson(obj)
|
def json = jsonOutput.toJson(obj)
|
||||||
dos.writeShort((short)json.length())
|
dos.writeShort((short)json.length())
|
||||||
|
@@ -62,7 +62,7 @@ class ConnectionEstablisher {
|
|||||||
void stop() {
|
void stop() {
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
executor.shutdownNow()
|
executor.shutdownNow()
|
||||||
closer.shutdown()
|
closer.shutdownNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
private void connectIfNeeded() {
|
private void connectIfNeeded() {
|
||||||
@@ -123,10 +123,12 @@ class ConnectionEstablisher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void fail(Endpoint endpoint) {
|
private void fail(Endpoint endpoint) {
|
||||||
closer.execute {
|
if (!closer.isShutdown()) {
|
||||||
endpoint.close()
|
closer.execute {
|
||||||
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
endpoint.close()
|
||||||
} as Runnable
|
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
||||||
|
} as Runnable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readK(Endpoint e) {
|
private void readK(Endpoint e) {
|
||||||
|
@@ -36,9 +36,8 @@ abstract class ConnectionManager {
|
|||||||
timer.schedule({sendPings()} as TimerTask, 1000,1000)
|
timer.schedule({sendPings()} as TimerTask, 1000,1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void shutdown() {
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
getConnections().each { it.close() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTrustEvent(TrustEvent e) {
|
void onTrustEvent(TrustEvent e) {
|
||||||
@@ -62,8 +61,6 @@ abstract class ConnectionManager {
|
|||||||
|
|
||||||
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
||||||
|
|
||||||
abstract void shutdown()
|
|
||||||
|
|
||||||
protected void sendPings() {
|
protected void sendPings() {
|
||||||
final long now = System.currentTimeMillis()
|
final long now = System.currentTimeMillis()
|
||||||
getConnections().each {
|
getConnections().each {
|
||||||
|
@@ -31,9 +31,6 @@ class Endpoint implements Closeable {
|
|||||||
if (inputStream != null) {
|
if (inputStream != null) {
|
||||||
try {inputStream.close()} catch (Exception ignore) {}
|
try {inputStream.close()} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
if (outputStream != null) {
|
|
||||||
try {outputStream.close()} catch (Exception ignore) {}
|
|
||||||
}
|
|
||||||
if (toClose != null) {
|
if (toClose != null) {
|
||||||
try {toClose.reset()} catch (Exception ignore) {}
|
try {toClose.reset()} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
|
@@ -104,6 +104,7 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
|
super.shutdown()
|
||||||
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||||
leafConnections.values().stream().parallel().forEach({v -> v.close()})
|
leafConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||||
peerConnections.clear()
|
peerConnections.clear()
|
||||||
|
@@ -12,6 +12,7 @@ import com.muwire.core.util.DataUtil
|
|||||||
import groovy.json.JsonBuilder
|
import groovy.json.JsonBuilder
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
import groovy.json.JsonSlurper
|
import groovy.json.JsonSlurper
|
||||||
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
import net.i2p.util.ConcurrentHashSet
|
import net.i2p.util.ConcurrentHashSet
|
||||||
@@ -25,7 +26,9 @@ import com.muwire.core.UILoadedEvent
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
@Log
|
||||||
public class DownloadManager {
|
public class DownloadManager {
|
||||||
|
|
||||||
private final EventBus eventBus
|
private final EventBus eventBus
|
||||||
@@ -135,22 +138,33 @@ public class DownloadManager {
|
|||||||
else
|
else
|
||||||
incompletes = new File(home, "incompletes")
|
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)
|
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
|
||||||
|
|
||||||
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
|
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
|
||||||
if (json.paused != null)
|
if (json.paused != null)
|
||||||
downloader.paused = json.paused
|
downloader.paused = json.paused
|
||||||
downloaders.put(infoHash, downloader)
|
|
||||||
downloader.readPieces()
|
try {
|
||||||
if (!downloader.paused)
|
downloader.readPieces()
|
||||||
downloader.download()
|
if (!downloader.paused)
|
||||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
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) {
|
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
|
||||||
int pieceSize = 0x1 << pieceSizePow2
|
long pieceSize = 0x1L << pieceSizePow2
|
||||||
int nPieces = (int)(length / pieceSize)
|
int nPieces = (int)(length / pieceSize)
|
||||||
if (length % pieceSize != 0)
|
if (length % pieceSize != 0)
|
||||||
nPieces++
|
nPieces++
|
||||||
|
@@ -141,6 +141,8 @@ class DownloadSession {
|
|||||||
// parse X-Have if present
|
// parse X-Have if present
|
||||||
if (headers.containsKey("X-Have")) {
|
if (headers.containsKey("X-Have")) {
|
||||||
DataUtil.decodeXHave(headers["X-Have"]).each {
|
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)
|
available.add(it)
|
||||||
}
|
}
|
||||||
if (!available.contains(piece))
|
if (!available.contains(piece))
|
||||||
|
@@ -75,6 +75,8 @@ class Pieces {
|
|||||||
}
|
}
|
||||||
|
|
||||||
synchronized void markDownloaded(int piece) {
|
synchronized void markDownloaded(int piece) {
|
||||||
|
if (piece >= nPieces)
|
||||||
|
throw new IllegalArgumentException("invalid piece marked as downloaded? $piece/$nPieces")
|
||||||
done.set(piece)
|
done.set(piece)
|
||||||
claimed.set(piece)
|
claimed.set(piece)
|
||||||
partials.remove(piece)
|
partials.remove(piece)
|
||||||
|
@@ -4,4 +4,8 @@ import com.muwire.core.Event
|
|||||||
|
|
||||||
class DirectoryUnsharedEvent extends Event {
|
class DirectoryUnsharedEvent extends Event {
|
||||||
File directory
|
File directory
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
super.toString() + " unshared directory "+ directory.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class DirectoryWatchedEvent extends Event {
|
||||||
|
File directory
|
||||||
|
}
|
@@ -66,10 +66,8 @@ class DirectoryWatcher {
|
|||||||
watchService?.close()
|
watchService?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
void onFileSharedEvent(FileSharedEvent e) {
|
void onDirectoryWatchedEvent(DirectoryWatchedEvent e) {
|
||||||
if (!e.file.isDirectory())
|
File canonical = e.directory.getCanonicalFile()
|
||||||
return
|
|
||||||
File canonical = e.file.getCanonicalFile()
|
|
||||||
Path path = canonical.toPath()
|
Path path = canonical.toPath()
|
||||||
WatchKey wk = path.register(watchService, kinds)
|
WatchKey wk = path.register(watchService, kinds)
|
||||||
watchedDirectories.put(canonical, wk)
|
watchedDirectories.put(canonical, wk)
|
||||||
@@ -125,7 +123,8 @@ class DirectoryWatcher {
|
|||||||
private void processModified(Path parent, Path path) {
|
private void processModified(Path parent, Path path) {
|
||||||
File f = join(parent, path)
|
File f = join(parent, path)
|
||||||
log.fine("modified entry $f")
|
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) {
|
private void processDeleted(Path parent, Path path) {
|
||||||
@@ -133,7 +132,7 @@ class DirectoryWatcher {
|
|||||||
log.fine("deleted entry $f")
|
log.fine("deleted entry $f")
|
||||||
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||||
if (sf != null)
|
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) {
|
private static File join(Path parent, Path path) {
|
||||||
|
@@ -13,8 +13,10 @@ import java.security.NoSuchAlgorithmException
|
|||||||
|
|
||||||
class FileHasher {
|
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 */
|
/** 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
|
* @param size of the file to be shared
|
||||||
@@ -24,9 +26,9 @@ class FileHasher {
|
|||||||
*/
|
*/
|
||||||
static int getPieceSize(long size) {
|
static int getPieceSize(long size) {
|
||||||
if (size <= 0x1 << 30)
|
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) {
|
if (size <= 0x1L << i) {
|
||||||
return i-13
|
return i-13
|
||||||
}
|
}
|
||||||
@@ -48,27 +50,28 @@ class FileHasher {
|
|||||||
|
|
||||||
InfoHash hashFile(File file) {
|
InfoHash hashFile(File file) {
|
||||||
final long length = file.length()
|
final long length = file.length()
|
||||||
final int size = 0x1 << getPieceSize(length)
|
final long size = 0x1L << getPieceSize(length)
|
||||||
int numPieces = (int) (length / size)
|
int numPieces = (length / size).toInteger()
|
||||||
if (numPieces * size < length)
|
if (numPieces * size < length)
|
||||||
numPieces++
|
numPieces++
|
||||||
|
|
||||||
def output = new ByteArrayOutputStream()
|
def output = new ByteArrayOutputStream()
|
||||||
RandomAccessFile raf = new RandomAccessFile(file, "r")
|
RandomAccessFile raf = new RandomAccessFile(file, "r")
|
||||||
|
MappedByteBuffer buf = null
|
||||||
try {
|
try {
|
||||||
MappedByteBuffer buf
|
|
||||||
for (int i = 0; i < numPieces - 1; i++) {
|
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
|
digest.update buf
|
||||||
DataUtil.tryUnmap(buf)
|
DataUtil.tryUnmap(buf)
|
||||||
output.write(digest.digest(), 0, 32)
|
output.write(digest.digest(), 0, 32)
|
||||||
}
|
}
|
||||||
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
long lastPieceLength = length - (numPieces - 1) * size
|
||||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength.toInteger())
|
||||||
digest.update buf
|
digest.update buf
|
||||||
output.write(digest.digest(), 0, 32)
|
output.write(digest.digest(), 0, 32)
|
||||||
} finally {
|
} finally {
|
||||||
raf.close()
|
raf.close()
|
||||||
|
DataUtil.tryUnmap(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
byte [] hashList = output.toByteArray()
|
byte [] hashList = output.toByteArray()
|
||||||
|
@@ -24,12 +24,17 @@ class FileManager {
|
|||||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||||
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
||||||
final SearchIndex index = new SearchIndex()
|
final SearchIndex index = new SearchIndex()
|
||||||
|
final FileTree negativeTree = new FileTree()
|
||||||
|
|
||||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
|
|
||||||
|
for (String negative : settings.negativeFileTree) {
|
||||||
|
negativeTree.add(new File(negative))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onFileHashedEvent(FileHashedEvent e) {
|
void onFileHashedEvent(FileHashedEvent e) {
|
||||||
if (e.sharedFile != null)
|
if (e.sharedFile != null)
|
||||||
addToIndex(e.sharedFile)
|
addToIndex(e.sharedFile)
|
||||||
@@ -56,6 +61,13 @@ class FileManager {
|
|||||||
}
|
}
|
||||||
existing.add(sf)
|
existing.add(sf)
|
||||||
fileToSharedFile.put(sf.file, 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()
|
String name = sf.getFile().getName()
|
||||||
Set<File> existingFiles = nameToFiles.get(name)
|
Set<File> existingFiles = nameToFiles.get(name)
|
||||||
@@ -92,6 +104,10 @@ class FileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileToSharedFile.remove(sf.file)
|
fileToSharedFile.remove(sf.file)
|
||||||
|
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
|
||||||
|
negativeTree.add(sf.file)
|
||||||
|
saveNegativeTree()
|
||||||
|
}
|
||||||
|
|
||||||
String name = sf.getFile().getName()
|
String name = sf.getFile().getName()
|
||||||
Set<File> existingFiles = nameToFiles.get(name)
|
Set<File> existingFiles = nameToFiles.get(name)
|
||||||
@@ -158,8 +174,10 @@ class FileManager {
|
|||||||
Set<SharedFile> found
|
Set<SharedFile> found
|
||||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||||
found = filter(found, e.oobInfohash)
|
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)
|
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
def names = index.search e.searchTerms
|
def names = index.search e.searchTerms
|
||||||
Set<File> files = new HashSet<>()
|
Set<File> files = new HashSet<>()
|
||||||
@@ -172,8 +190,10 @@ class FileManager {
|
|||||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||||
files = filter(sharedFiles, e.oobInfohash)
|
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)
|
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,8 +211,10 @@ class FileManager {
|
|||||||
}
|
}
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||||
|
negativeTree.remove(e.directory)
|
||||||
|
saveNegativeTree()
|
||||||
e.directory.listFiles().each {
|
e.directory.listFiles().each {
|
||||||
if (it.isDirectory())
|
if (it.isDirectory())
|
||||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||||
@@ -203,4 +225,9 @@ class FileManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void saveNegativeTree() {
|
||||||
|
settings.negativeFileTree.clear()
|
||||||
|
settings.negativeFileTree.addAll(negativeTree.fileToNode.keySet().collect { it.getAbsolutePath() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
64
core/src/main/groovy/com/muwire/core/files/FileTree.groovy
Normal file
64
core/src/main/groovy/com/muwire/core/files/FileTree.groovy
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -5,4 +5,5 @@ import com.muwire.core.SharedFile
|
|||||||
|
|
||||||
class FileUnsharedEvent extends Event {
|
class FileUnsharedEvent extends Event {
|
||||||
SharedFile unsharedFile
|
SharedFile unsharedFile
|
||||||
|
boolean deleted
|
||||||
}
|
}
|
||||||
|
@@ -47,7 +47,10 @@ class HasherService {
|
|||||||
|
|
||||||
private void process(File f) {
|
private void process(File f) {
|
||||||
if (f.isDirectory()) {
|
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 {
|
} else {
|
||||||
if (f.length() == 0) {
|
if (f.length() == 0) {
|
||||||
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
||||||
|
@@ -82,7 +82,7 @@ class PersisterService extends Service {
|
|||||||
} else {
|
} else {
|
||||||
listener.publish(new AllFilesLoadedEvent())
|
listener.publish(new AllFilesLoadedEvent())
|
||||||
}
|
}
|
||||||
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
timer.schedule({persistFiles()} as TimerTask, 1000, interval)
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,9 +129,15 @@ class PersisterService extends Service {
|
|||||||
return new FileLoadedEvent(loadedFile : df)
|
return new FileLoadedEvent(loadedFile : df)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int hits = 0
|
||||||
|
if (json.hits != null)
|
||||||
|
hits = json.hits
|
||||||
|
|
||||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||||
sf.setComment(json.comment)
|
sf.setComment(json.comment)
|
||||||
|
sf.hits = hits
|
||||||
|
if (json.downloaders != null)
|
||||||
|
sf.getDownloaders().addAll(json.downloaders)
|
||||||
return new FileLoadedEvent(loadedFile: sf)
|
return new FileLoadedEvent(loadedFile: sf)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -163,6 +169,8 @@ class PersisterService extends Service {
|
|||||||
json.pieceSize = sf.getPieceSize()
|
json.pieceSize = sf.getPieceSize()
|
||||||
json.hashList = sf.getB64EncodedHashList()
|
json.hashList = sf.getB64EncodedHashList()
|
||||||
json.comment = sf.getComment()
|
json.comment = sf.getComment()
|
||||||
|
json.hits = sf.getHits()
|
||||||
|
json.downloaders = sf.getDownloaders()
|
||||||
|
|
||||||
if (sf instanceof DownloadedFile) {
|
if (sf instanceof DownloadedFile) {
|
||||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package com.muwire.core.mesh
|
package com.muwire.core.mesh
|
||||||
|
|
||||||
|
import java.util.logging.Level
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
@@ -13,8 +14,10 @@ import com.muwire.core.util.DataUtil
|
|||||||
|
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
import groovy.json.JsonSlurper
|
import groovy.json.JsonSlurper
|
||||||
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
@Log
|
||||||
class MeshManager {
|
class MeshManager {
|
||||||
|
|
||||||
private final Map<InfoHash, Mesh> meshes = Collections.synchronizedMap(new HashMap<>())
|
private final Map<InfoHash, Mesh> meshes = Collections.synchronizedMap(new HashMap<>())
|
||||||
@@ -67,7 +70,10 @@ class MeshManager {
|
|||||||
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
|
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
|
||||||
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
|
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
|
||||||
json.nPieces = mesh.pieces.nPieces
|
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))
|
writer.println(JsonOutput.toJson(json))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +88,9 @@ class MeshManager {
|
|||||||
JsonSlurper slurper = new JsonSlurper()
|
JsonSlurper slurper = new JsonSlurper()
|
||||||
meshFile.eachLine {
|
meshFile.eachLine {
|
||||||
def json = slurper.parseText(it)
|
def json = slurper.parseText(it)
|
||||||
|
if (json.nPieces == null || json.nPieces == 0)
|
||||||
|
return // skip it, invalid
|
||||||
|
|
||||||
if (now - json.timestamp > settings.meshExpiration * 60 * 1000)
|
if (now - json.timestamp > settings.meshExpiration * 60 * 1000)
|
||||||
return
|
return
|
||||||
InfoHash infoHash = new InfoHash(Base64.decode(json.infoHash))
|
InfoHash infoHash = new InfoHash(Base64.decode(json.infoHash))
|
||||||
@@ -93,8 +102,13 @@ class MeshManager {
|
|||||||
mesh.sources.add(persona)
|
mesh.sources.add(persona)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.xHave != null)
|
if (json.xHave != null) {
|
||||||
DataUtil.decodeXHave(json.xHave).each { pieces.markDownloaded(it) }
|
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())
|
if (!mesh.sources.isEmpty())
|
||||||
meshes.put(infoHash, mesh)
|
meshes.put(infoHash, mesh)
|
||||||
|
@@ -6,6 +6,7 @@ import javax.naming.directory.InvalidSearchControlsException
|
|||||||
|
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.files.FileHasher
|
||||||
import com.muwire.core.util.DataUtil
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
@@ -30,12 +31,12 @@ class ResultsParser {
|
|||||||
private static parseV1(Persona p, UUID uuid, def json) {
|
private static parseV1(Persona p, UUID uuid, def json) {
|
||||||
if (json.name == null)
|
if (json.name == null)
|
||||||
throw new InvalidSearchResultException("name missing")
|
throw new InvalidSearchResultException("name missing")
|
||||||
if (json.size == null)
|
if (json.size == null || json.size <= 0 || json.size > FileHasher.MAX_SIZE)
|
||||||
throw new InvalidSearchResultException("length missing")
|
throw new InvalidSearchResultException("length missing or invalid, $json.size")
|
||||||
if (json.infohash == null)
|
if (json.infohash == null)
|
||||||
throw new InvalidSearchResultException("infohash missing")
|
throw new InvalidSearchResultException("infohash missing")
|
||||||
if (json.pieceSize == null)
|
if (json.pieceSize == null || json.pieceSize < FileHasher.MIN_PIECE_SIZE_POW2 || json.pieceSize > FileHasher.MAX_PIECE_SIZE_POW2)
|
||||||
throw new InvalidSearchResultException("pieceSize missing")
|
throw new InvalidSearchResultException("pieceSize missing or invalid, $json.pieceSize")
|
||||||
if (!(json.hashList instanceof List))
|
if (!(json.hashList instanceof List))
|
||||||
throw new InvalidSearchResultException("hashlist not a list")
|
throw new InvalidSearchResultException("hashlist not a list")
|
||||||
try {
|
try {
|
||||||
@@ -71,12 +72,12 @@ class ResultsParser {
|
|||||||
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
|
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
|
||||||
if (json.name == null)
|
if (json.name == null)
|
||||||
throw new InvalidSearchResultException("name missing")
|
throw new InvalidSearchResultException("name missing")
|
||||||
if (json.size == null)
|
if (json.size == null || json.size <= 0 || json.size > FileHasher.MAX_SIZE)
|
||||||
throw new InvalidSearchResultException("length missing")
|
throw new InvalidSearchResultException("length missing or invalid $json.size")
|
||||||
if (json.infohash == null)
|
if (json.infohash == null)
|
||||||
throw new InvalidSearchResultException("infohash missing")
|
throw new InvalidSearchResultException("infohash missing")
|
||||||
if (json.pieceSize == null)
|
if (json.pieceSize == null || json.pieceSize < FileHasher.MIN_PIECE_SIZE_POW2 || json.pieceSize > FileHasher.MAX_PIECE_SIZE_POW2)
|
||||||
throw new InvalidSearchResultException("pieceSize missing")
|
throw new InvalidSearchResultException("pieceSize missing or invalid, $json.pieceSize")
|
||||||
if (json.hashList != null)
|
if (json.hashList != null)
|
||||||
throw new InvalidSearchResultException("V2 result with hashlist")
|
throw new InvalidSearchResultException("V2 result with hashlist")
|
||||||
try {
|
try {
|
||||||
|
@@ -20,6 +20,8 @@ class ContentUploader extends Uploader {
|
|||||||
private final ContentRequest request
|
private final ContentRequest request
|
||||||
private final Mesh mesh
|
private final Mesh mesh
|
||||||
private final int pieceSize
|
private final int pieceSize
|
||||||
|
|
||||||
|
private volatile boolean done
|
||||||
|
|
||||||
ContentUploader(File file, ContentRequest request, Endpoint endpoint, Mesh mesh, int pieceSize) {
|
ContentUploader(File file, ContentRequest request, Endpoint endpoint, Mesh mesh, int pieceSize) {
|
||||||
super(endpoint)
|
super(endpoint)
|
||||||
@@ -62,20 +64,23 @@ class ContentUploader extends Uploader {
|
|||||||
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
||||||
byte [] tmp = new byte[0x1 << 13]
|
byte [] tmp = new byte[0x1 << 13]
|
||||||
while(mapped.hasRemaining()) {
|
while(mapped.hasRemaining()) {
|
||||||
int start = mapped.position()
|
int read
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
|
int start = mapped.position()
|
||||||
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
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().write(tmp, 0, read)
|
||||||
}
|
}
|
||||||
|
done = true
|
||||||
} finally {
|
} finally {
|
||||||
try {channel?.close() } catch (IOException ignored) {}
|
try {channel?.close() } catch (IOException ignored) {}
|
||||||
endpoint.getOutputStream().flush()
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
DataUtil.tryUnmap(mapped)
|
DataUtil.tryUnmap(mapped)
|
||||||
mapped = null
|
mapped = null
|
||||||
}
|
}
|
||||||
|
endpoint.getOutputStream().flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +103,7 @@ class ContentUploader extends Uploader {
|
|||||||
@Override
|
@Override
|
||||||
public synchronized int getProgress() {
|
public synchronized int getProgress() {
|
||||||
if (mapped == null)
|
if (mapped == null)
|
||||||
return 0
|
return done ? 100 : 0
|
||||||
int position = mapped.position()
|
int position = mapped.position()
|
||||||
int total = request.getRange().end - request.getRange().start
|
int total = request.getRange().end - request.getRange().start
|
||||||
(int)(position * 100.0 / total)
|
(int)(position * 100.0 / total)
|
||||||
@@ -123,4 +128,13 @@ class ContentUploader extends Uploader {
|
|||||||
public long getTotalSize() {
|
public long getTotalSize() {
|
||||||
return file.length();
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,11 +26,13 @@ class HashListUploader extends Uploader {
|
|||||||
|
|
||||||
byte[]tmp = new byte[0x1 << 13]
|
byte[]tmp = new byte[0x1 << 13]
|
||||||
while(mapped.hasRemaining()) {
|
while(mapped.hasRemaining()) {
|
||||||
int start = mapped.position()
|
int read
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
|
int start = mapped.position()
|
||||||
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
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().write(tmp, 0, read)
|
||||||
}
|
}
|
||||||
endpoint.getOutputStream().flush()
|
endpoint.getOutputStream().flush()
|
||||||
@@ -65,4 +67,12 @@ class HashListUploader extends Uploader {
|
|||||||
public long getTotalSize() {
|
public long getTotalSize() {
|
||||||
return -1;
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,8 @@ import java.nio.charset.StandardCharsets
|
|||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
import com.muwire.core.download.DownloadManager
|
import com.muwire.core.download.DownloadManager
|
||||||
@@ -22,15 +24,22 @@ public class UploadManager {
|
|||||||
private final FileManager fileManager
|
private final FileManager fileManager
|
||||||
private final MeshManager meshManager
|
private final MeshManager meshManager
|
||||||
private final DownloadManager downloadManager
|
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() {}
|
||||||
|
|
||||||
public UploadManager(EventBus eventBus, FileManager fileManager,
|
public UploadManager(EventBus eventBus, FileManager fileManager,
|
||||||
MeshManager meshManager, DownloadManager downloadManager) {
|
MeshManager meshManager, DownloadManager downloadManager,
|
||||||
|
MuWireSettings props) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.fileManager = fileManager
|
this.fileManager = fileManager
|
||||||
this.meshManager = meshManager
|
this.meshManager = meshManager
|
||||||
this.downloadManager = downloadManager
|
this.downloadManager = downloadManager
|
||||||
|
this.props = props
|
||||||
}
|
}
|
||||||
|
|
||||||
public void processGET(Endpoint e) throws IOException {
|
public void processGET(Endpoint e) throws IOException {
|
||||||
@@ -82,7 +91,15 @@ public class UploadManager {
|
|||||||
|
|
||||||
if (request.have > 0)
|
if (request.have > 0)
|
||||||
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
|
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
|
Mesh mesh
|
||||||
File file
|
File file
|
||||||
int pieceSize
|
int pieceSize
|
||||||
@@ -91,6 +108,7 @@ public class UploadManager {
|
|||||||
file = downloader.incompleteFile
|
file = downloader.incompleteFile
|
||||||
pieceSize = downloader.pieceSizePow2
|
pieceSize = downloader.pieceSizePow2
|
||||||
} else {
|
} else {
|
||||||
|
sharedFiles.each { it.getDownloaders().add(request.downloader.getHumanReadableName()) }
|
||||||
SharedFile sharedFile = sharedFiles.iterator().next();
|
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
||||||
file = sharedFile.file
|
file = sharedFile.file
|
||||||
@@ -102,6 +120,7 @@ public class UploadManager {
|
|||||||
try {
|
try {
|
||||||
uploader.respond()
|
uploader.respond()
|
||||||
} finally {
|
} finally {
|
||||||
|
decrementUploads(request.downloader)
|
||||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,12 +175,21 @@ public class UploadManager {
|
|||||||
return
|
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)
|
Uploader uploader = new HashListUploader(e, fullInfoHash, request)
|
||||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
try {
|
try {
|
||||||
uploader.respond()
|
uploader.respond()
|
||||||
} finally {
|
} finally {
|
||||||
|
decrementUploads(request.downloader)
|
||||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +244,7 @@ public class UploadManager {
|
|||||||
file = downloader.incompleteFile
|
file = downloader.incompleteFile
|
||||||
pieceSize = downloader.pieceSizePow2
|
pieceSize = downloader.pieceSizePow2
|
||||||
} else {
|
} else {
|
||||||
|
sharedFiles.each { it.getDownloaders().add(request.downloader.getHumanReadableName()) }
|
||||||
SharedFile sharedFile = sharedFiles.iterator().next();
|
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
||||||
file = sharedFile.file
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,7 +11,13 @@ import com.muwire.core.connection.Endpoint
|
|||||||
abstract class Uploader {
|
abstract class Uploader {
|
||||||
protected final Endpoint endpoint
|
protected final Endpoint endpoint
|
||||||
protected ByteBuffer mapped
|
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) {
|
Uploader(Endpoint endpoint) {
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
}
|
}
|
||||||
@@ -38,4 +44,34 @@ abstract class Uploader {
|
|||||||
abstract int getTotalPieces();
|
abstract int getTotalPieces();
|
||||||
|
|
||||||
abstract long getTotalSize();
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,10 @@ package com.muwire.core;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import com.muwire.core.util.DataUtil;
|
import com.muwire.core.util.DataUtil;
|
||||||
|
|
||||||
@@ -23,6 +26,8 @@ public class SharedFile {
|
|||||||
private final List<String> b64EncodedHashList;
|
private final List<String> b64EncodedHashList;
|
||||||
|
|
||||||
private volatile String comment;
|
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 {
|
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
@@ -90,6 +95,22 @@ public class SharedFile {
|
|||||||
public String getComment() {
|
public String getComment() {
|
||||||
return comment;
|
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
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
|
@@ -95,7 +95,7 @@ class ConnectionAcceptorTest {
|
|||||||
connectionEstablisher = connectionEstablisherMock.proxyInstance()
|
connectionEstablisher = connectionEstablisherMock.proxyInstance()
|
||||||
|
|
||||||
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
|
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
|
||||||
hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher)
|
||||||
acceptor.start()
|
acceptor.start()
|
||||||
Thread.sleep(100)
|
Thread.sleep(100)
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
group = com.muwire
|
group = com.muwire
|
||||||
version = 0.5.2
|
version = 0.5.5
|
||||||
i2pVersion = 0.9.42
|
i2pVersion = 0.9.43
|
||||||
groovyVersion = 2.4.15
|
groovyVersion = 2.4.15
|
||||||
slf4jVersion = 1.7.25
|
slf4jVersion = 1.7.25
|
||||||
spockVersion = 1.1-groovy-2.4
|
spockVersion = 1.1-groovy-2.4
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
application {
|
application {
|
||||||
title = 'MuWire'
|
title = 'MuWire'
|
||||||
startupGroups = ['EventList', 'MainFrame']
|
startupGroups = ['EventList', 'MainFrame', 'ShutdownWindow']
|
||||||
autoShutdown = true
|
autoShutdown = false
|
||||||
}
|
}
|
||||||
|
|
||||||
mvcGroups {
|
mvcGroups {
|
||||||
@@ -16,6 +16,11 @@ mvcGroups {
|
|||||||
view = 'com.muwire.gui.MainFrameView'
|
view = 'com.muwire.gui.MainFrameView'
|
||||||
controller = 'com.muwire.gui.MainFrameController'
|
controller = 'com.muwire.gui.MainFrameController'
|
||||||
}
|
}
|
||||||
|
'ShutdownWindow' {
|
||||||
|
model = 'com.muwire.gui.ShutdownWindowModel'
|
||||||
|
view = 'com.muwire.gui.ShutdownWindowView'
|
||||||
|
controller = 'com.muwire.gui.ShutdownWindowController'
|
||||||
|
}
|
||||||
'SearchTab' {
|
'SearchTab' {
|
||||||
model = 'com.muwire.gui.SearchTabModel'
|
model = 'com.muwire.gui.SearchTabModel'
|
||||||
view = 'com.muwire.gui.SearchTabView'
|
view = 'com.muwire.gui.SearchTabView'
|
||||||
@@ -61,4 +66,9 @@ mvcGroups {
|
|||||||
view = 'com.muwire.gui.BrowseView'
|
view = 'com.muwire.gui.BrowseView'
|
||||||
controller = 'com.muwire.gui.BrowseController'
|
controller = 'com.muwire.gui.BrowseController'
|
||||||
}
|
}
|
||||||
|
'close-warning' {
|
||||||
|
model = 'com.muwire.gui.CloseWarningModel'
|
||||||
|
view = 'com.muwire.gui.CloseWarningView'
|
||||||
|
controller = 'com.muwire.gui.CloseWarningController'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -292,6 +292,11 @@ class MainFrameController {
|
|||||||
params['core'] = core
|
params['core'] = core
|
||||||
mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
|
mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void clearUploads() {
|
||||||
|
model.uploads.removeAll { it.finished }
|
||||||
|
}
|
||||||
|
|
||||||
void saveMuWireSettings() {
|
void saveMuWireSettings() {
|
||||||
File f = new File(core.home, "MuWire.properties")
|
File f = new File(core.home, "MuWire.properties")
|
||||||
|
@@ -36,8 +36,8 @@ class MuWireStatusController {
|
|||||||
|
|
||||||
|
|
||||||
model.sharedFiles = core.fileManager.fileToSharedFile.size()
|
model.sharedFiles = core.fileManager.fileToSharedFile.size()
|
||||||
|
|
||||||
model.downloads = core.downloadManager.downloaders.size()
|
model.downloads = core.downloadManager.downloaders.size()
|
||||||
|
model.browsed = core.connectionAcceptor.browsed
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
|
@@ -69,6 +69,16 @@ class OptionsController {
|
|||||||
text = view.updateField.text
|
text = view.updateField.text
|
||||||
model.updateCheckInterval = text
|
model.updateCheckInterval = text
|
||||||
settings.updateCheckInterval = Integer.valueOf(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()
|
boolean searchComments = view.searchCommentsCheckbox.model.isSelected()
|
||||||
model.searchComments = searchComments
|
model.searchComments = searchComments
|
||||||
@@ -157,6 +167,14 @@ class OptionsController {
|
|||||||
boolean excludeLocalResult = view.excludeLocalResultCheckbox.model.isSelected()
|
boolean excludeLocalResult = view.excludeLocalResultCheckbox.model.isSelected()
|
||||||
model.excludeLocalResult = excludeLocalResult
|
model.excludeLocalResult = excludeLocalResult
|
||||||
uiSettings.excludeLocalResult = excludeLocalResult
|
uiSettings.excludeLocalResult = excludeLocalResult
|
||||||
|
|
||||||
|
boolean clearUploads = view.clearUploadsCheckbox.model.isSelected()
|
||||||
|
model.clearUploads = clearUploads
|
||||||
|
uiSettings.clearUploads = clearUploads
|
||||||
|
|
||||||
|
uiSettings.exitOnClose = model.exitOnClose
|
||||||
|
if (model.closeDecisionMade)
|
||||||
|
uiSettings.closeWarning = false
|
||||||
|
|
||||||
File uiSettingsFile = new File(core.home, "gui.properties")
|
File uiSettingsFile = new File(core.home, "gui.properties")
|
||||||
uiSettingsFile.withOutputStream {
|
uiSettingsFile.withOutputStream {
|
||||||
@@ -195,13 +213,25 @@ class OptionsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
void automaticFontAction() {
|
void automaticFont() {
|
||||||
model.automaticFontSize = true
|
model.automaticFontSize = true
|
||||||
model.customFontSize = 12
|
model.customFontSize = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
void customFontAction() {
|
void customFont() {
|
||||||
model.automaticFontSize = false
|
model.automaticFontSize = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void exitOnClose() {
|
||||||
|
model.exitOnClose = true
|
||||||
|
model.closeDecisionMade = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void minimizeOnClose() {
|
||||||
|
model.exitOnClose = false
|
||||||
|
model.closeDecisionMade = true
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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 {
|
||||||
|
}
|
@@ -10,7 +10,9 @@ import com.muwire.gui.UISettings
|
|||||||
|
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.swing.ImageIcon
|
||||||
import javax.swing.JLabel
|
import javax.swing.JLabel
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
import javax.swing.JTable
|
import javax.swing.JTable
|
||||||
import javax.swing.LookAndFeel
|
import javax.swing.LookAndFeel
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
@@ -20,7 +22,11 @@ import static griffon.util.GriffonApplicationUtils.isMacOSX
|
|||||||
import static groovy.swing.SwingBuilder.lookAndFeel
|
import static groovy.swing.SwingBuilder.lookAndFeel
|
||||||
|
|
||||||
import java.awt.Font
|
import java.awt.Font
|
||||||
|
import java.awt.MenuItem
|
||||||
|
import java.awt.PopupMenu
|
||||||
|
import java.awt.SystemTray
|
||||||
import java.awt.Toolkit
|
import java.awt.Toolkit
|
||||||
|
import java.awt.TrayIcon
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
import java.util.logging.LogManager
|
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"
|
log.info "Loading home dir"
|
||||||
def portableHome = System.getProperty("portable.home")
|
def portableHome = System.getProperty("portable.home")
|
||||||
def home = portableHome == null ?
|
def home = portableHome == null ?
|
||||||
|
@@ -18,8 +18,8 @@ class Shutdown extends AbstractLifecycleHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
void execute() {
|
void execute() {
|
||||||
log.info("shutting down")
|
log.info("shutting down from lifecycle")
|
||||||
Core core = application.context.get("core")
|
Core core = application.context.get("core")
|
||||||
core.shutdown()
|
core?.shutdown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
}
|
@@ -28,6 +28,7 @@ import com.muwire.core.download.DownloadStartedEvent
|
|||||||
import com.muwire.core.download.Downloader
|
import com.muwire.core.download.Downloader
|
||||||
import com.muwire.core.files.AllFilesLoadedEvent
|
import com.muwire.core.files.AllFilesLoadedEvent
|
||||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||||
|
import com.muwire.core.files.DirectoryWatchedEvent
|
||||||
import com.muwire.core.files.FileDownloadedEvent
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
import com.muwire.core.files.FileHashedEvent
|
import com.muwire.core.files.FileHashedEvent
|
||||||
import com.muwire.core.files.FileHashingEvent
|
import com.muwire.core.files.FileHashingEvent
|
||||||
@@ -35,6 +36,7 @@ import com.muwire.core.files.FileLoadedEvent
|
|||||||
import com.muwire.core.files.FileSharedEvent
|
import com.muwire.core.files.FileSharedEvent
|
||||||
import com.muwire.core.files.FileUnsharedEvent
|
import com.muwire.core.files.FileUnsharedEvent
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
|
import com.muwire.core.search.SearchEvent
|
||||||
import com.muwire.core.search.UIResultBatchEvent
|
import com.muwire.core.search.UIResultBatchEvent
|
||||||
import com.muwire.core.search.UIResultEvent
|
import com.muwire.core.search.UIResultEvent
|
||||||
import com.muwire.core.trust.TrustEvent
|
import com.muwire.core.trust.TrustEvent
|
||||||
@@ -45,6 +47,7 @@ import com.muwire.core.update.UpdateAvailableEvent
|
|||||||
import com.muwire.core.update.UpdateDownloadedEvent
|
import com.muwire.core.update.UpdateDownloadedEvent
|
||||||
import com.muwire.core.upload.UploadEvent
|
import com.muwire.core.upload.UploadEvent
|
||||||
import com.muwire.core.upload.UploadFinishedEvent
|
import com.muwire.core.upload.UploadFinishedEvent
|
||||||
|
import com.muwire.core.upload.Uploader
|
||||||
|
|
||||||
import griffon.core.GriffonApplication
|
import griffon.core.GriffonApplication
|
||||||
import griffon.core.artifact.GriffonModel
|
import griffon.core.artifact.GriffonModel
|
||||||
@@ -199,6 +202,7 @@ class MainFrameModel {
|
|||||||
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
||||||
core.eventBus.register(UpdateDownloadedEvent.class, this)
|
core.eventBus.register(UpdateDownloadedEvent.class, this)
|
||||||
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
|
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
|
||||||
|
core.eventBus.register(SearchEvent.class, this)
|
||||||
|
|
||||||
core.muOptions.watchedKeywords.each {
|
core.muOptions.watchedKeywords.each {
|
||||||
core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true))
|
core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true))
|
||||||
@@ -248,7 +252,7 @@ class MainFrameModel {
|
|||||||
|
|
||||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
|
core.muOptions.watchedDirectories.each { core.eventBus.publish(new DirectoryWatchedEvent(directory : new File(it))) }
|
||||||
|
|
||||||
core.muOptions.trustSubscriptions.each {
|
core.muOptions.trustSubscriptions.each {
|
||||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
|
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
|
||||||
@@ -355,6 +359,7 @@ class MainFrameModel {
|
|||||||
def dmtn = fileToNode.remove(e.unsharedFile)
|
def dmtn = fileToNode.remove(e.unsharedFile)
|
||||||
if (dmtn != null) {
|
if (dmtn != null) {
|
||||||
loadedFiles = fileToNode.size()
|
loadedFiles = fileToNode.size()
|
||||||
|
List<File> unshared = new ArrayList<>()
|
||||||
while (true) {
|
while (true) {
|
||||||
def parent = dmtn.getParent()
|
def parent = dmtn.getParent()
|
||||||
parent.remove(dmtn)
|
parent.remove(dmtn)
|
||||||
@@ -363,12 +368,16 @@ class MainFrameModel {
|
|||||||
if (parent.getChildCount() == 0) {
|
if (parent.getChildCount() == 0) {
|
||||||
File file = parent.getUserObject().file
|
File file = parent.getUserObject().file
|
||||||
if (core.muOptions.watchedDirectories.contains(file.toString()))
|
if (core.muOptions.watchedDirectories.contains(file.toString()))
|
||||||
core.eventBus.publish(new DirectoryUnsharedEvent(directory : parent.getUserObject().file))
|
unshared.add(file)
|
||||||
dmtn = parent
|
dmtn = parent
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if (!unshared.isEmpty()) {
|
||||||
|
File unsharedRoot = unshared.get( unshared.size() -1 )
|
||||||
|
core.eventBus.publish(new DirectoryUnsharedEvent(directory : unsharedRoot))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
view.refreshSharedFiles()
|
view.refreshSharedFiles()
|
||||||
}
|
}
|
||||||
@@ -376,15 +385,39 @@ class MainFrameModel {
|
|||||||
|
|
||||||
void onUploadEvent(UploadEvent e) {
|
void onUploadEvent(UploadEvent e) {
|
||||||
runInsideUIAsync {
|
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")
|
JTable table = builder.getVariable("uploads-table")
|
||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
|
view.refreshSharedFiles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onUploadFinishedEvent(UploadFinishedEvent e) {
|
void onUploadFinishedEvent(UploadFinishedEvent e) {
|
||||||
runInsideUIAsync {
|
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")
|
JTable table = builder.getVariable("uploads-table")
|
||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
@@ -416,6 +449,12 @@ class MainFrameModel {
|
|||||||
updateTablePreservingSelection("subscription-table")
|
updateTablePreservingSelection("subscription-table")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onSearchEvent(SearchEvent e) {
|
||||||
|
runInsideUIAsync {
|
||||||
|
view.refreshSharedFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onQueryEvent(QueryEvent e) {
|
void onQueryEvent(QueryEvent e) {
|
||||||
if (e.replyTo == core.me.destination)
|
if (e.replyTo == core.me.destination)
|
||||||
@@ -499,6 +538,8 @@ class MainFrameModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onRouterDisconnectedEvent(RouterDisconnectedEvent e) {
|
void onRouterDisconnectedEvent(RouterDisconnectedEvent e) {
|
||||||
|
if (core.getShutdown().get())
|
||||||
|
return
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
JOptionPane.showMessageDialog(null, "MuWire lost connection to the I2P router and will now exit.",
|
JOptionPane.showMessageDialog(null, "MuWire lost connection to the I2P router and will now exit.",
|
||||||
"Connection to I2P router lost", JOptionPane.WARNING_MESSAGE)
|
"Connection to I2P router lost", JOptionPane.WARNING_MESSAGE)
|
||||||
@@ -573,4 +614,10 @@ class MainFrameModel {
|
|||||||
boolean canDownload(InfoHash hash) {
|
boolean canDownload(InfoHash hash) {
|
||||||
!downloadInfoHashes.contains(hash)
|
!downloadInfoHashes.contains(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UploaderWrapper {
|
||||||
|
Uploader uploader
|
||||||
|
int requests
|
||||||
|
boolean finished
|
||||||
|
}
|
||||||
}
|
}
|
@@ -20,6 +20,7 @@ class MuWireStatusModel {
|
|||||||
@Observable int hopelessHosts
|
@Observable int hopelessHosts
|
||||||
@Observable int sharedFiles
|
@Observable int sharedFiles
|
||||||
@Observable int downloads
|
@Observable int downloads
|
||||||
|
@Observable int browsed
|
||||||
|
|
||||||
void mvcGroupInit(Map<String,String> args) {
|
void mvcGroupInit(Map<String,String> args) {
|
||||||
controller.refresh()
|
controller.refresh()
|
||||||
|
@@ -19,6 +19,8 @@ class OptionsModel {
|
|||||||
@Observable boolean searchComments
|
@Observable boolean searchComments
|
||||||
@Observable boolean browseFiles
|
@Observable boolean browseFiles
|
||||||
@Observable int speedSmoothSeconds
|
@Observable int speedSmoothSeconds
|
||||||
|
@Observable int totalUploadSlots
|
||||||
|
@Observable int uploadSlotsPerUser
|
||||||
|
|
||||||
// i2p options
|
// i2p options
|
||||||
@Observable String inboundLength
|
@Observable String inboundLength
|
||||||
@@ -38,6 +40,9 @@ class OptionsModel {
|
|||||||
@Observable boolean clearFinishedDownloads
|
@Observable boolean clearFinishedDownloads
|
||||||
@Observable boolean excludeLocalResult
|
@Observable boolean excludeLocalResult
|
||||||
@Observable boolean showSearchHashes
|
@Observable boolean showSearchHashes
|
||||||
|
@Observable boolean clearUploads
|
||||||
|
@Observable boolean exitOnClose
|
||||||
|
@Observable boolean closeDecisionMade
|
||||||
|
|
||||||
// bw options
|
// bw options
|
||||||
@Observable String inBw
|
@Observable String inBw
|
||||||
@@ -62,6 +67,8 @@ class OptionsModel {
|
|||||||
searchComments = settings.searchComments
|
searchComments = settings.searchComments
|
||||||
browseFiles = settings.browseFiles
|
browseFiles = settings.browseFiles
|
||||||
speedSmoothSeconds = settings.speedSmoothSeconds
|
speedSmoothSeconds = settings.speedSmoothSeconds
|
||||||
|
totalUploadSlots = settings.totalUploadSlots
|
||||||
|
uploadSlotsPerUser = settings.uploadSlotsPerUser
|
||||||
|
|
||||||
Core core = application.context.get("core")
|
Core core = application.context.get("core")
|
||||||
inboundLength = core.i2pOptions["inbound.length"]
|
inboundLength = core.i2pOptions["inbound.length"]
|
||||||
@@ -81,6 +88,8 @@ class OptionsModel {
|
|||||||
clearFinishedDownloads = uiSettings.clearFinishedDownloads
|
clearFinishedDownloads = uiSettings.clearFinishedDownloads
|
||||||
excludeLocalResult = uiSettings.excludeLocalResult
|
excludeLocalResult = uiSettings.excludeLocalResult
|
||||||
showSearchHashes = uiSettings.showSearchHashes
|
showSearchHashes = uiSettings.showSearchHashes
|
||||||
|
clearUploads = uiSettings.clearUploads
|
||||||
|
exitOnClose = uiSettings.exitOnClose
|
||||||
|
|
||||||
if (core.router != null) {
|
if (core.router != null) {
|
||||||
inBw = String.valueOf(settings.inBw)
|
inBw = String.valueOf(settings.inBw)
|
||||||
|
@@ -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 {
|
||||||
|
}
|
59
gui/griffon-app/views/com/muwire/gui/CloseWarningView.groovy
Normal file
59
gui/griffon-app/views/com/muwire/gui/CloseWarningView.groovy
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
package com.muwire.gui
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import griffon.core.GriffonApplication
|
||||||
import griffon.core.artifact.GriffonView
|
import griffon.core.artifact.GriffonView
|
||||||
import griffon.core.env.Metadata
|
import griffon.core.env.Metadata
|
||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
@@ -11,6 +12,7 @@ import javax.swing.BorderFactory
|
|||||||
import javax.swing.Box
|
import javax.swing.Box
|
||||||
import javax.swing.BoxLayout
|
import javax.swing.BoxLayout
|
||||||
import javax.swing.JFileChooser
|
import javax.swing.JFileChooser
|
||||||
|
import javax.swing.JFrame
|
||||||
import javax.swing.JLabel
|
import javax.swing.JLabel
|
||||||
import javax.swing.JMenuItem
|
import javax.swing.JMenuItem
|
||||||
import javax.swing.JPopupMenu
|
import javax.swing.JPopupMenu
|
||||||
@@ -19,6 +21,7 @@ import javax.swing.JTable
|
|||||||
import javax.swing.JTree
|
import javax.swing.JTree
|
||||||
import javax.swing.ListSelectionModel
|
import javax.swing.ListSelectionModel
|
||||||
import javax.swing.SwingConstants
|
import javax.swing.SwingConstants
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.TransferHandler
|
import javax.swing.TransferHandler
|
||||||
import javax.swing.border.Border
|
import javax.swing.border.Border
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
@@ -26,6 +29,7 @@ import javax.swing.tree.TreeNode
|
|||||||
import javax.swing.tree.TreePath
|
import javax.swing.tree.TreePath
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.Core
|
||||||
import com.muwire.core.MuWireSettings
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
import com.muwire.core.download.Downloader
|
import com.muwire.core.download.Downloader
|
||||||
@@ -42,6 +46,8 @@ import java.awt.datatransfer.DataFlavor
|
|||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
@@ -54,6 +60,7 @@ class MainFrameView {
|
|||||||
@MVCMember @Nonnull
|
@MVCMember @Nonnull
|
||||||
MainFrameModel model
|
MainFrameModel model
|
||||||
|
|
||||||
|
@Inject @Nonnull GriffonApplication application
|
||||||
@Inject Metadata metadata
|
@Inject Metadata metadata
|
||||||
|
|
||||||
def downloadsTable
|
def downloadsTable
|
||||||
@@ -68,6 +75,7 @@ class MainFrameView {
|
|||||||
builder.with {
|
builder.with {
|
||||||
application(size : [1024,768], id: 'main-frame',
|
application(size : [1024,768], id: 'main-frame',
|
||||||
locationRelativeTo : null,
|
locationRelativeTo : null,
|
||||||
|
defaultCloseOperation : JFrame.DO_NOTHING_ON_CLOSE,
|
||||||
title: application.configuration['application.title'] + " " +
|
title: application.configuration['application.title'] + " " +
|
||||||
metadata["application.version"] + " revision " + metadata["build.revision"],
|
metadata["application.version"] + " revision " + metadata["build.revision"],
|
||||||
iconImage: imageIcon('/MuWire-48x48.png').image,
|
iconImage: imageIcon('/MuWire-48x48.png').image,
|
||||||
@@ -77,6 +85,9 @@ class MainFrameView {
|
|||||||
pack : false,
|
pack : false,
|
||||||
visible : bind { model.coreInitialized }) {
|
visible : bind { model.coreInitialized }) {
|
||||||
menuBar {
|
menuBar {
|
||||||
|
menu (text : "File") {
|
||||||
|
menuItem("Exit", actionPerformed : {closeApplication()})
|
||||||
|
}
|
||||||
menu (text : "Options") {
|
menu (text : "Options") {
|
||||||
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
|
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
|
||||||
menuItem("Content Control", actionPerformed : {
|
menuItem("Content Control", actionPerformed : {
|
||||||
@@ -215,8 +226,10 @@ class MainFrameView {
|
|||||||
table(id : "shared-files-table", autoCreateRowSorter: true) {
|
table(id : "shared-files-table", autoCreateRowSorter: true) {
|
||||||
tableModel(list : model.shared) {
|
tableModel(list : model.shared) {
|
||||||
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
|
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
|
||||||
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() })
|
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.getCachedLength() })
|
||||||
closureColumn(header : "Comments", preferredWidth : 100, type : Boolean, read : {it.getComment() != null})
|
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()})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,31 +271,40 @@ class MainFrameView {
|
|||||||
scrollPane (constraints : BorderLayout.CENTER) {
|
scrollPane (constraints : BorderLayout.CENTER) {
|
||||||
table(id : "uploads-table") {
|
table(id : "uploads-table") {
|
||||||
tableModel(list : model.uploads) {
|
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 ->
|
closureColumn(header : "Progress", type : String, read : { row ->
|
||||||
int percent = row.getProgress()
|
int percent = row.uploader.getProgress()
|
||||||
"$percent% of piece".toString()
|
"$percent% of piece".toString()
|
||||||
})
|
})
|
||||||
closureColumn(header : "Downloader", type : String, read : { row ->
|
closureColumn(header : "Downloader", type : String, read : { row ->
|
||||||
row.getDownloader()
|
row.uploader.getDownloader()
|
||||||
})
|
})
|
||||||
closureColumn(header : "Remote Pieces", type : String, read : { row ->
|
closureColumn(header : "Remote Pieces", type : String, read : { row ->
|
||||||
int pieces = row.getTotalPieces()
|
int pieces = row.uploader.getTotalPieces()
|
||||||
int done = row.getDonePieces()
|
int done = row.uploader.getDonePieces()
|
||||||
|
if (row.uploader.getProgress() == 100)
|
||||||
|
done++
|
||||||
int percent = -1
|
int percent = -1
|
||||||
if ( pieces != 0 ) {
|
if ( pieces != 0 ) {
|
||||||
percent = (done * 100) / pieces
|
percent = (done * 100) / pieces
|
||||||
}
|
}
|
||||||
long size = row.getTotalSize()
|
long size = row.uploader.getTotalSize()
|
||||||
String totalSize = ""
|
String totalSize = ""
|
||||||
if (size >= 0 ) {
|
if (size >= 0 ) {
|
||||||
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
|
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
|
||||||
}
|
}
|
||||||
String.format("%02d", percent) + "% ${totalSize} ($done/$pieces pcs)".toString()
|
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") {
|
panel (constraints: "monitor window") {
|
||||||
@@ -432,6 +454,23 @@ class MainFrameView {
|
|||||||
true
|
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()
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
|
||||||
def downloadsTable = builder.getVariable("downloads-table")
|
def downloadsTable = builder.getVariable("downloads-table")
|
||||||
def selectionModel = downloadsTable.getSelectionModel()
|
def selectionModel = downloadsTable.getSelectionModel()
|
||||||
@@ -894,4 +933,18 @@ class MainFrameView {
|
|||||||
tree.setSelectionPaths(selectedPaths)
|
tree.setSelectionPaths(selectedPaths)
|
||||||
builder.getVariable("shared-files-table").model.fireTableDataChanged()
|
builder.getVariable("shared-files-table").model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void closeApplication() {
|
||||||
|
def mainFrame = builder.getVariable("main-frame")
|
||||||
|
mainFrame.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -62,6 +62,8 @@ class MuWireStatusView {
|
|||||||
label(text : bind {model.sharedFiles}, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
|
label(text : bind {model.sharedFiles}, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
|
||||||
label(text : "Downloading", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
label(text : "Downloading", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||||
label(text : bind {model.downloads}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
|
label(text : bind {model.downloads}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
|
||||||
|
label(text : "Times Browsed", constraints : gbc(gridx:0, gridy:2, anchor: GridBagConstraints.LINE_START, weightx: 100))
|
||||||
|
label(text : bind {model.browsed}, constraints : gbc(gridx: 1, gridy: 2, anchor : GridBagConstraints.LINE_END))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buttonsPanel = builder.panel {
|
buttonsPanel = builder.panel {
|
||||||
|
@@ -42,6 +42,8 @@ class OptionsView {
|
|||||||
def searchCommentsCheckbox
|
def searchCommentsCheckbox
|
||||||
def browseFilesCheckbox
|
def browseFilesCheckbox
|
||||||
def speedSmoothSecondsField
|
def speedSmoothSecondsField
|
||||||
|
def totalUploadSlotsField
|
||||||
|
def uploadSlotsPerUserField
|
||||||
|
|
||||||
def inboundLengthField
|
def inboundLengthField
|
||||||
def inboundQuantityField
|
def inboundQuantityField
|
||||||
@@ -58,6 +60,7 @@ class OptionsView {
|
|||||||
def clearFinishedDownloadsCheckbox
|
def clearFinishedDownloadsCheckbox
|
||||||
def excludeLocalResultCheckbox
|
def excludeLocalResultCheckbox
|
||||||
def showSearchHashesCheckbox
|
def showSearchHashesCheckbox
|
||||||
|
def clearUploadsCheckbox
|
||||||
|
|
||||||
def inBwField
|
def inBwField
|
||||||
def outBwField
|
def outBwField
|
||||||
@@ -106,8 +109,19 @@ class OptionsView {
|
|||||||
button(text : "Choose", constraints : gbc(gridx : 2, gridy:2), incompleteLocationAction)
|
button(text : "Choose", constraints : gbc(gridx : 2, gridy:2), incompleteLocationAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
panel (border : titledBorder(title : "Upload Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
||||||
|
constraints : gbc(gridx : 0, gridy:2, fill : GridBagConstraints.HORIZONTAL))) {
|
||||||
|
gridBagLayout()
|
||||||
|
label(text : "Total upload slots (-1 means unlimited)", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||||
|
totalUploadSlotsField = textField(text : bind {model.totalUploadSlots}, columns: 2,
|
||||||
|
constraints : gbc(gridx : 1, gridy: 0, anchor : GridBagConstraints.LINE_END))
|
||||||
|
label(text : "Upload slots per user (-1 means unlimited)", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||||
|
uploadSlotsPerUserField = textField(text : bind {model.uploadSlotsPerUser}, columns: 2,
|
||||||
|
constraints : gbc(gridx : 1, gridy: 1, anchor : GridBagConstraints.LINE_END))
|
||||||
|
}
|
||||||
|
|
||||||
panel (border : titledBorder(title : "Sharing Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
panel (border : titledBorder(title : "Sharing Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
||||||
constraints : gbc(gridx : 0, gridy : 2, fill : GridBagConstraints.HORIZONTAL))) {
|
constraints : gbc(gridx : 0, gridy : 3, fill : GridBagConstraints.HORIZONTAL))) {
|
||||||
gridBagLayout()
|
gridBagLayout()
|
||||||
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||||
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:0, weightx : 0))
|
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:0, weightx : 0))
|
||||||
@@ -117,7 +131,7 @@ class OptionsView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
panel (border : titledBorder(title : "Update Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
panel (border : titledBorder(title : "Update Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
||||||
constraints : gbc(gridx : 0, gridy : 3, fill : GridBagConstraints.HORIZONTAL))) {
|
constraints : gbc(gridx : 0, gridy : 4, fill : GridBagConstraints.HORIZONTAL))) {
|
||||||
gridBagLayout()
|
gridBagLayout()
|
||||||
label(text : "Check for updates every (hours)", constraints : gbc(gridx : 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
label(text : "Check for updates every (hours)", constraints : gbc(gridx : 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||||
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 0, weightx: 0))
|
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 0, weightx: 0))
|
||||||
@@ -188,15 +202,24 @@ class OptionsView {
|
|||||||
label(text : "Automatically clear cancelled downloads", constraints: gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
label(text : "Automatically clear cancelled downloads", constraints: gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||||
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads},
|
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads},
|
||||||
constraints : gbc(gridx : 1, gridy:0, anchor : GridBagConstraints.LINE_END))
|
constraints : gbc(gridx : 1, gridy:0, anchor : GridBagConstraints.LINE_END))
|
||||||
label(text : "Automatically flear finished downloads", constraints: gbc(gridx: 0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
label(text : "Automatically clear finished downloads", constraints: gbc(gridx: 0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||||
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads},
|
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads},
|
||||||
constraints : gbc(gridx : 1, gridy:1, anchor : GridBagConstraints.LINE_END))
|
constraints : gbc(gridx : 1, gridy:1, anchor : GridBagConstraints.LINE_END))
|
||||||
label(text : "Smooth download speed over (seconds)", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
label(text : "Smooth download speed over (seconds)", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||||
speedSmoothSecondsField = textField(text : bind {model.speedSmoothSeconds},
|
speedSmoothSecondsField = textField(text : bind {model.speedSmoothSeconds},
|
||||||
constraints : gbc(gridx:1, gridy: 2, anchor : GridBagConstraints.LINE_START))
|
constraints : gbc(gridx:1, gridy: 2, anchor : GridBagConstraints.LINE_END))
|
||||||
label(text : "Exclude local files from results", constraints: gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
label(text : "Exclude local files from results", constraints: gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||||
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult},
|
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult},
|
||||||
constraints : gbc(gridx: 1, gridy : 3, anchor : GridBagConstraints.LINE_END))
|
constraints : gbc(gridx: 1, gridy : 3, anchor : GridBagConstraints.LINE_END))
|
||||||
|
label(text : "Automatically Clear finished uploads", constraints:gbc(gridx:0, gridy:4, anchor: GridBagConstraints.LINE_START, weightx : 100))
|
||||||
|
clearUploadsCheckbox = checkBox(selected : bind {model.clearUploads},
|
||||||
|
constraints : gbc(gridx:1, gridy: 4, anchor:GridBagConstraints.LINE_END))
|
||||||
|
label(text : "When closing MuWire", constraints : gbc(gridx: 0, gridy : 5, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||||
|
panel (constraints : gbc(gridx:1, gridy: 5, anchor : GridBagConstraints.LINE_END)) {
|
||||||
|
buttonGroup(id : "closeBehaviorGroup")
|
||||||
|
radioButton(text : "Minimize to tray", selected : bind {!model.exitOnClose}, buttonGroup: closeBehaviorGroup, minimizeOnCloseAction)
|
||||||
|
radioButton(text : "Exit", selected : bind {model.exitOnClose}, buttonGroup : closeBehaviorGroup, exitOnCloseAction)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
panel (constraints : gbc(gridx: 0, gridy: 2, weighty: 100))
|
panel (constraints : gbc(gridx: 0, gridy: 2, weighty: 100))
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,40 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import griffon.core.GriffonApplication
|
||||||
|
import griffon.core.artifact.GriffonView
|
||||||
|
import griffon.inject.MVCMember
|
||||||
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
|
import javax.swing.Box
|
||||||
|
import javax.swing.SwingConstants
|
||||||
|
import javax.annotation.Nonnull
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ArtifactProviderFor(GriffonView)
|
||||||
|
class ShutdownWindowView {
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
FactoryBuilderSupport builder
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
ShutdownWindowModel model
|
||||||
|
|
||||||
|
void initUI() {
|
||||||
|
builder.with {
|
||||||
|
app = application(size: [320, 80], id: 'shutdown-window',
|
||||||
|
locationRelativeTo : null,
|
||||||
|
title: application.configuration['application.title'],
|
||||||
|
iconImage: imageIcon('/MuWire-48x48.png').image,
|
||||||
|
iconImages: [imageIcon('/MuWire-48x48.png').image,
|
||||||
|
imageIcon('/MuWire-32x32.png').image,
|
||||||
|
imageIcon('/MuWire-16x16.png').image],
|
||||||
|
visible: false ) {
|
||||||
|
panel {
|
||||||
|
vbox {
|
||||||
|
label("MuWire is shutting down, please wait...")
|
||||||
|
Box.createVerticalGlue()
|
||||||
|
progressBar(indeterminate : true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,9 @@ class UISettings {
|
|||||||
boolean clearFinishedDownloads
|
boolean clearFinishedDownloads
|
||||||
boolean excludeLocalResult
|
boolean excludeLocalResult
|
||||||
boolean showSearchHashes
|
boolean showSearchHashes
|
||||||
|
boolean closeWarning
|
||||||
|
boolean exitOnClose
|
||||||
|
boolean clearUploads
|
||||||
|
|
||||||
UISettings(Properties props) {
|
UISettings(Properties props) {
|
||||||
lnf = props.getProperty("lnf", "system")
|
lnf = props.getProperty("lnf", "system")
|
||||||
@@ -22,6 +25,9 @@ class UISettings {
|
|||||||
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true"))
|
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true"))
|
||||||
autoFontSize = Boolean.parseBoolean(props.getProperty("autoFontSize","false"))
|
autoFontSize = Boolean.parseBoolean(props.getProperty("autoFontSize","false"))
|
||||||
fontSize = Integer.parseInt(props.getProperty("fontSize","12"))
|
fontSize = Integer.parseInt(props.getProperty("fontSize","12"))
|
||||||
|
closeWarning = Boolean.parseBoolean(props.getProperty("closeWarning","true"))
|
||||||
|
exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false"))
|
||||||
|
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads","false"))
|
||||||
}
|
}
|
||||||
|
|
||||||
void write(OutputStream out) throws IOException {
|
void write(OutputStream out) throws IOException {
|
||||||
@@ -34,6 +40,9 @@ class UISettings {
|
|||||||
props.setProperty("showSearchHashes", String.valueOf(showSearchHashes))
|
props.setProperty("showSearchHashes", String.valueOf(showSearchHashes))
|
||||||
props.setProperty("autoFontSize", String.valueOf(autoFontSize))
|
props.setProperty("autoFontSize", String.valueOf(autoFontSize))
|
||||||
props.setProperty("fontSize", String.valueOf(fontSize))
|
props.setProperty("fontSize", String.valueOf(fontSize))
|
||||||
|
props.setProperty("closeWarning", String.valueOf(closeWarning))
|
||||||
|
props.setProperty("exitOnClose", String.valueOf(exitOnClose))
|
||||||
|
props.setProperty("clearUploads", String.valueOf(clearUploads))
|
||||||
if (font != null)
|
if (font != null)
|
||||||
props.setProperty("font", font)
|
props.setProperty("font", font)
|
||||||
|
|
||||||
|
@@ -4,5 +4,6 @@ include 'update-server'
|
|||||||
include 'core'
|
include 'core'
|
||||||
include 'gui'
|
include 'gui'
|
||||||
include 'cli'
|
include 'cli'
|
||||||
|
include 'cli-lanterna'
|
||||||
// include 'webui'
|
// include 'webui'
|
||||||
// include 'plug'
|
// include 'plug'
|
||||||
|
Reference in New Issue
Block a user