Compare commits

...

42 Commits

Author SHA1 Message Date
Zlatin Balevsky
ff1df88601 Release 0.5.7 2019-11-03 12:35:04 +00:00
Zlatin Balevsky
4ed572ba51 clear search button 2019-11-03 12:03:12 +00:00
Zlatin Balevsky
fd3f55ab4d implement restore session 2019-11-03 10:06:55 +00:00
Zlatin Balevsky
1358e14467 add options for search history 2019-11-03 08:12:10 +00:00
Zlatin Balevsky
e22d5fea11 better search box 2019-11-03 01:50:55 +00:00
Zlatin Balevsky
7ade4aa10d set row height to trees 2019-11-02 19:06:26 +00:00
Zlatin Balevsky
a9f623a91a correct method name 2019-11-02 18:51:02 +00:00
Zlatin Balevsky
1ce410e943 wip on signing queries 2019-11-02 18:34:13 +00:00
Zlatin Balevsky
27aad9d75d do not collapse tree on updates pt2 2019-11-02 17:41:04 +00:00
Zlatin Balevsky
24591b10f2 change the griffon environment 2019-11-02 10:13:28 -07:00
Zlatin Balevsky
e4f1ea5c10 make table rows a bit larger 2019-11-02 15:58:48 +00:00
Zlatin Balevsky
c73c44c5f2 base table row height on the size of the font 2019-11-02 15:46:50 +00:00
Zlatin Balevsky
309cbcc580 UTF-8 in props of cli 2019-11-02 15:23:15 +00:00
Zlatin Balevsky
86894f242b support UTF-8 in persona names 2019-11-02 14:43:24 +00:00
Zlatin Balevsky
568255140f visualize the negative tree as well 2019-11-02 12:54:43 +00:00
Zlatin Balevsky
f6d2bac5bb show all watched directories 2019-11-02 12:26:19 +00:00
Zlatin Balevsky
1c396711ed Fix sidecar files larger than the limit from being shared 2019-11-02 11:15:08 +00:00
Zlatin Balevsky
c154d9538d only check negative tree for files, not directories 2019-11-02 10:28:04 +00:00
Zlatin Balevsky
8043782446 logging config with all logs turned off 2019-11-02 08:52:29 +00:00
Zlatin Balevsky
00c529cca1 toString() 2019-11-02 00:40:08 +00:00
Zlatin Balevsky
094b9ac2b0 restore behavior where watched directories get scanned on startup 2019-11-02 00:27:12 +00:00
Zlatin Balevsky
0dae0a561b more accurate speed measurement. Makes a difference if MW is minimized for a long time 2019-11-01 18:39:41 +00:00
Zlatin Balevsky
82eaafc2c3 Release 0.5.6 2019-10-31 23:22:13 +00:00
Zlatin Balevsky
a3fc1a62e7 format the I2P bandwidths 2019-10-31 21:52:22 +00:00
Zlatin Balevsky
2fd8f45107 update text in cli 2019-10-31 21:22:50 +00:00
Zlatin Balevsky
2429bbf59e Add update notification window 2019-10-31 20:51:09 +00:00
Zlatin Balevsky
f7e28e04f6 add a system status panel 2019-10-31 14:14:14 +00:00
Zlatin Balevsky
cc0188f20e show used memory, not free memory 2019-10-31 13:46:16 +00:00
Zlatin Balevsky
af9b4f4679 change package name for cli 2019-10-31 13:05:42 +00:00
Zlatin Balevsky
625a559d02 change package name 2019-10-31 13:02:44 +00:00
Zlatin Balevsky
6e20193d57 properly set Xmx 2019-10-31 07:15:54 +00:00
Zlatin Balevsky
88ac267f99 show java version and ram usage in cli 2019-10-31 07:14:52 +00:00
Zlatin Balevsky
9b3a7473d1 limit Xmx on cli-lanterna too 2019-10-31 06:52:56 +00:00
Zlatin Balevsky
5b0180280e fix changing font and size on metal lnf 2019-10-30 22:20:27 +00:00
Zlatin Balevsky
d0462034fc enforce comment length in cli as well 2019-10-30 21:51:16 +00:00
Zlatin Balevsky
f3e4098107 refresh gui when processing a sidecar file 2019-10-30 21:45:38 +00:00
Zlatin Balevsky
26e7ca0b21 enforce maximum comment length in the gui 2019-10-30 21:22:08 +00:00
Zlatin Balevsky
11007e5f19 allow up to exact max comment length 2019-10-30 21:20:09 +00:00
Zlatin Balevsky
ae651cb6bd implement sidecar files 2019-10-30 21:07:59 +00:00
Zlatin Balevsky
cad3a88517 Xmx256M by default 2019-10-30 21:06:33 +00:00
Zlatin Balevsky
29c81646af word-wrap the comment views 2019-10-30 19:52:37 +00:00
Zlatin Balevsky
8a0257927b Link to CLI configuration options 2019-10-30 19:43:51 +00:00
59 changed files with 1118 additions and 168 deletions

View File

@@ -4,7 +4,7 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.5.3 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
The current stable release - 0.5.5 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
### Building
@@ -31,7 +31,7 @@ If you have an I2P router running on the same machine that is all you need to do
### 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.
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
The CLI is under active development and doesn't have all the features of the GUI.

View File

@@ -11,10 +11,14 @@ buildscript {
}
apply plugin : 'application'
mainClassName = 'com.muwire.clilanterna.CliLanterna'
application {
mainClassName = 'com.muwire.clilanterna.CliLanterna'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
applicationName = 'MuWire-cli'
}
apply plugin : 'com.github.johnrengelman.shadow'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile project(":core")

View File

@@ -3,6 +3,8 @@ package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
@@ -10,6 +12,7 @@ import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.SharedFile
import com.muwire.core.files.UICommentEvent
@@ -49,11 +52,16 @@ class AddCommentView extends BasicWindow {
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()
if (newComment.length() > Constants.MAX_COMMENT_LENGTH) {
String error = "Your comment is too long - ${newComment.length()} bytes. Maximum is $Constants.MAX_COMMENT_LENGTH bytes"
MessageDialog.showMessageDialog(textGUI, "Comment Too Long", error, MessageDialogButton.Close)
} else {
newComment = Base64.encode(DataUtil.encodei18nString(newComment))
String encodedOldComment = sharedFile.getComment()
sharedFile.setComment(newComment)
core.eventBus.publish(new UICommentEvent(sharedFile : sharedFile, oldComment : encodedOldComment))
close()
}
})
Button cancelButton = new Button("Cancel", {close()})

View File

@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.5.5"
private static final String MW_VERSION = "0.5.7"
private static volatile Core core
@@ -82,14 +82,14 @@ class CliLanterna {
props.setDownloadLocation(downloadLocationFile)
props.incompleteLocation = incompletesLocationFile
propsFile.withOutputStream {
propsFile.withPrintWriter("UTF-8", {
props.write(it)
}
})
} else {
props = new Properties()
propsFile.withInputStream {
propsFile.withReader("UTF-8", {
props.load(it)
}
})
props = new MuWireSettings(props)
}
props.updateType = "cli-lanterna"

View File

@@ -30,6 +30,7 @@ import com.muwire.core.update.UpdateAvailableEvent
import com.muwire.core.update.UpdateDownloadedEvent
import net.i2p.data.Base64
import net.i2p.data.DataHelper
class MainWindowView extends BasicWindow {
@@ -49,6 +50,7 @@ class MainWindowView extends BasicWindow {
private final Label sharedFiles
private final Label timesBrowsed
private final Label updateStatus
private final Label usedRam, totalRam, maxRam
public MainWindowView(String title, Core core, TextGUI textGUI, Screen screen, CliSettings props) {
super(title);
@@ -130,6 +132,9 @@ class MainWindowView extends BasicWindow {
sharedFiles = new Label("0")
timesBrowsed = new Label("0")
updateStatus = new Label("Unknown")
usedRam = new Label("0")
maxRam = new Label("0")
totalRam = new Label("0")
statusPanel.with {
addComponent(new Label("Incoming Connections: "), layoutData)
@@ -148,6 +153,14 @@ class MainWindowView extends BasicWindow {
addComponent(timesBrowsed, layoutData)
addComponent(new Label("Update Status: "), layoutData)
addComponent(updateStatus, layoutData)
addComponent(new Label("Java Version: "), layoutData)
addComponent(new Label(System.getProperty("java.vendor")+ " " + System.getProperty("java.version")), layoutData)
addComponent(new Label("Used Memory: "), layoutData)
addComponent(usedRam, layoutData)
addComponent(new Label("Total Memory: "), layoutData)
addComponent(totalRam, layoutData)
addComponent(new Label("Maximum Memory: "), layoutData)
addComponent(maxRam, layoutData)
}
refreshStats()
@@ -204,8 +217,11 @@ class MainWindowView extends BasicWindow {
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)
String message = "Version $e.version is available, with hash $e.infoHash . Show details?"
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
if (button == MessageDialogButton.No)
return
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
}
}
@@ -213,8 +229,11 @@ class MainWindowView extends BasicWindow {
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)
String message = "MuWire version $e.version has been downloaded. Show details?."
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
if (button == MessageDialogButton.No)
return
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
}
}
@@ -266,6 +285,17 @@ class MainWindowView extends BasicWindow {
int hopelessHosts = core.hostCache.countHopelessHosts()
int shared = core.fileManager.fileToSharedFile.size()
int browsed = core.connectionAcceptor.browsed
long freeMemL = Runtime.getRuntime().freeMemory()
long totalMemL = Runtime.getRuntime().totalMemory()
String usedMem = DataHelper.formatSize2Decimal(freeMemL, false) + "B"
String totalMem = DataHelper.formatSize2Decimal(totalMemL, false)+"B"
String maxMem
long maxMemL = Runtime.getRuntime().maxMemory()
if (maxMemL >= Long.MAX_VALUE / 2)
maxMem = "Unlimited"
else
maxMem = DataHelper.formatSize2Decimal(maxMemL, false) + "B"
incoming.setText(String.valueOf(inCon))
outgoing.setText(String.valueOf(outCon))
@@ -274,5 +304,8 @@ class MainWindowView extends BasicWindow {
hopeless.setText(String.valueOf(hopelessHosts))
sharedFiles.setText(String.valueOf(shared))
timesBrowsed.setText(String.valueOf(browsed))
usedRam.setText(usedMem)
totalRam.setText(totalMem)
maxRam.setText(maxMem)
}
}

View File

@@ -8,7 +8,11 @@ import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import java.nio.charset.StandardCharsets
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
@@ -40,20 +44,27 @@ class SearchModel {
}
def searchEvent
byte [] payload
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
payload = root
} else {
def replaced = query.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
def terms = replaced.split(" ")
def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
}
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
originator : core.me, sig: sig.data))
}
void unregister() {

View File

@@ -199,6 +199,6 @@ class TrustView extends BasicWindow {
private void saveMuSettings() {
File settingsFile = new File(core.home,"MuWire.properties")
settingsFile.withOutputStream { core.muOptions.write(it) }
settingsFile.withPrintWriter("UTF-8",{ core.muOptions.write(it) })
}
}

View File

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

View File

@@ -28,6 +28,7 @@ import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService
import com.muwire.core.files.SideCarFileEvent
import com.muwire.core.files.UICommentEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.files.AllFilesLoadedEvent
@@ -102,6 +103,8 @@ public class Core {
private final Router router
final AtomicBoolean shutdown = new AtomicBoolean()
final SigningPrivateKey spk
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
@@ -179,7 +182,7 @@ public class Core {
i2pSession = socketManager.getSession()
def destination = new Destination()
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
spk = new SigningPrivateKey(Constants.SIG_TYPE)
keyDat.withInputStream {
destination.readBytes(it)
def privateKey = new PrivateKey()
@@ -190,8 +193,9 @@ public class Core {
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.write(Constants.PERSONA_VERSION)
daos.writeShort((short)props.getNickname().length())
daos.write(props.getNickname().getBytes(StandardCharsets.UTF_8))
byte [] name = props.getNickname().getBytes(StandardCharsets.UTF_8)
daos.writeShort((short)name.length)
daos.write(name)
destination.writeBytes(daos)
daos.flush()
byte [] payload = baos.toByteArray()
@@ -221,6 +225,7 @@ public class Core {
eventBus.register(SearchEvent.class, fileManager)
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
eventBus.register(UICommentEvent.class, fileManager)
eventBus.register(SideCarFileEvent.class, fileManager)
log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager, home, props)
@@ -333,8 +338,7 @@ public class Core {
return
}
log.info("saving settings")
File f = new File(home, "MuWire.properties")
f.withOutputStream { muOptions.write(it) }
saveMuSettings()
log.info("shutting down trust subscriber")
trustSubscriber.stop()
log.info("shutting down download manageer")
@@ -355,6 +359,11 @@ public class Core {
}
log.info("shutdown complete")
}
public void saveMuSettings() {
File f = new File(home, "MuWire.properties")
f.withPrintWriter("UTF-8", { muOptions.write(it) })
}
static main(args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"
@@ -380,7 +389,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.5.5")
Core core = new Core(props, home, "0.5.7")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -78,10 +78,10 @@ class MuWireSettings {
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
watchedDirectories = readEncodedSet(props, "watchedDirectories")
watchedKeywords = readEncodedSet(props, "watchedKeywords")
watchedRegexes = readEncodedSet(props, "watchedRegexes")
negativeFileTree = readEncodedSet(props, "negativeFileTree")
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
watchedRegexes = DataUtil.readEncodedSet(props, "watchedRegexes")
negativeFileTree = DataUtil.readEncodedSet(props, "negativeFileTree")
trustSubscriptions = new HashSet<>()
if (props.containsKey("trustSubscriptions")) {
@@ -93,7 +93,7 @@ class MuWireSettings {
}
void write(OutputStream out) throws IOException {
void write(Writer out) throws IOException {
Properties props = new Properties()
props.setProperty("leaf", isLeaf.toString())
props.setProperty("allowUntrusted", allowUntrusted.toString())
@@ -125,10 +125,10 @@ class MuWireSettings {
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
writeEncodedSet(negativeFileTree, "negativeFileTree", props)
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
DataUtil.writeEncodedSet(watchedRegexes, "watchedRegexes", props)
DataUtil.writeEncodedSet(negativeFileTree, "negativeFileTree", props)
if (!trustSubscriptions.isEmpty()) {
String encoded = trustSubscriptions.stream().
@@ -137,25 +137,7 @@ class MuWireSettings {
props.setProperty("trustSubscriptions", encoded)
}
props.store(out, "")
}
private static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new ConcurrentHashSet<>()
if (props.containsKey(property)) {
String[] encoded = props.getProperty(property).split(",")
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
}
rv
}
private static void writeEncodedSet(Set<String> set, String property, Properties props) {
if (set.isEmpty())
return
String encoded = set.stream().
map({Base64.encode(DataUtil.encodei18nString(it))}).
collect(Collectors.joining(","))
props.setProperty(property, encoded)
props.store(out, "This file is UTF-8")
}
boolean isLeaf() {

View File

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

View File

@@ -1,5 +1,6 @@
package com.muwire.core.connection
import java.nio.charset.StandardCharsets
import java.util.concurrent.BlockingQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutorService
@@ -10,6 +11,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
@@ -21,8 +23,10 @@ import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.data.Signature
@Log
abstract class Connection implements Closeable {
@@ -147,6 +151,8 @@ abstract class Connection implements Closeable {
query.replyTo = e.replyTo.toBase64()
if (e.originator != null)
query.originator = e.originator.toBase64()
if (e.sig != null)
query.sig = Base64.encode(e.sig)
messages.put(query)
}
@@ -225,6 +231,24 @@ abstract class Connection implements Closeable {
boolean compressedResults = false
if (search.compressedResults != null)
compressedResults = search.compressedResults
byte[] sig = null
// TODO: make this mandatory at some point
if (search.sig != null) {
sig = Base64.decode(search.sig)
byte [] payload
if (infohash != null)
payload = infohash
else
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig)
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
log.info("signature didn't match keywords")
return
} else
log.info("query signature verified")
} else
log.info("no signature in query")
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash,
@@ -236,7 +260,8 @@ abstract class Connection implements Closeable {
replyTo : replyTo,
originator : originator,
receivedOn : endpoint.destination,
firstHop : search.firstHop )
firstHop : search.firstHop,
sig : sig )
eventBus.publish(event)
}

View File

@@ -21,6 +21,7 @@ import java.nio.file.Files
import java.nio.file.StandardOpenOption
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Level
@Log
@@ -37,13 +38,12 @@ class DownloadSession {
private final Set<Integer> available
private final MessageDigest digest
private long lastSpeedRead = System.currentTimeMillis()
private long dataSinceLastRead
private final AtomicLong dataSinceLastRead
private MappedByteBuffer mapped
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength, Set<Integer> available) {
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead) {
this.eventBus = eventBus
this.meB64 = meB64
this.pieces = pieces
@@ -53,6 +53,7 @@ class DownloadSession {
this.pieceSize = pieceSize
this.fileLength = fileLength
this.available = available
this.dataSinceLastRead = dataSinceLastRead
try {
digest = MessageDigest.getInstance("SHA-256")
} catch (NoSuchAlgorithmException impossible) {
@@ -190,7 +191,7 @@ class DownloadSession {
throw new IOException()
synchronized(this) {
mapped.put(tmp, 0, read)
dataSinceLastRead += read
dataSinceLastRead.addAndGet(read)
pieces.markPartial(piece, mapped.position())
}
}
@@ -222,13 +223,4 @@ class DownloadSession {
return 0
mapped.position()
}
synchronized int speed() {
final long now = System.currentTimeMillis()
long interval = Math.max(1000, now - lastSpeedRead)
lastSpeedRead = now;
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
dataSinceLastRead = 0
rv
}
}

View File

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

View File

@@ -86,9 +86,9 @@ class DirectoryWatcher {
private void saveMuSettings() {
File muSettingsFile = new File(home, "MuWire.properties")
muSettingsFile.withOutputStream {
muSettingsFile.withPrintWriter("UTF-8", {
muOptions.write(it)
}
})
}
private void watch() {

View File

@@ -25,6 +25,7 @@ class FileManager {
final Map<String, Set<File>> commentToFile = new HashMap<>()
final SearchIndex index = new SearchIndex()
final FileTree negativeTree = new FileTree()
final Set<File> sideCarFiles = new HashSet<>()
FileManager(EventBus eventBus, MuWireSettings settings) {
this.settings = settings
@@ -36,8 +37,15 @@ class FileManager {
}
void onFileHashedEvent(FileHashedEvent e) {
if (e.sharedFile != null)
addToIndex(e.sharedFile)
if (e.sharedFile == null)
return
File f = e.sharedFile.getFile()
if (sideCarFiles.remove(f)) {
File sideCar = new File(f.getParentFile(), f.getName() + ".mwcomment")
if (sideCar.exists())
e.sharedFile.setComment(Base64.encode(DataUtil.encodei18nString(sideCar.text)))
}
addToIndex(e.sharedFile)
}
void onFileLoadedEvent(FileLoadedEvent e) {
@@ -49,6 +57,21 @@ class FileManager {
addToIndex(e.downloadedFile)
}
}
void onSideCarFileEvent(SideCarFileEvent e) {
String name = e.file.getName()
name = name.substring(0, name.length() - ".mwcomment".length())
File target = new File(e.file.getParentFile(), name)
SharedFile existing = fileToSharedFile.get(target)
if (existing == null) {
sideCarFiles.add(target)
return
}
String comment = Base64.encode(DataUtil.encodei18nString(e.file.text))
String oldComment = existing.getComment()
existing.setComment(comment)
eventBus.publish(new UICommentEvent(oldComment : oldComment, sharedFile : existing))
}
private void addToIndex(SharedFile sf) {
log.info("Adding shared file " + sf.getFile())

View File

@@ -45,7 +45,7 @@ class FileTree {
true
}
private static class TreeNode {
public static class TreeNode {
TreeNode parent
File file
final Set<TreeNode> children = new HashSet<>()

View File

@@ -3,6 +3,7 @@ package com.muwire.core.files
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
@@ -33,7 +34,12 @@ class HasherService {
return
if (fileManager.fileToSharedFile.containsKey(canonical))
return
if (hashed.add(canonical))
if (canonical.isFile() && fileManager.negativeTree.fileToNode.containsKey(canonical))
return
if (canonical.getName().endsWith(".mwcomment")) {
if (canonical.length() <= Constants.MAX_COMMENT_LENGTH)
eventBus.publish(new SideCarFileEvent(file : canonical))
} else if (hashed.add(canonical))
executor.execute( { -> process(canonical) } as Runnable)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,6 +41,11 @@ mvcGroups {
view = 'com.muwire.gui.I2PStatusView'
controller = 'com.muwire.gui.I2PStatusController'
}
'system-status' {
model = 'com.muwire.gui.SystemStatusModel'
view = 'com.muwire.gui.SystemStatusView'
controller = 'com.muwire.gui.SystemStatusController'
}
'trust-list' {
model = 'com.muwire.gui.TrustListModel'
view = 'com.muwire.gui.TrustListView'
@@ -71,4 +76,14 @@ mvcGroups {
view = 'com.muwire.gui.CloseWarningView'
controller = 'com.muwire.gui.CloseWarningController'
}
'update' {
model = 'com.muwire.gui.UpdateModel'
view = 'com.muwire.gui.UpdateView'
controller = 'com.muwire.gui.UpdateController'
}
'advanced-sharing' {
model = 'com.muwire.gui.AdvancedSharingModel'
view = 'com.muwire.gui.AdvancedSharingView'
controller = 'com.muwire.gui.AdvancedSharingController'
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import java.util.logging.Level
import javax.annotation.Nonnull
import javax.swing.JFileChooser
import javax.swing.JOptionPane
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
@@ -20,12 +21,14 @@ class OptionsController {
OptionsModel model
@MVCMember @Nonnull
OptionsView view
Core core
MuWireSettings settings
UISettings uiSettings
@ControllerAction
void save() {
String text
Core core = application.context.get("core")
MuWireSettings settings = application.context.get("muwire-settings")
def i2pProps = core.i2pOptions
@@ -137,14 +140,10 @@ class OptionsController {
model.trustListInterval = trustListInterval
settings.trustListInterval = Integer.parseInt(trustListInterval)
File settingsFile = new File(core.home, "MuWire.properties")
settingsFile.withOutputStream {
settings.write(it)
}
core.saveMuSettings()
// UI Setttings
UISettings uiSettings = application.context.get("ui-settings")
text = view.lnfField.text
model.lnf = text
uiSettings.lnf = text
@@ -172,16 +171,24 @@ class OptionsController {
model.clearUploads = clearUploads
uiSettings.clearUploads = clearUploads
boolean storeSearchHistory = view.storeSearchHistoryCheckbox.model.isSelected()
model.storeSearchHistory = storeSearchHistory
uiSettings.storeSearchHistory = storeSearchHistory
uiSettings.exitOnClose = model.exitOnClose
if (model.closeDecisionMade)
uiSettings.closeWarning = false
saveUISettings()
cancel()
}
private void saveUISettings() {
File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream {
uiSettings.write(it)
}
cancel()
}
@ControllerAction
@@ -234,4 +241,11 @@ class OptionsController {
model.exitOnClose = false
model.closeDecisionMade = true
}
@ControllerAction
void clearHistory() {
uiSettings.searchHistory.clear()
saveUISettings()
JOptionPane.showMessageDialog(null, "Search history has been cleared")
}
}

View File

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

View File

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

View File

@@ -117,6 +117,7 @@ class Initialize extends AbstractLifecycleHandler {
def guiPropsFile = new File(home, "gui.properties")
UISettings uiSettings
int rowHeight = 15
if (guiPropsFile.exists()) {
Properties props = new Properties()
guiPropsFile.withInputStream { props.load(it) }
@@ -149,15 +150,17 @@ class Initialize extends AbstractLifecycleHandler {
} else {
fontSize = uiSettings.fontSize
}
rowHeight = fontSize + 3
FontUIResource font = new FontUIResource(fontName, Font.PLAIN, fontSize)
def keys = lnf.getDefaults().keys()
while(keys.hasMoreElements()) {
def key = keys.nextElement()
def value = lnf.getDefaults().get(key)
if (value instanceof FontUIResource)
if (value instanceof FontUIResource) {
lnf.getDefaults().put(key, font)
UIManager.put(key, font)
}
}
}
} else {
@@ -181,6 +184,7 @@ class Initialize extends AbstractLifecycleHandler {
}
}
application.context.put("row-height", rowHeight)
application.context.put("ui-settings", uiSettings)
}

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
package com.muwire.gui
import java.util.concurrent.ConcurrentHashMap
@@ -35,6 +36,7 @@ import com.muwire.core.files.FileHashingEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.SideCarFileEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
@@ -86,6 +88,8 @@ class MainFrameModel {
def trusted = []
def distrusted = []
def subscriptions = []
boolean sessionRestored
@Observable int connections
@Observable String me
@@ -189,6 +193,7 @@ class MainFrameModel {
core.eventBus.register(ConnectionEvent.class, this)
core.eventBus.register(DisconnectionEvent.class, this)
core.eventBus.register(FileHashedEvent.class, this)
core.eventBus.register(SideCarFileEvent.class, this)
core.eventBus.register(FileHashingEvent.class, this)
core.eventBus.register(FileLoadedEvent.class, this)
core.eventBus.register(UploadEvent.class, this)
@@ -252,7 +257,7 @@ class MainFrameModel {
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
runInsideUIAsync {
core.muOptions.watchedDirectories.each { core.eventBus.publish(new DirectoryWatchedEvent(directory : new File(it))) }
core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
core.muOptions.trustSubscriptions.each {
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
@@ -262,8 +267,10 @@ class MainFrameModel {
void onUpdateDownloadedEvent(UpdateDownloadedEvent e) {
runInsideUIAsync {
JOptionPane.showMessageDialog(null, "MuWire $e.version has been downloaded. You can update now",
"Update Downloaded", JOptionPane.INFORMATION_MESSAGE)
Map<String, Object> args = new HashMap<>()
args['available'] = null
args['downloaded'] = e
mvcGroup.createMVCGroup("update", "update", args)
}
}
@@ -290,6 +297,8 @@ class MainFrameModel {
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
view.showRestoreOrEmpty()
if (connections > 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-search-panel")
@@ -527,13 +536,10 @@ class MainFrameModel {
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
runInsideUIAsync {
int option = JOptionPane.showConfirmDialog(null,
"MuWire $e.version is available from $e.signer. You have "+ metadata["application.version"]+" Update?",
"New MuWire version availble", JOptionPane.OK_CANCEL_OPTION)
if (option == JOptionPane.CANCEL_OPTION)
return
controller.search(e.infoHash,"MuWire update")
Map<String, Object> args = new HashMap<>()
args['available'] = e
args['downloaded'] = null
mvcGroup.createMVCGroup("update", "update", args)
}
}
@@ -559,6 +565,12 @@ class MainFrameModel {
}
}
void onSideCarFileEvent(SideCarFileEvent e) {
runInsideUIAsync {
view.refreshSharedFiles()
}
}
private void insertIntoTree(SharedFile file) {
List<File> parents = new ArrayList<>()
File tmp = file.file.getParentFile()

View File

@@ -21,6 +21,7 @@ class OptionsModel {
@Observable int speedSmoothSeconds
@Observable int totalUploadSlots
@Observable int uploadSlotsPerUser
@Observable boolean storeSearchHistory
// i2p options
@Observable String inboundLength
@@ -90,6 +91,7 @@ class OptionsModel {
showSearchHashes = uiSettings.showSearchHashes
clearUploads = uiSettings.clearUploads
exitOnClose = uiSettings.exitOnClose
storeSearchHistory = uiSettings.storeSearchHistory
if (core.router != null) {
inBw = String.valueOf(settings.inBw)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import net.i2p.data.DataHelper
import javax.swing.BorderFactory
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JComboBox
import javax.swing.JFileChooser
import javax.swing.JFrame
import javax.swing.JLabel
@@ -24,6 +25,10 @@ import javax.swing.SwingConstants
import javax.swing.SwingUtilities
import javax.swing.TransferHandler
import javax.swing.border.Border
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import javax.swing.event.TreeExpansionEvent
import javax.swing.event.TreeExpansionListener
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.tree.TreeNode
import javax.swing.tree.TreePath
@@ -67,11 +72,14 @@ class MainFrameView {
def lastDownloadSortEvent
def lastSharedSortEvent
def trustTablesSortEvents = [:]
def expansionListener = new TreeExpansions()
UISettings settings
void initUI() {
settings = application.context.get("ui-settings")
int rowHeight = application.context.get("row-height")
builder.with {
application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null,
@@ -89,17 +97,31 @@ class MainFrameView {
menuItem("Exit", actionPerformed : {closeApplication()})
}
menu (text : "Options") {
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
menuItem("Content Control", actionPerformed : {
def env = [:]
env["core"] = model.core
mvcGroup.createMVCGroup("content-panel", env)
menuItem("Configuration", actionPerformed : {
def params = [:]
params['core'] = application.context.get("core")
params['settings'] = params['core'].muOptions
params['uiSettings'] = settings
mvcGroup.createMVCGroup("Options", params)
})
}
menu (text : "Status") {
menuItem("MuWire", actionPerformed : {mvcGroup.createMVCGroup("mu-wire-status")})
MuWireSettings muSettings = application.context.get("muwire-settings")
menuItem("I2P", enabled : bind {model.routerPresent}, actionPerformed: {mvcGroup.createMVCGroup("i-2-p-status")})
menuItem("System", actionPerformed : {mvcGroup.createMVCGroup("system-status")})
}
menu (text : "Tools") {
menuItem("Content Control", actionPerformed : {
def env = [:]
env["core"] = model.core
mvcGroup.createMVCGroup("content-panel", env)
})
menuItem("Advanced Sharing", actionPerformed : {
def env = [:]
env["core"] = model.core
mvcGroup.createMVCGroup("advanced-sharing",env)
})
}
}
borderLayout()
@@ -123,20 +145,39 @@ class MainFrameView {
panel(constraints: BorderLayout.CENTER) {
borderLayout()
label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
def searchFieldModel = new SearchFieldModel(settings, new File(application.context.get("muwire-home")))
JComboBox myComboBox = new SearchField(searchFieldModel)
myComboBox.setAction(searchAction)
widget(id: "search-field", constraints: BorderLayout.CENTER, myComboBox)
}
panel( constraints: BorderLayout.EAST) {
button(text: "Search", searchAction)
button(text : "", icon : imageIcon("/close_tab.png"), clearSearchAction)
}
}
}
}
panel (id: "cards-panel", constraints : BorderLayout.CENTER) {
cardLayout()
panel (constraints : "search window") {
borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
panel (id : "search window", constraints : "search window") {
cardLayout()
panel (constraints : "tabs-panel") {
borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
}
panel(constraints : "restore session") {
borderLayout()
panel (constraints : BorderLayout.CENTER) {
gridBagLayout()
label(text : "Saved Tabs:", constraints : gbc(gridx : 0, gridy : 0))
scrollPane (constraints : gbc(gridx : 0, gridy : 1)) {
list(items : new ArrayList(settings.openTabs))
}
button(text : "Restore Session", constraints : gbc(gridx :0, gridy : 2), restoreSessionAction)
}
}
}
panel (constraints: "downloads window") {
gridLayout(rows : 1, cols : 1)
@@ -144,7 +185,7 @@ class MainFrameView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true) {
downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list: model.downloads) {
closureColumn(header: "Name", preferredWidth: 300, type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState().toString()})
@@ -223,7 +264,7 @@ class MainFrameView {
panel (constraints : "shared files table") {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "shared-files-table", autoCreateRowSorter: true) {
table(id : "shared-files-table", autoCreateRowSorter: true, rowHeight : rowHeight) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.getCachedLength() })
@@ -239,7 +280,7 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
def jtree = new JTree(model.sharedTree)
jtree.setCellRenderer(new SharedTreeRenderer())
tree(id : "shared-files-tree", rootVisible : false, expandsSelectedPaths: true, jtree)
tree(id : "shared-files-tree", rowHeight : rowHeight, rootVisible : false, expandsSelectedPaths: true, jtree)
}
}
}
@@ -269,7 +310,7 @@ class MainFrameView {
label("Uploads")
}
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "uploads-table") {
table(id : "uploads-table", rowHeight : rowHeight) {
tableModel(list : model.uploads) {
closureColumn(header : "Name", type : String, read : {row -> row.uploader.getName() })
closureColumn(header : "Progress", type : String, read : { row ->
@@ -315,7 +356,7 @@ class MainFrameView {
label("Connections")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "connections-table") {
table(id : "connections-table", rowHeight : rowHeight) {
tableModel(list : model.connectionList) {
closureColumn(header : "Destination", preferredWidth: 250, type: String, read : { row -> row.destination.toBase32() })
closureColumn(header : "Direction", preferredWidth: 20, type: String, read : { row ->
@@ -334,7 +375,7 @@ class MainFrameView {
label("Incoming searches")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") {
table(id : "searches-table", rowHeight : rowHeight) {
tableModel(list : model.searches) {
closureColumn(header : "Keywords", type : String, read : {
sanitized = it.search.replace('<', ' ')
@@ -367,7 +408,7 @@ class MainFrameView {
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table", autoCreateRowSorter : true) {
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
}
@@ -383,7 +424,7 @@ class MainFrameView {
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table", autoCreateRowSorter : true) {
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
}
@@ -402,7 +443,7 @@ class MainFrameView {
label(text : "Trust List Subscriptions")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "subscription-table", autoCreateRowSorter : true) {
table(id : "subscription-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.subscriptions) {
closureColumn(header : "Name", preferredWidth: 200, type: String, read : {it.persona.getHumanReadableName()})
closureColumn(header : "Trusted", preferredWidth : 20, type: Integer, read : {it.good.size()})
@@ -472,6 +513,10 @@ class MainFrameView {
}
}})
// search field
def searchField = builder.getVariable("search-field")
// downloads table
def downloadsTable = builder.getVariable("downloads-table")
def selectionModel = downloadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
@@ -589,6 +634,8 @@ class MainFrameView {
model.addCommentButtonEnabled = selectedNode != null
})
sharedFilesTree.addTreeExpansionListener(expansionListener)
// searches table
def searchesTable = builder.getVariable("searches-table")
@@ -694,6 +741,9 @@ class MainFrameView {
// show tree by default
showSharedFilesTree.call()
// show search panel by default
showSearchWindow.call()
}
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
@@ -838,10 +888,23 @@ class MainFrameView {
showPopupMenu(menu, e)
}
void showRestoreOrEmpty() {
def searchWindow = builder.getVariable("search window")
String id
if (!model.sessionRestored && !settings.openTabs.isEmpty())
id = model.connections > 0 ? "restore session" : "tabs-panel"
else
id = "tabs-panel"
searchWindow.getLayout().show(searchWindow, id)
}
def showSearchWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
showRestoreOrEmpty()
model.searchesPaneButtonEnabled = false
model.downloadsPaneButtonEnabled = true
model.uploadsPaneButtonEnabled = true
@@ -929,16 +992,31 @@ class MainFrameView {
public void refreshSharedFiles() {
def tree = builder.getVariable("shared-files-tree")
TreePath[] selectedPaths = tree.getSelectionPaths()
Set<TreePath> expanded = new HashSet<>(expansionListener.expandedPaths)
model.sharedTree.nodeStructureChanged(model.treeRoot)
expanded.each { tree.expandPath(it) }
tree.setSelectionPaths(selectedPaths)
builder.getVariable("shared-files-table").model.fireTableDataChanged()
}
private void closeApplication() {
Core core = application.getContext().get("core")
def tabbedPane = builder.getVariable("result-tabs")
settings.openTabs.clear()
int count = tabbedPane.getTabCount()
for (int i = 0; i < count; i++)
settings.openTabs.add(tabbedPane.getTitleAt(i))
File uiPropsFile = new File(core.home, "gui.properties")
uiPropsFile.withOutputStream { settings.write(it) }
def mainFrame = builder.getVariable("main-frame")
mainFrame.setVisible(false)
application.getWindowManager().findWindow("shutdown-window").setVisible(true)
Core core = application.getContext().get("core")
if (core != null) {
Thread t = new Thread({
core.shutdown()
@@ -947,4 +1025,19 @@ class MainFrameView {
t.start()
}
}
private static class TreeExpansions implements TreeExpansionListener {
private final Set<TreePath> expandedPaths = new HashSet<>()
@Override
public void treeExpanded(TreeExpansionEvent event) {
expandedPaths.add(event.getPath())
}
@Override
public void treeCollapsed(TreeExpansionEvent event) {
expandedPaths.remove(event.getPath())
}
}
}

View File

@@ -61,6 +61,7 @@ class OptionsView {
def excludeLocalResultCheckbox
def showSearchHashesCheckbox
def clearUploadsCheckbox
def storeSearchHistoryCheckbox
def inBwField
def outBwField
@@ -196,8 +197,17 @@ class OptionsView {
constraints : gbc(gridx : 3, gridy : 2, anchor : GridBagConstraints.LINE_END))
}
panel (border : titledBorder(title : "Other Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
panel (border : titledBorder(title : "Search Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
gridBagLayout()
label(text : "Remember search history", constraints: gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100))
storeSearchHistoryCheckbox = checkBox(selected : bind {model.storeSearchHistory},
constraints : gbc(gridx : 1, gridy:0, anchor : GridBagConstraints.LINE_END))
button(text : "Clear history", constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_END), clearHistoryAction)
}
panel (border : titledBorder(title : "Other Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy : 2, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
gridBagLayout()
label(text : "Automatically clear cancelled downloads", constraints: gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads},
@@ -221,7 +231,7 @@ class OptionsView {
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: 3, weighty: 100))
}
bandwidth = builder.panel {
gridBagLayout()

View File

@@ -49,6 +49,7 @@ class SearchTabView {
def sequentialDownloadCheckbox
void initUI() {
int rowHeight = application.context.get("row-height")
builder.with {
def resultsTable
def sendersTable
@@ -59,7 +60,7 @@ class SearchTabView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
sendersTable = table(id : "senders-table", autoCreateRowSorter : true) {
sendersTable = table(id : "senders-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.senders) {
closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()})
closureColumn(header : "Results", preferredWidth : 20, type: Integer, read : {row -> model.sendersBucket[row].size()})
@@ -85,7 +86,7 @@ class SearchTabView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
resultsTable = table(id : "results-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list: model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})

View File

@@ -37,7 +37,7 @@ class ShowCommentView {
borderLayout()
panel (constraints : BorderLayout.CENTER) {
scrollPane {
textArea(text : model.result.comment, rows : 20, columns : 100, editable : false)
textArea(text : model.result.comment, rows : 20, columns : 100, editable : false, lineWrap : true, wrapStyleWord : true)
}
}
panel (constraints : BorderLayout.SOUTH) {

View File

@@ -0,0 +1,83 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.DataHelper
import javax.swing.JDialog
import javax.swing.JPanel
import javax.swing.SwingConstants
import javax.swing.border.TitledBorder
import java.awt.BorderLayout
import java.awt.GridBagConstraints
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SystemStatusView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
SystemStatusModel model
def mainFrame
def dialog
def panel
def buttonsPanel
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, "System Status", true)
panel = builder.panel {
gridBagLayout()
panel(border : titledBorder(title : "Java", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy: 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
gridBagLayout()
label(text : "Vendor ", constraints : gbc(gridx:0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.javaVendor}, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "Version", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.javaVersion}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
}
panel(border : titledBorder(title : "Memory", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
gridBagLayout()
label(text : "Used", constraints : gbc(gridx:0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {DataHelper.formatSize2Decimal(model.usedRam,false)+"B"}, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "Total", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {DataHelper.formatSize2Decimal(model.totalRam,false)+"B"}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
label(text : "Max", constraints : gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {DataHelper.formatSize2Decimal(model.maxRam,false)+"B"}, constraints : gbc(gridx:1, gridy:2, anchor : GridBagConstraints.LINE_END))
}
buttonsPanel = builder.panel {
gridBagLayout()
button(text : "Refresh", constraints: gbc(gridx: 0, gridy: 0), refreshAction)
button(text : "Close", constraints : gbc(gridx : 1, gridy :0), closeAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
JPanel statusPanel = new JPanel()
statusPanel.setLayout(new BorderLayout())
statusPanel.add(panel, BorderLayout.CENTER)
statusPanel.add(buttonsPanel, BorderLayout.SOUTH)
dialog.getContentPane().add(statusPanel)
dialog.pack()
dialog.setResizable(false)
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
}

View File

@@ -29,6 +29,7 @@ class TrustListView {
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
int rowHeight = application.context.get("row-height")
dialog = new JDialog(mainFrame, model.trustList.persona.getHumanReadableName(), true)
mainPanel = builder.panel {
borderLayout()
@@ -46,7 +47,7 @@ class TrustListView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER){
table(id : "trusted-table", autoCreateRowSorter : true) {
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.trusted) {
closureColumn(header: "Trusted Users", type : String, read : {it.getHumanReadableName()})
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()})
@@ -62,7 +63,7 @@ class TrustListView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER ){
table(id : "distrusted-table", autoCreateRowSorter : true) {
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : {it.getHumanReadableName()})
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()})

View File

@@ -0,0 +1,61 @@
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.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class UpdateView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
UpdateModel model
def mainFrame
def dialog
def p
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
String title = model.downloaded != null ? "Update Downloaded" : "Update Available"
dialog = new JDialog(mainFrame, title, true)
dialog.setResizable(true)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.CENTER) {
scrollPane {
def text = model.downloaded != null ? model.downloaded.text : model.available.text
textArea(text : text, rows : 20, columns : 50, editable : false, lineWrap : true, wrapStyleWord : true)
}
}
panel (constraints : BorderLayout.SOUTH) {
if (model.available != null)
button(text : "Find", searchAction)
button(text : "Close", closeAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
dialog.getContentPane().add(p)
dialog.pack()
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
}

View File

@@ -0,0 +1,14 @@
package com.muwire.gui
import java.awt.event.KeyEvent
import javax.swing.JComboBox
class SearchField extends JComboBox {
SearchField(SearchFieldModel model) {
super()
setEditable(true)
setModel(model)
setEditor(new SearchFieldEditor(model, this))
}
}

View File

@@ -0,0 +1,47 @@
package com.muwire.gui
import javax.swing.JTextField
import javax.swing.SwingUtilities
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import javax.swing.plaf.basic.BasicComboBoxEditor
class SearchFieldEditor extends BasicComboBoxEditor {
private final SearchFieldModel model
private final SearchField field
SearchFieldEditor(SearchFieldModel model, SearchField field) {
super()
this.model = model
this.field = field
def action = field.getAction()
field.setAction(null)
editor.setAction(action)
editor.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
SwingUtilities.invokeLater({
field.hidePopup()
if (model.onKeyStroke(editor.text))
field.showPopup()
})
}
@Override
public void removeUpdate(DocumentEvent e) {
SwingUtilities.invokeLater({
field.hidePopup()
if (model.onKeyStroke(editor.text))
field.showPopup()
})
}
@Override
public void changedUpdate(DocumentEvent e) {
}
})
}
}

View File

@@ -0,0 +1,100 @@
package com.muwire.gui
import javax.swing.AbstractListModel
import javax.swing.MutableComboBoxModel
class SearchFieldModel extends AbstractListModel implements MutableComboBoxModel {
private final UISettings uiSettings
private final File settingsFile
private final List<String> objects = new ArrayList<>()
private String selectedObject
SearchFieldModel(UISettings uiSettings, File home) {
super()
this.uiSettings = uiSettings
this.settingsFile = new File(home, "gui.properties")
uiSettings.searchHistory.each { objects.add(it) }
fireIntervalAdded(this, 0, objects.size() - 1)
}
public void addElement(Object string) {
if (string == null)
return
if (uiSettings.storeSearchHistory) {
if (!uiSettings.searchHistory.add(string))
return
settingsFile.withOutputStream { uiSettings.write(it) }
}
objects.add(string);
fireIntervalAdded(this,objects.size()-1, objects.size()-1);
if ( objects.size() == 1 && selectedObject == null && string != null ) {
setSelectedItem( string );
}
}
boolean onKeyStroke(String selected) {
selectedObject = selected
if (selected == null || selected.length() == 0) {
objects.clear()
uiSettings.searchHistory.each { objects.add(it) }
return true
}
objects.clear()
Set<String> matching = new HashSet<>(uiSettings.searchHistory)
matching.retainAll { it.contains(selected) }
matching.each {
objects.add(it)
}
Collections.sort(objects)
if (!objects.isEmpty()) {
fireIntervalAdded(this, 0, objects.size() - 1)
return true
}
false
}
@Override
public void setSelectedItem(Object anObject) {
if ((selectedObject != null && !selectedObject.equals( anObject )) ||
selectedObject == null && anObject != null) {
selectedObject = anObject;
fireContentsChanged(this, -1, -1);
}
}
@Override
public Object getSelectedItem() {
selectedObject
}
@Override
public int getSize() {
objects.size()
}
@Override
public Object getElementAt(int index) {
if ( index >= 0 && index < objects.size() )
return objects.get(index);
else
return null;
}
@Override
public void removeElement(Object obj) {
}
@Override
public void insertElementAt(Object item, int index) {
}
@Override
public void removeElementAt(int index) {
}
}

View File

@@ -1,5 +1,7 @@
package com.muwire.gui
import com.muwire.core.util.DataUtil
class UISettings {
String lnf
@@ -14,6 +16,9 @@ class UISettings {
boolean closeWarning
boolean exitOnClose
boolean clearUploads
boolean storeSearchHistory
Set<String> searchHistory
Set<String> openTabs
UISettings(Properties props) {
lnf = props.getProperty("lnf", "system")
@@ -28,6 +33,10 @@ class UISettings {
closeWarning = Boolean.parseBoolean(props.getProperty("closeWarning","true"))
exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false"))
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads","false"))
storeSearchHistory = Boolean.parseBoolean(props.getProperty("storeSearchHistory","true"))
searchHistory = DataUtil.readEncodedSet(props, "searchHistory")
openTabs = DataUtil.readEncodedSet(props, "openTabs")
}
void write(OutputStream out) throws IOException {
@@ -43,9 +52,12 @@ class UISettings {
props.setProperty("closeWarning", String.valueOf(closeWarning))
props.setProperty("exitOnClose", String.valueOf(exitOnClose))
props.setProperty("clearUploads", String.valueOf(clearUploads))
props.setProperty("storeSearchHistory", String.valueOf(storeSearchHistory))
if (font != null)
props.setProperty("font", font)
DataUtil.writeEncodedSet(searchHistory, "searchHistory", props)
DataUtil.writeEncodedSet(openTabs, "openTabs", props)
props.store(out, "UI Properties")
}

View File

@@ -0,0 +1,30 @@
############################################################
# Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example java -Djava.util.logging.config.file=myfile
############################################################
############################################################
# Global properties
############################################################
# "handlers" specifies a comma separated list of log Handler
# classes. These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.FileHandler
# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers. For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= OFF