Compare commits
39 Commits
muwire-0.5
...
muwire-0.6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a2637570b1 | ||
![]() |
6012adbeab | ||
![]() |
8f6b6b0caa | ||
![]() |
8f3b5aea8d | ||
![]() |
ee098ace8e | ||
![]() |
5d8401e4bf | ||
![]() |
fbf9add82a | ||
![]() |
7379263fef | ||
![]() |
7d50843754 | ||
![]() |
f4a2864942 | ||
![]() |
afaadf65a4 | ||
![]() |
7bd422d6b4 | ||
![]() |
3f47274f61 | ||
![]() |
419e9a0ce6 | ||
![]() |
ac1068a681 | ||
![]() |
549457e36f | ||
![]() |
14d6d10546 | ||
![]() |
878e397aa0 | ||
![]() |
27831b488b | ||
![]() |
449f46c62b | ||
![]() |
5703b85386 | ||
![]() |
76d8d847bd | ||
![]() |
db84d8e5bf | ||
![]() |
cc9b384907 | ||
![]() |
72960c24a8 | ||
![]() |
71298e5e73 | ||
![]() |
11bc672544 | ||
![]() |
2f6cd311a0 | ||
![]() |
0448750491 | ||
![]() |
800dd1cbba | ||
![]() |
f95e9450f3 | ||
![]() |
d842e3f2f2 | ||
![]() |
2017b53a43 | ||
![]() |
6e2b3f4f33 | ||
![]() |
dbb305139b | ||
![]() |
0801bfec08 | ||
![]() |
00a8d100fe | ||
![]() |
e94b7cb0d4 | ||
![]() |
b0357f2ecd |
@@ -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.5 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
The current stable release - 0.6.0 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
|
||||
### Building
|
||||
|
||||
|
@@ -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.9"
|
||||
private static final String MW_VERSION = "0.6.1"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
|
@@ -7,6 +7,7 @@ import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
@@ -45,13 +46,16 @@ class SearchModel {
|
||||
|
||||
def searchEvent
|
||||
byte [] payload
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
|
||||
if (hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash : true, compressedResults : true)
|
||||
payload = root
|
||||
} else {
|
||||
def nonEmpty = SplitPattern.termify(query)
|
||||
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||
}
|
||||
|
||||
@@ -61,7 +65,7 @@ class SearchModel {
|
||||
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig: sig.data))
|
||||
originator : core.me, sig: sig.data, queryTime : timestamp, sig2 : sig2))
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
class TrustEntryWrapper {
|
||||
TrustService.TrustEntry entry
|
||||
}
|
@@ -16,8 +16,8 @@ class TrustListModel {
|
||||
this.trustList = trustList
|
||||
this.core = core
|
||||
|
||||
trustedTableModel = new TableModel("Trusted User","Your Trust")
|
||||
distrustedTableModel = new TableModel("Distrusted User", "Your Trust")
|
||||
trustedTableModel = new TableModel("Trusted User","Reason","Your Trust")
|
||||
distrustedTableModel = new TableModel("Distrusted User", "Reason", "Your Trust")
|
||||
refreshModels()
|
||||
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
@@ -36,10 +36,10 @@ class TrustListModel {
|
||||
distrustRows.times { distrustedTableModel.removeRow(0) }
|
||||
|
||||
trustList.good.each {
|
||||
trustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
|
||||
trustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
|
||||
}
|
||||
trustList.bad.each {
|
||||
distrustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
|
||||
distrustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,7 @@ 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.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
@@ -48,7 +49,7 @@ class TrustListView extends BasicWindow {
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
trusted = new Table("Trusted User","Your Trust")
|
||||
trusted = new Table("Trusted User","Reason","Your Trust")
|
||||
trusted.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.trustedTableModel)
|
||||
@@ -57,7 +58,7 @@ class TrustListView extends BasicWindow {
|
||||
trusted.setSelectAction({ actionsForUser(true) })
|
||||
topPanel.addComponent(trusted, layoutData)
|
||||
|
||||
distrusted = new Table("Distrusted User", "Your Trust")
|
||||
distrusted = new Table("Distrusted User","Reason", "Your Trust")
|
||||
distrusted.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.distrustedTableModel)
|
||||
@@ -90,7 +91,8 @@ class TrustListView extends BasicWindow {
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button trustButton = new Button("Trust",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
@@ -100,7 +102,8 @@ class TrustListView extends BasicWindow {
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button distrustButton = new Button("Distrust",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
|
@@ -17,8 +17,8 @@ class TrustModel {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
|
||||
modelTrusted = new TableModel("Trusted Users")
|
||||
modelDistrusted = new TableModel("Distrusted Users")
|
||||
modelTrusted = new TableModel("Trusted Users","Reason")
|
||||
modelDistrusted = new TableModel("Distrusted Users","Reason")
|
||||
modelSubscriptions = new TableModel("Name","Trusted","Distrusted","Status","Last Updated")
|
||||
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
@@ -57,11 +57,11 @@ class TrustModel {
|
||||
subsRows.times { modelSubscriptions.removeRow(0) }
|
||||
|
||||
core.trustService.good.values().each {
|
||||
modelTrusted.addRow(new PersonaWrapper(it))
|
||||
modelTrusted.addRow(new PersonaWrapper(it.persona),it.reason)
|
||||
}
|
||||
|
||||
core.trustService.bad.values().each {
|
||||
modelDistrusted.addRow(new PersonaWrapper(it))
|
||||
modelDistrusted.addRow(new PersonaWrapper(it.persona),it.reason)
|
||||
}
|
||||
|
||||
core.trustSubscriber.remoteTrustLists.values().each {
|
||||
|
@@ -11,6 +11,7 @@ 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.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
@@ -44,14 +45,14 @@ class TrustView extends BasicWindow {
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
trusted = new Table("Trusted Users")
|
||||
trusted = new Table("Trusted Users","Reason")
|
||||
trusted.setCellSelection(false)
|
||||
trusted.setSelectAction({trustedActions()})
|
||||
trusted.setTableModel(model.modelTrusted)
|
||||
trusted.setVisibleRows(tableSize)
|
||||
topPanel.addComponent(trusted, layoutData)
|
||||
|
||||
distrusted = new Table("Distrusted users")
|
||||
distrusted = new Table("Distrusted users","Reason")
|
||||
distrusted.setCellSelection(false)
|
||||
distrusted.setSelectAction({distrustedActions()})
|
||||
distrusted.setTableModel(model.modelDistrusted)
|
||||
@@ -106,7 +107,8 @@ class TrustView extends BasicWindow {
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button markDistrusted = new Button("Mark Distrusted", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
@@ -122,7 +124,7 @@ class TrustView extends BasicWindow {
|
||||
}
|
||||
|
||||
private void distrustedActions() {
|
||||
int selectedRow = trusted.getSelectedRow()
|
||||
int selectedRow = distrusted.getSelectedRow()
|
||||
def row = model.modelDistrusted.getRow(selectedRow)
|
||||
Persona persona = row[0].persona
|
||||
|
||||
@@ -138,7 +140,8 @@ class TrustView extends BasicWindow {
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button markDistrusted = new Button("Mark Trusted", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
|
@@ -406,7 +406,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.5.9")
|
||||
Core core = new Core(props, home, "0.6.1")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -153,6 +153,10 @@ abstract class Connection implements Closeable {
|
||||
query.originator = e.originator.toBase64()
|
||||
if (e.sig != null)
|
||||
query.sig = Base64.encode(e.sig)
|
||||
if (e.queryTime > 0)
|
||||
query.queryTime = e.queryTime
|
||||
if (e.sig2 != null)
|
||||
query.sig2 = Base64.encode(e.sig2)
|
||||
messages.put(query)
|
||||
}
|
||||
|
||||
@@ -232,7 +236,6 @@ abstract class Connection implements Closeable {
|
||||
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
|
||||
@@ -247,8 +250,36 @@ abstract class Connection implements Closeable {
|
||||
return
|
||||
} else
|
||||
log.info("query signature verified")
|
||||
} else
|
||||
} else {
|
||||
log.info("no signature in query")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: make this mandatory at some point
|
||||
byte[] sig2 = null
|
||||
long queryTime = 0
|
||||
if (search.sig2 != null) {
|
||||
if (search.queryTime == null) {
|
||||
log.info("extended signature but no timestamp")
|
||||
return
|
||||
}
|
||||
sig2 = Base64.decode(search.sig2)
|
||||
queryTime = search.queryTime
|
||||
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig2)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("extended signature didn't match uuid and timestamp")
|
||||
return
|
||||
} else {
|
||||
log.info("extended query signature verified")
|
||||
if (queryTime < System.currentTimeMillis() - Constants.MAX_QUERY_AGE) {
|
||||
log.info("query too old")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else
|
||||
log.info("no extended signature in query")
|
||||
|
||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||
searchHash : infohash,
|
||||
@@ -262,7 +293,9 @@ abstract class Connection implements Closeable {
|
||||
originator : originator,
|
||||
receivedOn : endpoint.destination,
|
||||
firstHop : search.firstHop,
|
||||
sig : sig )
|
||||
sig : sig,
|
||||
queryTime : queryTime,
|
||||
sig2 : sig2 )
|
||||
eventBus.publish(event)
|
||||
|
||||
}
|
||||
|
@@ -159,7 +159,9 @@ class ConnectionAcceptor {
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.log(Level.WARNING, "incoming connection failed",ex)
|
||||
e.getOutputStream().close()
|
||||
try {
|
||||
e.getOutputStream().close()
|
||||
} catch (Exception ignore) {}
|
||||
e.close()
|
||||
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
||||
}
|
||||
@@ -208,7 +210,9 @@ class ConnectionAcceptor {
|
||||
os.writeShort(json.bytes.length)
|
||||
os.write(json.bytes)
|
||||
}
|
||||
e.outputStream.close()
|
||||
try {
|
||||
e.outputStream.close()
|
||||
} catch (Exception ignored) {}
|
||||
e.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
|
||||
}
|
||||
@@ -379,10 +383,10 @@ class ConnectionAcceptor {
|
||||
dis.readFully(RUST)
|
||||
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid TRUST connection")
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(dis)
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.allowTrustLists) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
@@ -390,22 +394,53 @@ class ConnectionAcceptor {
|
||||
return
|
||||
}
|
||||
|
||||
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
List<Persona> good = new ArrayList<>(trustService.good.values())
|
||||
int size = Math.min(Short.MAX_VALUE * 2, good.size())
|
||||
good = good.subList(0, size)
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
boolean json = headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])
|
||||
|
||||
List<TrustService.TrustEntry> good = new ArrayList<>(trustService.good.values())
|
||||
List<TrustService.TrustEntry> bad = new ArrayList<>(trustService.bad.values())
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
dos.writeShort(size)
|
||||
good.each {
|
||||
it.write(dos)
|
||||
}
|
||||
|
||||
List<Persona> bad = new ArrayList<>(trustService.bad.values())
|
||||
size = Math.min(Short.MAX_VALUE * 2, bad.size())
|
||||
bad = bad.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
bad.each {
|
||||
it.write(dos)
|
||||
if (!json) {
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
int size = Math.min(Short.MAX_VALUE * 2, good.size())
|
||||
good = good.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
good.each {
|
||||
it.persona.write(dos)
|
||||
}
|
||||
|
||||
size = Math.min(Short.MAX_VALUE * 2, bad.size())
|
||||
bad = bad.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
bad.each {
|
||||
it.persona.write(dos)
|
||||
}
|
||||
} else {
|
||||
dos.write("Json: true\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("Good:${good.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("Bad:${bad.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
good.each {
|
||||
def obj = [:]
|
||||
obj.persona = it.persona.toBase64()
|
||||
obj.reason = it.reason
|
||||
String toJson = JsonOutput.toJson(obj)
|
||||
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
bad.each {
|
||||
def obj = [:]
|
||||
obj.persona = it.persona.toBase64()
|
||||
obj.reason = it.reason
|
||||
String toJson = JsonOutput.toJson(obj)
|
||||
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
}
|
||||
|
||||
dos.flush()
|
||||
|
@@ -143,6 +143,7 @@ class FileManager {
|
||||
|
||||
String comment = sf.getComment()
|
||||
if (comment != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if (existingComment != null) {
|
||||
existingComment.remove(sf.getFile())
|
||||
@@ -229,7 +230,7 @@ class FileManager {
|
||||
return files
|
||||
Set<SharedFile> rv = new HashSet<>()
|
||||
files.each {
|
||||
if (it.getPieceSize() != 0)
|
||||
if (it != null && it.getPieceSize() != 0)
|
||||
rv.add(it)
|
||||
}
|
||||
rv
|
||||
|
@@ -13,6 +13,8 @@ class QueryEvent extends Event {
|
||||
Persona originator
|
||||
Destination receivedOn
|
||||
byte[] sig
|
||||
long queryTime
|
||||
byte[] sig2
|
||||
|
||||
String toString() {
|
||||
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
|
||||
|
@@ -39,10 +39,11 @@ class SearchIndex {
|
||||
split.each { if (it.length() > 0) rv << it }
|
||||
|
||||
// then just by ' '
|
||||
source.split(' ').each { if (it.length() > 0) rv << it }
|
||||
source.toLowerCase().split(' ').each { if (it.length() > 0) rv << it }
|
||||
|
||||
// and add original string
|
||||
rv << source
|
||||
rv << source.toLowerCase()
|
||||
rv.toArray(new String[0])
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ package com.muwire.core.trust
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustService.TrustEntry
|
||||
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@@ -10,7 +11,7 @@ class RemoteTrustList {
|
||||
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
|
||||
|
||||
private final Persona persona
|
||||
private final Set<Persona> good, bad
|
||||
private final Set<TrustEntry> good, bad
|
||||
volatile long timestamp
|
||||
volatile boolean forceUpdate
|
||||
Status status = Status.NEW
|
||||
|
@@ -7,4 +7,5 @@ class TrustEvent extends Event {
|
||||
|
||||
Persona persona
|
||||
TrustLevel level
|
||||
String reason
|
||||
}
|
||||
|
@@ -1,21 +1,26 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Service
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
class TrustService extends Service {
|
||||
|
||||
final File persistGood, persistBad
|
||||
final long persistInterval
|
||||
|
||||
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
|
||||
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
|
||||
final Map<Destination, TrustEntry> good = new ConcurrentHashMap<>()
|
||||
final Map<Destination, TrustEntry> bad = new ConcurrentHashMap<>()
|
||||
|
||||
final Timer timer
|
||||
|
||||
@@ -37,18 +42,41 @@ class TrustService extends Service {
|
||||
}
|
||||
|
||||
void load() {
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
if (persistGood.exists()) {
|
||||
persistGood.eachLine {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
good.put(persona.destination, persona)
|
||||
try {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
good.put(persona.destination, new TrustEntry(persona, null))
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
def json = slurper.parseText(it)
|
||||
byte [] decoded = Base64.decode(json.persona)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
good.put(persona.destination, new TrustEntry(persona, json.reason))
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"couldn't parse trust entry $it",bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (persistBad.exists()) {
|
||||
persistBad.eachLine {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
bad.put(persona.destination, persona)
|
||||
try {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
bad.put(persona.destination, new TrustEntry(persona, null))
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
def json = slurper.parseText(it)
|
||||
byte [] decoded = Base64.decode(json.persona)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
bad.put(persona.destination, new TrustEntry(persona, json.reason))
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"couldn't parse trust entry $it",bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
|
||||
@@ -59,13 +87,19 @@ class TrustService extends Service {
|
||||
persistGood.delete()
|
||||
persistGood.withPrintWriter { writer ->
|
||||
good.each {k,v ->
|
||||
writer.println v.toBase64()
|
||||
def json = [:]
|
||||
json.persona = v.persona.toBase64()
|
||||
json.reason = v.reason
|
||||
writer.println JsonOutput.toJson(json)
|
||||
}
|
||||
}
|
||||
persistBad.delete()
|
||||
persistBad.withPrintWriter { writer ->
|
||||
bad.each { k,v ->
|
||||
writer.println v.toBase64()
|
||||
def json = [:]
|
||||
json.persona = v.persona.toBase64()
|
||||
json.reason = v.reason
|
||||
writer.println JsonOutput.toJson(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,11 +116,11 @@ class TrustService extends Service {
|
||||
switch(e.level) {
|
||||
case TrustLevel.TRUSTED:
|
||||
bad.remove(e.persona.destination)
|
||||
good.put(e.persona.destination, e.persona)
|
||||
good.put(e.persona.destination, new TrustEntry(e.persona, e.reason))
|
||||
break
|
||||
case TrustLevel.DISTRUSTED:
|
||||
good.remove(e.persona.destination)
|
||||
bad.put(e.persona.destination, e.persona)
|
||||
bad.put(e.persona.destination, new TrustEntry(e.persona, e.reason))
|
||||
break
|
||||
case TrustLevel.NEUTRAL:
|
||||
good.remove(e.persona.destination)
|
||||
@@ -94,4 +128,24 @@ class TrustService extends Service {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public static class TrustEntry {
|
||||
private final Persona persona
|
||||
private final String reason
|
||||
TrustEntry(Persona persona, String reason) {
|
||||
this.persona = persona
|
||||
this.reason = reason
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
persona.hashCode()
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof TrustEntry))
|
||||
return false
|
||||
persona == o.persona
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,9 +12,12 @@ import com.muwire.core.Persona
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.trust.TrustService.TrustEntry
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
@@ -109,7 +112,9 @@ class TrustSubscriber {
|
||||
endpoint = i2pConnector.connect(trustList.persona.destination)
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
InputStream is = endpoint.getInputStream()
|
||||
os.write("TRUST\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("TRUST\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Json:true\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
|
||||
String codeString = DataUtil.readTillRN(is)
|
||||
@@ -123,24 +128,47 @@ class TrustSubscriber {
|
||||
return false
|
||||
}
|
||||
|
||||
// swallow any headers
|
||||
String header
|
||||
while (( header = DataUtil.readTillRN(is)) != "");
|
||||
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is)
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
Set<TrustService.TrustEntry> good = new HashSet<>()
|
||||
Set<TrustService.TrustEntry> bad = new HashSet<>()
|
||||
|
||||
if (headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])) {
|
||||
int countGood = Integer.parseInt(headers['Good'])
|
||||
int countBad = Integer.parseInt(headers['Bad'])
|
||||
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
|
||||
for (int i = 0; i < countGood; i++) {
|
||||
int length = dis.readUnsignedShort()
|
||||
byte []payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(json.persona)))
|
||||
good.add(new TrustEntry(persona, json.reason))
|
||||
}
|
||||
|
||||
for (int i = 0; i < countBad; i++) {
|
||||
int length = dis.readUnsignedShort()
|
||||
byte []payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(json.persona)))
|
||||
bad.add(new TrustEntry(persona, json.reason))
|
||||
}
|
||||
|
||||
} else {
|
||||
int nGood = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nGood; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
good.add(new TrustEntry(p,null))
|
||||
}
|
||||
|
||||
Set<Persona> good = new HashSet<>()
|
||||
int nGood = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nGood; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
good.add(p)
|
||||
}
|
||||
|
||||
Set<Persona> bad = new HashSet<>()
|
||||
int nBad = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nBad; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
bad.add(p)
|
||||
int nBad = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nBad; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
bad.add(new TrustEntry(p, null))
|
||||
}
|
||||
}
|
||||
|
||||
trustList.timestamp = now
|
||||
|
@@ -13,6 +13,7 @@ import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
@@ -176,9 +177,12 @@ class UpdateClient {
|
||||
signer = payload.signer
|
||||
log.info("starting search for new version hash $payload.infoHash")
|
||||
Signature sig = DSAEngine.getInstance().sign(updateInfoHash.getRoot(), spk)
|
||||
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), oobInfohash : true, persona : me)
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, spk)
|
||||
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : uuid, oobInfohash : true, persona : me)
|
||||
def queryEvent = new QueryEvent(searchEvent : searchEvent, firstHop : true, replyTo : me.destination,
|
||||
receivedOn : me.destination, originator : me, sig : sig.data)
|
||||
receivedOn : me.destination, originator : me, sig : sig.data, queryTime : timestamp, sig2 : sig2)
|
||||
eventBus.publish(queryEvent)
|
||||
}
|
||||
}
|
||||
|
@@ -13,4 +13,6 @@ public class Constants {
|
||||
public static final int MAX_RESULTS = 0x1 << 16;
|
||||
|
||||
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
|
||||
|
||||
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
|
||||
}
|
||||
|
@@ -109,6 +109,10 @@ public class SharedFile {
|
||||
return downloaders;
|
||||
}
|
||||
|
||||
public Set<SearchEntry> getSearches() {
|
||||
return searches;
|
||||
}
|
||||
|
||||
public void addDownloader(String name) {
|
||||
downloaders.add(name);
|
||||
}
|
||||
|
@@ -15,11 +15,15 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.muwire.core.Constants;
|
||||
|
||||
import net.i2p.crypto.DSAEngine;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.Signature;
|
||||
import net.i2p.data.SigningPrivateKey;
|
||||
import net.i2p.util.ConcurrentHashSet;
|
||||
|
||||
public class DataUtil {
|
||||
@@ -203,4 +207,10 @@ public class DataUtil {
|
||||
.collect(Collectors.joining(","));
|
||||
props.setProperty(property, encoded);
|
||||
}
|
||||
|
||||
public static byte[] signUUID(UUID uuid, long timestamp, SigningPrivateKey spk) {
|
||||
byte [] payload = (uuid.toString() + String.valueOf(timestamp)).getBytes(StandardCharsets.US_ASCII);
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, spk);
|
||||
return sig.getData();
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll
|
||||
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@@ -9,6 +11,9 @@ import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class FileManagerTest {
|
||||
|
||||
@@ -185,4 +190,39 @@ class FileManagerTest {
|
||||
assert results == null
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplicatedScenario() {
|
||||
// this tries to reproduce an NPE when un-sharing then sharing again and searching
|
||||
String comment = "same comment"
|
||||
comment = Base64.encode(DataUtil.encodei18nString(comment))
|
||||
File f1 = new File("MuWire-0.5.10.AppImage")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
sf1.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf1))
|
||||
manager.onFileUnsharedEvent(new FileUnsharedEvent(unsharedFile : sf1, deleted : true))
|
||||
|
||||
File f2 = new File("MuWire-0.6.0.AppImage")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
sf2.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf2))
|
||||
|
||||
manager.onSearchEvent(new SearchEvent(searchTerms : ["muwire"]))
|
||||
Thread.sleep(20)
|
||||
|
||||
assert results != null
|
||||
assert results.results.size() == 1
|
||||
assert results.results.contains(sf2)
|
||||
|
||||
results = null
|
||||
manager.onSearchEvent(new SearchEvent(searchTerms : ['comment'], searchComments : true, oobInfohash : true))
|
||||
Thread.sleep(20)
|
||||
assert results != null
|
||||
assert results.results.size() == 1
|
||||
assert results.results.contains(sf2)
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import com.muwire.core.Destinations
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Personas
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@@ -55,13 +56,16 @@ class TrustServiceTest {
|
||||
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
|
||||
|
||||
Thread.sleep(250)
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
def trusted = new HashSet<>()
|
||||
persistGood.eachLine {
|
||||
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||
def json = slurper.parseText(it)
|
||||
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(json.persona))))
|
||||
}
|
||||
def distrusted = new HashSet<>()
|
||||
persistBad.eachLine {
|
||||
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||
def json = slurper.parseText(it)
|
||||
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(json.persona))))
|
||||
}
|
||||
|
||||
assert trusted.size() == 1
|
||||
|
@@ -1,5 +1,5 @@
|
||||
group = com.muwire
|
||||
version = 0.5.9
|
||||
version = 0.6.1
|
||||
i2pVersion = 0.9.43
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
|
@@ -5,6 +5,7 @@ import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.EventBus
|
||||
@@ -83,8 +84,9 @@ class ContentPanelController {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
if (selectedHit < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Match m = model.hits[selectedHit]
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED))
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -92,8 +94,9 @@ class ContentPanelController {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
if (selectedHit < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Match m = model.hits[selectedHit]
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED))
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
|
@@ -7,10 +7,13 @@ import griffon.core.mvc.MVCGroup
|
||||
import griffon.core.mvc.MVCGroupConfiguration
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import groovy.json.StringEscapeUtils
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
|
||||
import java.awt.Desktop
|
||||
import java.awt.event.ActionEvent
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
@@ -42,6 +45,7 @@ import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
import com.muwire.core.upload.HashListUploader
|
||||
import com.muwire.core.upload.Uploader
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class MainFrameController {
|
||||
@@ -90,6 +94,7 @@ class MainFrameController {
|
||||
params["search-terms"] = search
|
||||
params["uuid"] = uuid.toString()
|
||||
params["core"] = core
|
||||
params["settings"] = view.settings
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
model.results[uuid.toString()] = group
|
||||
|
||||
@@ -119,9 +124,10 @@ class MainFrameController {
|
||||
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
|
||||
|
||||
long timestamp = System.currentTimeMillis()
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig : sig.data))
|
||||
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : DataUtil.signUUID(uuid, timestamp, core.spk)))
|
||||
|
||||
}
|
||||
|
||||
@@ -138,14 +144,16 @@ class MainFrameController {
|
||||
|
||||
byte [] infoHashBytes = Base64.decode(infoHash)
|
||||
Signature sig = DSAEngine.getInstance().sign(infoHashBytes, core.spk)
|
||||
long timestamp = System.currentTimeMillis()
|
||||
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
|
||||
|
||||
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid,
|
||||
oobInfohash: true, persona : core.me)
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig : sig.data))
|
||||
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : sig2))
|
||||
}
|
||||
|
||||
|
||||
private int selectedDownload() {
|
||||
def downloadsTable = builder.getVariable("downloads-table")
|
||||
def selected = downloadsTable.getSelectedRow()
|
||||
@@ -160,8 +168,9 @@ class MainFrameController {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.searches[selected].originator
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED) )
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason) )
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -169,8 +178,9 @@ class MainFrameController {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.searches[selected].originator
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED) )
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason) )
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -217,7 +227,7 @@ class MainFrameController {
|
||||
if (row < 0)
|
||||
return
|
||||
builder.getVariable(tableName).model.fireTableDataChanged()
|
||||
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
|
||||
core.eventBus.publish(new TrustEvent(persona : list[row].persona, level : level))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -255,7 +265,7 @@ class MainFrameController {
|
||||
int row = view.getSelectedTrustTablesRow("trusted-table")
|
||||
if (row < 0)
|
||||
return
|
||||
Persona p = model.trusted[row]
|
||||
Persona p = model.trusted[row].persona
|
||||
core.muOptions.trustSubscriptions.add(p)
|
||||
saveMuWireSettings()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true))
|
||||
@@ -380,7 +390,7 @@ class MainFrameController {
|
||||
@ControllerAction
|
||||
void showFileDetails() {
|
||||
def selected = view.selectedSharedFiles()
|
||||
if (selected.size() != 1) {
|
||||
if (selected == null || selected.size() != 1) {
|
||||
JOptionPane.showMessageDialog(null, "Please select only one file to view it's details")
|
||||
return
|
||||
}
|
||||
@@ -389,6 +399,19 @@ class MainFrameController {
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("shared-file", params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void openContainingFolder() {
|
||||
def selected = view.selectedSharedFiles()
|
||||
if (selected == null || selected.size() != 1) {
|
||||
JOptionPane.showMessageDialog(null, "Please select only one file to open it's containing folder")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Desktop.getDesktop().open(selected[0].file.getParentFile())
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
core.saveMuSettings()
|
||||
|
@@ -155,6 +155,8 @@ class OptionsController {
|
||||
uiSettings.autoFontSize = model.automaticFontSize
|
||||
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
|
||||
|
||||
uiSettings.groupByFile = model.groupByFile
|
||||
|
||||
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
|
||||
model.clearCancelledDownloads = clearCancelledDownloads
|
||||
uiSettings.clearCancelledDownloads = clearCancelledDownloads
|
||||
@@ -242,6 +244,16 @@ class OptionsController {
|
||||
model.closeDecisionMade = true
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void groupByFile() {
|
||||
model.groupByFile = true
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void groupBySender() {
|
||||
model.groupByFile = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clearHistory() {
|
||||
uiSettings.searchHistory.clear()
|
||||
|
@@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
@@ -26,117 +27,109 @@ class SearchTabController {
|
||||
Core core
|
||||
|
||||
private def selectedResults() {
|
||||
int[] rows = view.resultsTable.getSelectedRows()
|
||||
if (rows.length == 0)
|
||||
return null
|
||||
def sortEvt = view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
if (model.groupedByFile) {
|
||||
return [view.getSelectedResult()]
|
||||
} else {
|
||||
int[] rows = view.resultsTable.getSelectedRows()
|
||||
if (rows.length == 0)
|
||||
return null
|
||||
def sortEvt = view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
}
|
||||
}
|
||||
List<UIResultEvent> results = new ArrayList<>()
|
||||
rows.each { results.add(model.results[it]) }
|
||||
return results
|
||||
}
|
||||
List<UIResultEvent> results = new ArrayList<>()
|
||||
rows.each { results.add(model.results[it]) }
|
||||
results
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def results = selectedResults()
|
||||
if (results == null)
|
||||
return
|
||||
|
||||
results.removeAll {
|
||||
!mvcGroup.parentGroup.model.canDownload(it.infohash)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def results = selectedResults()
|
||||
if (results == null)
|
||||
return
|
||||
results.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
|
||||
results.removeAll {
|
||||
!mvcGroup.parentGroup.model.canDownload(it.infohash)
|
||||
}
|
||||
def resultsBucket = model.hashBucket[result.infohash]
|
||||
def sources = model.sourcesBucket[result.infohash]
|
||||
|
||||
results.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
|
||||
target : file, sequential : view.sequentialDownload()))
|
||||
}
|
||||
mvcGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
}
|
||||
|
||||
def resultsBucket = model.hashBucket[result.infohash]
|
||||
def sources = model.sourcesBucket[result.infohash]
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
|
||||
target : file, sequential : view.sequentialDownloadCheckbox.model.isSelected()))
|
||||
}
|
||||
mvcGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
}
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
|
||||
}
|
||||
@ControllerAction
|
||||
void neutral() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED))
|
||||
}
|
||||
@ControllerAction
|
||||
void browse() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
|
||||
@ControllerAction
|
||||
void neutral() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void browse() {
|
||||
int selectedSender = view.selectedSenderRow()
|
||||
if (selectedSender < 0)
|
||||
return
|
||||
Persona sender = model.senders[selectedSender]
|
||||
|
||||
String groupId = sender.getHumanReadableName()
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['host'] = sender
|
||||
params['core'] = core
|
||||
|
||||
mvcGroup.createMVCGroup("browse", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void showComment() {
|
||||
int[] selectedRows = view.resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return
|
||||
if (view.lastSortEvent != null)
|
||||
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
|
||||
UIResultEvent event = model.results[selectedRows[0]]
|
||||
if (event.comment == null)
|
||||
return
|
||||
|
||||
String groupId = Base64.encode(event.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['text'] = event.comment
|
||||
params['name'] = event.name
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewCertificates() {
|
||||
int[] selectedRows = view.resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return
|
||||
if (view.lastSortEvent != null)
|
||||
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
|
||||
UIResultEvent event = model.results[selectedRows[0]]
|
||||
if (event.certificates <= 0)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['result'] = event
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||
}
|
||||
}
|
||||
String groupId = sender.getHumanReadableName()
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['host'] = sender
|
||||
params['core'] = core
|
||||
|
||||
mvcGroup.createMVCGroup("browse", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void showComment() {
|
||||
UIResultEvent event = view.getSelectedResult()
|
||||
if (event == null || event.comment == null)
|
||||
return
|
||||
|
||||
String groupId = Base64.encode(event.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['text'] = event.comment
|
||||
params['name'] = event.name
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewCertificates() {
|
||||
UIResultEvent event = view.getSelectedResult()
|
||||
if (event == null || event.certificates <= 0)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['result'] = event
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||
}
|
||||
}
|
@@ -6,6 +6,23 @@ import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.filecert.Certificate
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class SharedFileController {
|
||||
@MVCMember @Nonnull
|
||||
SharedFileView view
|
||||
@MVCMember @Nonnull
|
||||
SharedFileModel model
|
||||
|
||||
@ControllerAction
|
||||
void showComment() {
|
||||
Certificate cert = view.getSelectedCertificate()
|
||||
if (cert == null || cert.comment == null)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['text'] = cert.comment.name
|
||||
mvcGroup.createMVCGroup('show-comment',params)
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.Persona
|
||||
@@ -25,8 +26,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("trusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.trusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.trusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason))
|
||||
view.fireUpdate("trusted-table")
|
||||
}
|
||||
|
||||
@@ -35,8 +37,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("distrusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.distrusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.distrusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason))
|
||||
view.fireUpdate("distrusted-table")
|
||||
}
|
||||
|
||||
@@ -45,8 +48,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("trusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.trusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.trusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
view.fireUpdate("trusted-table")
|
||||
}
|
||||
|
||||
@@ -55,8 +59,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("distrusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.distrusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.distrusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
view.fireUpdate("distrusted-table")
|
||||
}
|
||||
}
|
@@ -42,6 +42,7 @@ class OptionsModel {
|
||||
@Observable boolean excludeLocalResult
|
||||
@Observable boolean showSearchHashes
|
||||
@Observable boolean clearUploads
|
||||
@Observable boolean groupByFile
|
||||
@Observable boolean exitOnClose
|
||||
@Observable boolean closeDecisionMade
|
||||
|
||||
@@ -92,6 +93,7 @@ class OptionsModel {
|
||||
clearUploads = uiSettings.clearUploads
|
||||
exitOnClose = uiSettings.exitOnClose
|
||||
storeSearchHistory = uiSettings.storeSearchHistory
|
||||
groupByFile = uiSettings.groupByFile
|
||||
|
||||
if (core.router != null) {
|
||||
inBw = String.valueOf(settings.inBw)
|
||||
|
@@ -24,6 +24,7 @@ class SearchTabModel {
|
||||
@Observable boolean browseActionEnabled
|
||||
@Observable boolean viewCommentActionEnabled
|
||||
@Observable boolean viewCertificatesActionEnabled
|
||||
@Observable boolean groupedByFile
|
||||
|
||||
Core core
|
||||
UISettings uiSettings
|
||||
@@ -34,6 +35,9 @@ class SearchTabModel {
|
||||
def sourcesBucket = [:]
|
||||
def sendersBucket = new LinkedHashMap<>()
|
||||
|
||||
def results2 = []
|
||||
def senders2 = []
|
||||
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
core = mvcGroup.parentGroup.model.core
|
||||
@@ -72,9 +76,14 @@ class SearchTabModel {
|
||||
sourcesBucket.put(e.infohash, sourceBucket)
|
||||
}
|
||||
sourceBucket.addAll(e.sources)
|
||||
|
||||
results2.clear()
|
||||
results2.addAll(hashBucket.keySet())
|
||||
|
||||
JTable table = builder.getVariable("senders-table")
|
||||
table.model.fireTableDataChanged()
|
||||
table = builder.getVariable("results-table2")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,9 +116,16 @@ class SearchTabModel {
|
||||
|
||||
bucket << it
|
||||
senderBucket << it
|
||||
|
||||
}
|
||||
results2.clear()
|
||||
results2.addAll(hashBucket.keySet())
|
||||
JTable table = builder.getVariable("senders-table")
|
||||
table.model.fireTableDataChanged()
|
||||
table = builder.getVariable("results-table2")
|
||||
int selectedRow = table.getSelectedRow()
|
||||
table.model.fireTableDataChanged()
|
||||
table.selectionModel.setSelectionInterval(selectedRow, selectedRow)
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,9 +16,11 @@ class SharedFileModel {
|
||||
def downloaders = []
|
||||
def certificates = []
|
||||
|
||||
@Observable boolean showCommentActionEnabled
|
||||
|
||||
public void mvcGroupInit(Map<String,String> args) {
|
||||
searchers.addAll(sf.searches)
|
||||
downloaders.addAll(sf.downloaders)
|
||||
certificates.addAll(core.certificateManager.byInfoHash.get(sf.infoHash))
|
||||
searchers.addAll(sf.getSearches())
|
||||
downloaders.addAll(sf.getDownloaders())
|
||||
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(sf.infoHash,[]))
|
||||
}
|
||||
}
|
@@ -65,10 +65,7 @@ class CertificateControlView {
|
||||
closureColumn(header : "File Name", type : String, read : {it.name.name})
|
||||
closureColumn(header : "Hash", type : String, read : {Base64.encode(it.infoHash.getRoot())})
|
||||
closureColumn(header : "Comment", preferredWidth : 20, type : Boolean, read : {it.comment != null})
|
||||
closureColumn(header : "Timestamp", type : String, read : {
|
||||
def date = new Date(it.timestamp)
|
||||
date.toString()
|
||||
})
|
||||
closureColumn(header : "Timestamp", type : Long, read : { it.timestamp })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +110,8 @@ class CertificateControlView {
|
||||
}
|
||||
})
|
||||
|
||||
certsTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
|
||||
dialog.getContentPane().add(panel)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
|
@@ -422,7 +422,8 @@ class MainFrameView {
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.trusted) {
|
||||
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
|
||||
closureColumn(header : "Trusted Users", type : String, read : { it.persona.getHumanReadableName() } )
|
||||
closureColumn(header : "Reason", type : String, read : {it.reason})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -438,7 +439,8 @@ class MainFrameView {
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.distrusted) {
|
||||
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
|
||||
closureColumn(header: "Distrusted Users", type : String, read : { it.persona.getHumanReadableName() } )
|
||||
closureColumn(header: "Reason", type : String, read : {it.reason})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,12 +463,7 @@ class MainFrameView {
|
||||
closureColumn(header : "Trusted", preferredWidth : 20, type: Integer, read : {it.good.size()})
|
||||
closureColumn(header : "Distrusted", preferredWidth: 20, type: Integer, read : {it.bad.size()})
|
||||
closureColumn(header : "Status", preferredWidth: 30, type: String, read : {it.status.toString()})
|
||||
closureColumn(header : "Last Updated", preferredWidth: 200, type : String, read : {
|
||||
if (it.timestamp == 0)
|
||||
return "Never"
|
||||
else
|
||||
return String.valueOf(new Date(it.timestamp))
|
||||
})
|
||||
closureColumn(header : "Last Updated", preferredWidth: 200, type : Long, read : { it.timestamp })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -611,6 +608,9 @@ class MainFrameView {
|
||||
JMenuItem certifySelectedFiles = new JMenuItem("Certify selected files")
|
||||
certifySelectedFiles.addActionListener({mvcGroup.controller.issueCertificate()})
|
||||
sharedFilesMenu.add(certifySelectedFiles)
|
||||
JMenuItem openContainingFolder = new JMenuItem("Open containing folder")
|
||||
openContainingFolder.addActionListener({mvcGroup.controller.openContainingFolder()})
|
||||
sharedFilesMenu.add(openContainingFolder)
|
||||
JMenuItem showFileDetails = new JMenuItem("Show file details")
|
||||
showFileDetails.addActionListener({mvcGroup.controller.showFileDetails()})
|
||||
sharedFilesMenu.add(showFileDetails)
|
||||
@@ -743,6 +743,8 @@ class MainFrameView {
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
subscriptionTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
|
||||
// trusted table
|
||||
def trustedTable = builder.getVariable("trusted-table")
|
||||
|
@@ -200,10 +200,16 @@ class OptionsView {
|
||||
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))
|
||||
label(text : "By default, group search results by", constraints : gbc(gridx :0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
panel(constraints : gbc(gridx : 1, gridy : 0, anchor : GridBagConstraints.LINE_END)) {
|
||||
buttonGroup(id : "groupBy")
|
||||
radioButton(text : "Sender", selected : bind {!model.groupByFile}, buttonGroup : groupBy, groupBySenderAction)
|
||||
radioButton(text : "File", selected : bind {model.groupByFile}, buttonGroup : groupBy, groupByFileAction)
|
||||
}
|
||||
label(text : "Remember search history", constraints: gbc(gridx: 0, gridy:1, 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)
|
||||
constraints : gbc(gridx : 1, gridy:1, anchor : GridBagConstraints.LINE_END))
|
||||
button(text : "Clear history", constraints : gbc(gridx : 1, gridy : 2, anchor : GridBagConstraints.LINE_END), clearHistoryAction)
|
||||
|
||||
}
|
||||
panel (border : titledBorder(title : "Other Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||
@@ -221,7 +227,7 @@ class OptionsView {
|
||||
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},
|
||||
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))
|
||||
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))
|
||||
|
@@ -17,6 +17,7 @@ import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
@@ -38,77 +39,175 @@ class SearchTabView {
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
SearchTabModel model
|
||||
|
||||
UISettings settings
|
||||
|
||||
def pane
|
||||
def parent
|
||||
def searchTerms
|
||||
def sendersTable
|
||||
def sendersTable, sendersTable2
|
||||
def lastSendersSortEvent
|
||||
def resultsTable
|
||||
def resultsTable, resultsTable2
|
||||
def lastSortEvent
|
||||
def lastResults2SortEvent, lastSenders2SortEvent
|
||||
def sequentialDownloadCheckbox
|
||||
def sequentialDownloadCheckbox2
|
||||
|
||||
void initUI() {
|
||||
int rowHeight = application.context.get("row-height")
|
||||
builder.with {
|
||||
def resultsTable
|
||||
def sendersTable
|
||||
def sequentialDownloadCheckbox
|
||||
def resultsTable, resultsTable2
|
||||
def sendersTable, sendersTable2
|
||||
def sequentialDownloadCheckbox, sequentialDownloadCheckbox2
|
||||
def pane = panel {
|
||||
gridLayout(rows :1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
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()})
|
||||
closureColumn(header : "Browse", preferredWidth : 20, type: Boolean, read : {row -> model.sendersBucket[row].first().browse})
|
||||
closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row ->
|
||||
model.core.trustService.getLevel(row.destination).toString()
|
||||
})
|
||||
borderLayout()
|
||||
panel (id : "results-panel", constraints : BorderLayout.CENTER) {
|
||||
cardLayout()
|
||||
panel (constraints : "grouped-by-sender"){
|
||||
gridLayout(rows :1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
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()})
|
||||
closureColumn(header : "Browse", preferredWidth : 20, type: Boolean, read : {row -> model.sendersBucket[row].first().browse})
|
||||
closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row ->
|
||||
model.core.trustService.getLevel(row.destination).toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows: 1, cols : 2)
|
||||
panel (border : etchedBorder()){
|
||||
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
|
||||
}
|
||||
panel (border : etchedBorder()){
|
||||
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
|
||||
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
|
||||
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
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})
|
||||
closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
|
||||
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()})
|
||||
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
|
||||
closureColumn(header: "Certificates", preferredWidth: 20, type: Integer, read : {row -> row.certificates})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridBagLayout()
|
||||
label(text : "", constraints : gbc(gridx : 0, gridy: 0, weightx : 100))
|
||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, constraints : gbc(gridx : 1, gridy:0), downloadAction)
|
||||
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, constraints : gbc(gridx:2, gridy:0), showCommentAction)
|
||||
button(text : "View Certificates", enabled : bind {model.viewCertificatesActionEnabled}, constraints : gbc(gridx:3, gridy:0), viewCertificatesAction)
|
||||
label(text : "Download sequentially", constraints : gbc(gridx: 4, gridy: 0, weightx : 80, anchor : GridBagConstraints.LINE_END))
|
||||
sequentialDownloadCheckbox = checkBox(constraints : gbc(gridx : 5, gridy: 0, anchor : GridBagConstraints.LINE_END),
|
||||
selected : false, enabled : bind {model.downloadActionEnabled})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows: 1, cols : 2)
|
||||
panel (border : etchedBorder()){
|
||||
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
|
||||
}
|
||||
panel (border : etchedBorder()){
|
||||
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
|
||||
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
|
||||
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
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})
|
||||
closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
|
||||
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()})
|
||||
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
|
||||
closureColumn(header: "Certificates", preferredWidth: 20, type: Integer, read : {row -> row.certificates})
|
||||
panel (constraints : "grouped-by-file") {
|
||||
gridLayout(rows : 1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
resultsTable2 = table(id : "results-table2", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.results2) {
|
||||
closureColumn(header : "Name", preferredWidth : 350, type : String, read : {
|
||||
model.hashBucket[it].first().name.replace('<', '_')
|
||||
})
|
||||
closureColumn(header : "Size", preferredWidth : 20, type : Long, read : {
|
||||
model.hashBucket[it].first().size
|
||||
})
|
||||
closureColumn(header : "Direct Sources", preferredWidth : 20, type : Integer, read : {
|
||||
model.hashBucket[it].size()
|
||||
})
|
||||
closureColumn(header : "Possible Sources", preferredWidth : 20, type : Integer , read : {
|
||||
model.sourcesBucket[it].size()
|
||||
})
|
||||
closureColumn(header : "Comments", preferredWidth : 20, type : Integer, read : {
|
||||
int count = 0
|
||||
model.hashBucket[it].each {
|
||||
if (it.comment != null)
|
||||
count++
|
||||
}
|
||||
count
|
||||
})
|
||||
closureColumn(header : "Certificates", preferredWidth : 20, type : Integer, read : {
|
||||
int count = 0
|
||||
model.hashBucket[it].each {
|
||||
count += it.certificates
|
||||
}
|
||||
count
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows :1, cols : 3)
|
||||
panel {}
|
||||
panel {
|
||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
|
||||
}
|
||||
panel {
|
||||
gridBagLayout()
|
||||
label(text : "Download sequentially", constraints : gbc(gridx : 0, gridy : 0, weightx : 100, anchor : GridBagConstraints.LINE_END))
|
||||
sequentialDownloadCheckbox2 = checkBox( constraints : gbc(gridx: 1, gridy:0, weightx: 0, anchor : GridBagConstraints.LINE_END))
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
sendersTable2 = table(id : "senders-table2", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.senders2) {
|
||||
closureColumn(header : "Sender", preferredWidth : 350, type : String, read : {it.sender.getHumanReadableName()})
|
||||
closureColumn(header : "Browse", preferredWidth : 20, type : Boolean, read : {it.browse})
|
||||
closureColumn(header : "Comment", preferredWidth : 20, type : Boolean, read : {it.comment != null})
|
||||
closureColumn(header : "Certificates", preferredWidth : 20, type: Integer, read : {it.certificates})
|
||||
closureColumn(header : "Trust", preferredWidth : 50, type : String, read : {
|
||||
model.core.trustService.getLevel(it.sender.destination).toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows : 1, cols : 2)
|
||||
panel (border : etchedBorder()) {
|
||||
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
|
||||
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, showCommentAction)
|
||||
button(text : "View Certificates", enabled : bind {model.viewCertificatesActionEnabled}, viewCertificatesAction)
|
||||
}
|
||||
panel (border : etchedBorder()) {
|
||||
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
|
||||
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
|
||||
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridBagLayout()
|
||||
label(text : "", constraints : gbc(gridx : 0, gridy: 0, weightx : 100))
|
||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, constraints : gbc(gridx : 1, gridy:0), downloadAction)
|
||||
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, constraints : gbc(gridx:2, gridy:0), showCommentAction)
|
||||
button(text : "View Certificates", enabled : bind {model.viewCertificatesActionEnabled}, constraints : gbc(gridx:3, gridy:0), viewCertificatesAction)
|
||||
label(text : "Download sequentially", constraints : gbc(gridx: 4, gridy: 0, weightx : 80, anchor : GridBagConstraints.LINE_END))
|
||||
sequentialDownloadCheckbox = checkBox(constraints : gbc(gridx : 5, gridy: 0, anchor : GridBagConstraints.LINE_END),
|
||||
selected : false, enabled : bind {model.downloadActionEnabled})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
label(text : "Group by")
|
||||
buttonGroup(id : "groupBy")
|
||||
radioButton(text : "Sender", selected : bind {!model.groupedByFile}, buttonGroup : groupBy, actionPerformed: showSenderGrouping)
|
||||
radioButton(text : "File", selected : bind {model.groupedByFile}, buttonGroup : groupBy, actionPerformed: showFileGrouping)
|
||||
}
|
||||
}
|
||||
|
||||
this.pane = pane
|
||||
@@ -117,7 +216,10 @@ class SearchTabView {
|
||||
|
||||
this.resultsTable = resultsTable
|
||||
this.sendersTable = sendersTable
|
||||
this.resultsTable2 = resultsTable2
|
||||
this.sendersTable2 = sendersTable2
|
||||
this.sequentialDownloadCheckbox = sequentialDownloadCheckbox
|
||||
this.sequentialDownloadCheckbox2 = sequentialDownloadCheckbox2
|
||||
|
||||
def selectionModel = resultsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
|
||||
@@ -222,7 +324,67 @@ class SearchTabView {
|
||||
resultsTable.model.fireTableDataChanged()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// results table 2
|
||||
resultsTable2.setDefaultRenderer(Integer.class,centerRenderer)
|
||||
resultsTable2.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||
resultsTable2.rowSorter.addRowSorterListener({evt -> lastResults2SortEvent = evt})
|
||||
resultsTable2.rowSorter.setSortsOnUpdates(true)
|
||||
selectionModel = resultsTable2.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
UIResultEvent e = getSelectedResult()
|
||||
if (e == null) {
|
||||
model.trustButtonsEnabled = false
|
||||
model.browseActionEnabled = false
|
||||
model.viewCertificatesActionEnabled = false
|
||||
return
|
||||
}
|
||||
model.downloadActionEnabled = true
|
||||
def results = model.hashBucket[e.infohash]
|
||||
model.senders2.clear()
|
||||
model.senders2.addAll(results)
|
||||
int selectedRow = sendersTable2.getSelectedRow()
|
||||
sendersTable2.model.fireTableDataChanged()
|
||||
if (selectedRow < results.size())
|
||||
sendersTable2.selectionModel.setSelectionInterval(selectedRow,selectedRow)
|
||||
})
|
||||
|
||||
resultsTable2.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() > 1 && e.button == MouseEvent.BUTTON1)
|
||||
mvcGroup.controller.download()
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: add download right-click action
|
||||
|
||||
// senders table 2
|
||||
sendersTable2.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
sendersTable2.rowSorter.addRowSorterListener({ evt -> lastSenders2SortEvent = evt})
|
||||
sendersTable2.rowSorter.setSortsOnUpdates(true)
|
||||
selectionModel = sendersTable2.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int row = selectedSenderRow()
|
||||
if (row < 0 || model.senders2[row] == null) {
|
||||
model.browseActionEnabled = false
|
||||
model.viewCertificatesActionEnabled = false
|
||||
model.trustButtonsEnabled = false
|
||||
model.viewCommentActionEnabled = false
|
||||
return
|
||||
}
|
||||
model.browseActionEnabled = model.senders2[row].browse
|
||||
model.trustButtonsEnabled = true
|
||||
model.viewCommentActionEnabled = model.senders2[row].comment != null
|
||||
model.viewCertificatesActionEnabled = model.senders2[row].certificates > 0
|
||||
})
|
||||
|
||||
if (settings.groupByFile)
|
||||
showFileGrouping.call()
|
||||
else
|
||||
showSenderGrouping.call()
|
||||
}
|
||||
|
||||
def closeTab = {
|
||||
@@ -269,14 +431,35 @@ class SearchTabView {
|
||||
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||
}
|
||||
|
||||
private UIResultEvent getSelectedResult() {
|
||||
int[] selectedRows = resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return null
|
||||
int selected = selectedRows[0]
|
||||
if (lastSortEvent != null)
|
||||
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
|
||||
model.results[selected]
|
||||
UIResultEvent getSelectedResult() {
|
||||
if (model.groupedByFile) {
|
||||
int selectedRow = resultsTable2.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return null
|
||||
if (lastResults2SortEvent != null)
|
||||
selectedRow = resultsTable2.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
InfoHash infohash = model.results2[selectedRow]
|
||||
|
||||
Persona sender = selectedSender()
|
||||
if (sender == null) // really shouldn't happen
|
||||
return model.hashBucket[infohash].first()
|
||||
|
||||
for (UIResultEvent candidate : model.hashBucket[infohash]) {
|
||||
if (candidate.sender == sender)
|
||||
return candidate
|
||||
}
|
||||
|
||||
// also shouldn't happen
|
||||
return model.hashBucket[infohash].first()
|
||||
} else {
|
||||
int[] selectedRows = resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return null
|
||||
int selected = selectedRows[0]
|
||||
if (lastSortEvent != null)
|
||||
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
|
||||
return model.results[selected]
|
||||
}
|
||||
}
|
||||
|
||||
def copyHashToClipboard() {
|
||||
@@ -299,11 +482,49 @@ class SearchTabView {
|
||||
}
|
||||
|
||||
int selectedSenderRow() {
|
||||
int row = sendersTable.getSelectedRow()
|
||||
if (model.groupedByFile) {
|
||||
int row = sendersTable2.getSelectedRow()
|
||||
if (row < 0)
|
||||
return row
|
||||
if (lastSenders2SortEvent != null)
|
||||
row = sendersTable2.rowSorter.convertRowIndexToModel(row)
|
||||
return row
|
||||
} else {
|
||||
int row = sendersTable.getSelectedRow()
|
||||
if (row < 0)
|
||||
return -1
|
||||
if (lastSendersSortEvent != null)
|
||||
row = sendersTable.rowSorter.convertRowIndexToModel(row)
|
||||
return row
|
||||
}
|
||||
}
|
||||
|
||||
Persona selectedSender() {
|
||||
int row = selectedSenderRow()
|
||||
if (row < 0)
|
||||
return -1
|
||||
if (lastSendersSortEvent != null)
|
||||
row = sendersTable.rowSorter.convertRowIndexToModel(row)
|
||||
row
|
||||
return null
|
||||
if (model.groupedByFile)
|
||||
return model.senders2[row]?.sender
|
||||
else
|
||||
return model.senders[row]
|
||||
}
|
||||
|
||||
def showSenderGrouping = {
|
||||
model.groupedByFile = false
|
||||
def cardsPanel = builder.getVariable("results-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "grouped-by-sender")
|
||||
}
|
||||
|
||||
def showFileGrouping = {
|
||||
model.groupedByFile = true
|
||||
def cardsPanel = builder.getVariable("results-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "grouped-by-file")
|
||||
}
|
||||
|
||||
boolean sequentialDownload() {
|
||||
if (model.groupedByFile)
|
||||
return sequentialDownloadCheckbox2.model.isSelected()
|
||||
else
|
||||
return sequentialDownloadCheckbox.model.isSelected()
|
||||
}
|
||||
}
|
@@ -5,10 +5,17 @@ import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JMenuItem
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.JTabbedPane
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import com.muwire.core.filecert.Certificate
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
@@ -20,14 +27,18 @@ class SharedFileView {
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
SharedFileModel model
|
||||
@MVCMember @Nonnull
|
||||
SharedFileController controller
|
||||
|
||||
def mainFrame
|
||||
def dialog
|
||||
def panel
|
||||
def searchersPanel
|
||||
def searchersTable
|
||||
def downloadersPanel
|
||||
def certificatesTable
|
||||
def certificatesPanel
|
||||
def lastCertificateSortEvent
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
@@ -38,14 +49,11 @@ class SharedFileView {
|
||||
searchersPanel = builder.panel {
|
||||
borderLayout()
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
searchersTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.searchers) {
|
||||
closureColumn(header : "Searcher", type : String, read : {it.searcher.getHumanReadableName()})
|
||||
closureColumn(header : "Searcher", type : String, read : {it.searcher?.getHumanReadableName()})
|
||||
closureColumn(header : "Query", type : String, read : {it.query})
|
||||
closureColumn(header : "Timestamp", type : String, read : {
|
||||
Date d = new Date(it.timestamp)
|
||||
d.toString()
|
||||
})
|
||||
closureColumn(header : "Timestamp", type : Long, read : {it.timestamp})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,17 +78,42 @@ class SharedFileView {
|
||||
closureColumn(header : "Issuer", type:String, read : {it.issuer.getHumanReadableName()})
|
||||
closureColumn(header : "File Name", type : String, read : {it.name.name})
|
||||
closureColumn(header : "Comment", type : Boolean, read : {it.comment != null})
|
||||
closureColumn(header : "Timestamp", type : String, read : {
|
||||
Date d = new Date(it.timestamp)
|
||||
d.toString()
|
||||
})
|
||||
closureColumn(header : "Timestamp", type : Long, read : {it.timestamp})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Show Comment", enabled : bind {model.showCommentActionEnabled}, showCommentAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
|
||||
certificatesTable.rowSorter.addRowSorterListener({evt -> lastCertificateSortEvent = evt})
|
||||
def selectionModel = certificatesTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
Certificate c = getSelectedCertificate()
|
||||
model.showCommentActionEnabled = c != null && c.comment != null
|
||||
})
|
||||
|
||||
certificatesTable.addMouseListener(new MouseAdapter() {
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
})
|
||||
|
||||
certificatesTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
|
||||
|
||||
searchersTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
|
||||
def tabbedPane = new JTabbedPane()
|
||||
tabbedPane.addTab("Search Hits", searchersPanel)
|
||||
tabbedPane.addTab("Downloaders", downloadersPanel)
|
||||
@@ -99,4 +132,23 @@ class SharedFileView {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
Certificate getSelectedCertificate() {
|
||||
int selectedRow = certificatesTable.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return null
|
||||
if (lastCertificateSortEvent != null)
|
||||
selectedRow = certificatesTable.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
model.certificates[selectedRow]
|
||||
}
|
||||
|
||||
private void showMenu(MouseEvent e) {
|
||||
if (!model.showCommentActionEnabled)
|
||||
return
|
||||
JPopupMenu menu = new JPopupMenu()
|
||||
JMenuItem showComment = new JMenuItem("Show Comment")
|
||||
showComment.addActionListener({controller.showComment()})
|
||||
menu.add(showComment)
|
||||
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||
}
|
||||
}
|
@@ -49,8 +49,9 @@ class TrustListView {
|
||||
scrollPane (constraints : BorderLayout.CENTER){
|
||||
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()})
|
||||
closureColumn(header: "Trusted Users", type : String, read : {it.persona.getHumanReadableName()})
|
||||
closureColumn(header: "Reason", type : String, read : {it.reason})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.persona.destination).toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,8 +66,9 @@ class TrustListView {
|
||||
scrollPane (constraints : BorderLayout.CENTER ){
|
||||
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()})
|
||||
closureColumn(header: "Distrusted Users", type : String, read : {it.persona.getHumanReadableName()})
|
||||
closureColumn(header: "Reason", type:String, read : {it.reason})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.persona.destination).toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
gui/src/main/groovy/com/muwire/gui/DateRenderer.groovy
Normal file
33
gui/src/main/groovy/com/muwire/gui/DateRenderer.groovy
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JTable
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class DateRenderer extends DefaultTableCellRenderer {
|
||||
DateRenderer(){
|
||||
setHorizontalAlignment(JLabel.CENTER)
|
||||
}
|
||||
|
||||
JComponent getTableCellRendererComponent(JTable table, Object value,
|
||||
boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
Long l = (Long) value
|
||||
String formatted
|
||||
if (l == 0)
|
||||
formatted = "Never"
|
||||
else
|
||||
formatted = DataHelper.formatTime(l)
|
||||
setText(formatted)
|
||||
if (isSelected) {
|
||||
setForeground(table.getSelectionForeground())
|
||||
setBackground(table.getSelectionBackground())
|
||||
} else {
|
||||
setForeground(table.getForeground())
|
||||
setBackground(table.getBackground())
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
@@ -14,6 +14,10 @@ class SizeRenderer extends DefaultTableCellRenderer {
|
||||
@Override
|
||||
JComponent getTableCellRendererComponent(JTable table, Object value,
|
||||
boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
if (value == null) {
|
||||
// this is very strange, but it happens. Probably a swing bug?
|
||||
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
|
||||
}
|
||||
Long l = (Long) value
|
||||
String formatted = DataHelper.formatSize2Decimal(l, false)+"B"
|
||||
setText(formatted)
|
||||
|
@@ -18,6 +18,7 @@ class UISettings {
|
||||
boolean exitOnClose
|
||||
boolean clearUploads
|
||||
boolean storeSearchHistory
|
||||
boolean groupByFile
|
||||
Set<String> searchHistory
|
||||
Set<String> openTabs
|
||||
|
||||
@@ -36,6 +37,7 @@ class UISettings {
|
||||
exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false"))
|
||||
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads","false"))
|
||||
storeSearchHistory = Boolean.parseBoolean(props.getProperty("storeSearchHistory","true"))
|
||||
groupByFile = Boolean.parseBoolean(props.getProperty("groupByFile","false"))
|
||||
|
||||
searchHistory = DataUtil.readEncodedSet(props, "searchHistory")
|
||||
openTabs = DataUtil.readEncodedSet(props, "openTabs")
|
||||
@@ -56,6 +58,7 @@ class UISettings {
|
||||
props.setProperty("exitOnClose", String.valueOf(exitOnClose))
|
||||
props.setProperty("clearUploads", String.valueOf(clearUploads))
|
||||
props.setProperty("storeSearchHistory", String.valueOf(storeSearchHistory))
|
||||
props.setProperty("groupByFile", String.valueOf(groupByFile))
|
||||
if (font != null)
|
||||
props.setProperty("font", font)
|
||||
|
||||
|
Reference in New Issue
Block a user