Compare commits

...

26 Commits

Author SHA1 Message Date
Zlatin Balevsky
040248560a Release 0.1.11 2019-06-14 22:26:28 +01:00
Zlatin Balevsky
77caaf83de reset instead of close 2019-06-14 22:08:25 +01:00
Zlatin Balevsky
cc5ece5103 do not throw exception on shutdown 2019-06-14 21:36:50 +01:00
Zlatin Balevsky
db7e21e343 close connections in parallel, more shutdown fixes 2019-06-14 21:25:22 +01:00
Zlatin Balevsky
a388eaec1d shutdown all connections on shutdown 2019-06-14 20:53:54 +01:00
Zlatin Balevsky
8ff39072c7 download file on double-clicking a result 2019-06-14 20:42:26 +01:00
Zlatin Balevsky
55d2ac9b24 delete partial files and pieces file on cancel 2019-06-14 20:27:14 +01:00
Zlatin Balevsky
6ebe492fd8 if nothing is enabled cancel and retry buttons are disabled 2019-06-14 18:37:18 +01:00
Zlatin Balevsky
165cd542ec work around not having a selected row while cancelling a download 2019-06-14 18:28:00 +01:00
Zlatin Balevsky
5ca0c8b00d wip on unshare selected files popup menu 2019-06-14 18:08:56 +01:00
Zlatin Balevsky
b6a38e3f23 revert to default lnf if the desired one fails 2019-06-14 18:01:14 +01:00
Zlatin Balevsky
34d9165bd5 Release 0.1.10 2019-06-14 16:43:28 +01:00
Zlatin Balevsky
2e52dd5c49 fix overwriting of custom nickname 2019-06-14 16:20:21 +01:00
Zlatin Balevsky
2a315dd734 add option to exclude local results from searches 2019-06-14 14:48:01 +01:00
Zlatin Balevsky
6b661b99c5 fix sorting by size in shared files table 2019-06-14 13:47:35 +01:00
Zlatin Balevsky
5dacd60bbb hook up cleaning up of cancelled/finished downloads 2019-06-14 13:11:20 +01:00
Zlatin Balevsky
f8f7cfe836 UI options panel 2019-06-14 12:51:27 +01:00
Zlatin Balevsky
0b4f261bc1 ability to not show monitor panel 2019-06-14 12:21:14 +01:00
Zlatin Balevsky
042d67d784 fix selection of size column 2019-06-14 11:46:31 +01:00
Zlatin Balevsky
800df88f14 proper sorting by size 2019-06-14 11:10:19 +01:00
Zlatin Balevsky
4d1eac50a0 update readme for sorting bug 2019-06-14 10:39:58 +01:00
Zlatin Balevsky
c48df7f14b Release 0.1.9 2019-06-13 22:57:08 +01:00
Zlatin Balevsky
9d04148001 remember loaded downloads from previous sessions 2019-06-13 22:53:23 +01:00
Zlatin Balevsky
bb4d522572 Release 0.1.8 2019-06-13 15:27:06 +01:00
Zlatin Balevsky
8052501e52 increase persistence interval to 15 seconds 2019-06-13 15:25:30 +01:00
Zlatin Balevsky
66cc6d8ab7 reduce piece size by factor of 8 2019-06-13 15:24:26 +01:00
23 changed files with 256 additions and 26 deletions

View File

@@ -31,4 +31,4 @@ The first time you run MuWire it will ask you to select a nickname. This nickna
### Known bugs and limitations
* Many UI features you would expect are not there yet
* Sorting the results table sometimes causes the wrong result to be downloaded

View File

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

View File

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

View File

@@ -69,6 +69,7 @@ public class Core {
private final ConnectionAcceptor connectionAcceptor
private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService
private final DownloadManager downloadManager
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
@@ -92,9 +93,9 @@ public class Core {
if (i2pOptionsFile.exists()) {
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
if (!i2pOptions.hasProperty("inbound.nickname"))
if (!i2pOptions.containsKey("inbound.nickname"))
i2pOptions["inbound.nickname"] = "MuWire"
if (!i2pOptions.hasProperty("outbound.nickname"))
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
} else {
i2pOptions["inbound.nickname"] = "MuWire"
@@ -160,7 +161,7 @@ public class Core {
eventBus.register(SearchEvent.class, fileManager)
log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 5000, fileManager)
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json")
@@ -195,7 +196,7 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager")
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, home, me)
downloadManager = new DownloadManager(eventBus, i2pConnector, home, me)
eventBus.register(UIDownloadEvent.class, downloadManager)
eventBus.register(UILoadedEvent.class, downloadManager)
eventBus.register(FileDownloadedEvent.class, downloadManager)
@@ -233,7 +234,14 @@ public class Core {
}
public void shutdown() {
log.info("shutting down connection manager")
connectionManager.shutdown()
log.info("shutting down download manageer")
downloadManager.shutdown()
log.info("shutting down connection acceeptor")
connectionAcceptor.stop()
log.info("shutting down connection establisher")
connectionEstablisher.stop()
}
static main(args) {
@@ -260,7 +268,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.1.7")
Core core = new Core(props, home, "0.1.11")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -76,9 +76,9 @@ abstract class Connection implements Closeable {
return
}
log.info("closing $name")
endpoint.close()
reader.interrupt()
writer.interrupt()
endpoint.close()
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
}

View File

@@ -38,6 +38,8 @@ class ConnectionAcceptor {
final ExecutorService acceptorThread
final ExecutorService handshakerThreads
private volatile shutdown
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
@@ -73,11 +75,13 @@ class ConnectionAcceptor {
}
void stop() {
shutdown = true
acceptorThread.shutdownNow()
handshakerThreads.shutdownNow()
}
private void acceptLoop() {
try {
while(true) {
def incoming = acceptor.accept()
log.info("accepted connection from ${incoming.destination.toBase32()}")
@@ -93,6 +97,11 @@ class ConnectionAcceptor {
}
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
}
} catch (Exception e) {
log.log(Level.WARNING, "exception in accept loop",e)
if (!shutdown)
throw e
}
}
private void processIncoming(Endpoint e) {

View File

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

View File

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

View File

@@ -99,6 +99,7 @@ public class DownloadManager {
}
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
downloaders.add(downloader)
downloader.download()
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
}
@@ -134,4 +135,9 @@ public class DownloadManager {
}
}
}
public void shutdown() {
downloaders.each { it.stop() }
Downloader.executorService.shutdownNow()
}
}

View File

@@ -169,6 +169,12 @@ public class Downloader {
public void cancel() {
cancelled = true
stop()
file.delete()
piecesFile.delete()
}
void stop() {
activeWorkers.values().each {
it.cancel()
}

View File

@@ -20,12 +20,12 @@ class FileHasher {
* @return the size of each piece in power of 2
*/
static int getPieceSize(long size) {
if (size <= 0x1 << 27)
if (size <= 0x1 << 30)
return 17
for (int i = 28; i <= 37; i++) {
for (int i = 31; i <= 37; i++) {
if (size <= 0x1L << i) {
return i-10
return i-13
}
}

View File

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.1.7
version = 0.1.11
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4

View File

@@ -181,6 +181,10 @@ class MainFrameController {
model.hashSearch = true
}
void unshareSelectedFiles() {
println "unsharing selected files"
}
void mvcGroupInit(Map<String, String> args) {
application.addPropertyChangeListener("core", {e->
core = e.getNewValue()

View File

@@ -66,6 +66,38 @@ class OptionsController {
settings.write(it)
}
// UI Setttings
UISettings uiSettings = application.context.get("ui-settings")
text = view.lnfField.text
model.lnf = text
uiSettings.lnf = text
text = view.fontField.text
model.font = text
uiSettings.font = text
boolean showMonitor = view.monitorCheckbox.model.isSelected()
model.showMonitor = showMonitor
uiSettings.showMonitor = showMonitor
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
model.clearCancelledDownloads = clearCancelledDownloads
uiSettings.clearCancelledDownloads = clearCancelledDownloads
boolean clearFinishedDownloads = view.clearFinishedDownloadsCheckbox.model.isSelected()
model.clearFinishedDownloads = clearFinishedDownloads
uiSettings.clearFinishedDownloads = clearFinishedDownloads
boolean excludeLocalResult = view.excludeLocalResultCheckbox.model.isSelected()
model.excludeLocalResult = excludeLocalResult
uiSettings.excludeLocalResult = excludeLocalResult
File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream {
uiSettings.write(it)
}
cancel()
}

View File

@@ -18,6 +18,7 @@ import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel
import java.awt.Font
import java.util.logging.Level
@Log
class Initialize extends AbstractLifecycleHandler {
@@ -50,7 +51,12 @@ class Initialize extends AbstractLifecycleHandler {
uiSettings = new UISettings(props)
log.info("settting user-specified lnf $uiSettings.lnf")
lookAndFeel(uiSettings.lnf)
try {
lookAndFeel(uiSettings.lnf)
} catch (Throwable bad) {
log.log(Level.WARNING,"couldn't set desired look and feeel, switching to defaults", bad)
uiSettings.lnf = lookAndFeel("system","gtk","metal").getID()
}
if (uiSettings.font != null) {
log.info("setting user-specified font $uiSettings.font")
@@ -78,7 +84,7 @@ class Initialize extends AbstractLifecycleHandler {
}
} else {
LookAndFeel chosen = lookAndFeel('system', 'gtk')
uiSettings.lnf = chosen.name
uiSettings.lnf = chosen.getID()
log.info("ended up applying $chosen.name")
}
}

View File

@@ -79,11 +79,28 @@ class MainFrameModel {
void mvcGroupInit(Map<String, Object> args) {
UISettings uiSettings = application.context.get("ui-settings")
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
if (!mvcGroup.alive)
return
// remove cancelled or finished downloads
def toRemove = []
downloads.each {
if (uiSettings.clearCancelledDownloads &&
it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED)
toRemove << it
if (uiSettings.clearFinishedDownloads &&
it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED)
toRemove << it
}
toRemove.each {
downloads.remove(it)
}
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
updateTablePreservingSelection("downloads-table")

View File

@@ -20,6 +20,14 @@ class OptionsModel {
@Observable String outboundLength
@Observable String outboundQuantity
// gui options
@Observable boolean showMonitor
@Observable String lnf
@Observable String font
@Observable boolean clearCancelledDownloads
@Observable boolean clearFinishedDownloads
@Observable boolean excludeLocalResult
void mvcGroupInit(Map<String, String> args) {
MuWireSettings settings = application.context.get("muwire-settings")
downloadRetryInterval = settings.downloadRetryInterval
@@ -32,5 +40,13 @@ class OptionsModel {
inboundQuantity = core.i2pOptions["inbound.quantity"]
outboundLength = core.i2pOptions["outbound.length"]
outboundQuantity = core.i2pOptions["outbound.quantity"]
UISettings uiSettings = application.context.get("ui-settings")
showMonitor = uiSettings.showMonitor
lnf = uiSettings.lnf
font = uiSettings.font
clearCancelledDownloads = uiSettings.clearCancelledDownloads
clearFinishedDownloads = uiSettings.clearFinishedDownloads
excludeLocalResult = uiSettings.excludeLocalResult
}
}

View File

@@ -19,6 +19,7 @@ class SearchTabModel {
FactoryBuilderSupport builder
Core core
UISettings uiSettings
String uuid
def results = []
def hashBucket = [:]
@@ -26,6 +27,7 @@ class SearchTabModel {
void mvcGroupInit(Map<String, String> args) {
core = mvcGroup.parentGroup.model.core
uiSettings = application.context.get("ui-settings")
mvcGroup.parentGroup.model.results[UUID.fromString(uuid)] = mvcGroup
}
@@ -34,6 +36,9 @@ class SearchTabModel {
}
void handleResult(UIResultEvent e) {
if (uiSettings.excludeLocalResult &&
e.sender == core.me)
return
runInsideUIAsync {
def bucket = hashBucket.get(e.infohash)
if (bucket == null) {

View File

@@ -10,6 +10,8 @@ import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JFileChooser
import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JSplitPane
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
@@ -26,6 +28,8 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.Insets
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
@@ -41,6 +45,7 @@ class MainFrameView {
def lastDownloadSortEvent
void initUI() {
UISettings settings = application.context.get("ui-settings")
builder.with {
application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null,
@@ -63,7 +68,8 @@ class MainFrameView {
gridLayout(rows:1, cols: 2)
button(text: "Searches", actionPerformed : showSearchWindow)
button(text: "Uploads", actionPerformed : showUploadsWindow)
button(text: "Monitor", actionPerformed : showMonitorWindow)
if (settings.showMonitor)
button(text: "Monitor", actionPerformed : showMonitorWindow)
button(text: "Trust", actionPerformed : showTrustWindow)
}
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
@@ -142,8 +148,7 @@ class MainFrameView {
table(id : "shared-files-table", autoCreateRowSorter: true) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", preferredWidth : 50, type : String,
read : {row -> DataHelper.formatSize2Decimal(row.file.length(),false) + "B"})
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.file.length() })
}
}
}
@@ -264,7 +269,14 @@ class MainFrameView {
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
int selectedRow = selectedDownloaderRow()
def downloader = model.downloads[selectedRow].downloader
if (selectedRow < 0) {
model.cancelButtonEnabled = false
model.retryButtonEnabled = false
return
}
def downloader = model.downloads[selectedRow]?.downloader
if (downloader == null)
return
switch(downloader.getCurrentState()) {
case Downloader.DownloadState.CONNECTING :
case Downloader.DownloadState.DOWNLOADING :
@@ -286,6 +298,31 @@ class MainFrameView {
downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
// shared files table
def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
JPopupMenu sharedFilesMenu = new JPopupMenu()
JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFiles()})
sharedFilesMenu.add(unshareSelectedFiles)
sharedFilesTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
})
}
def showPopupMenu(JPopupMenu menu, MouseEvent event) {
menu.show(event.getComponent(), event.getX(), event.getY())
}
int selectedDownloaderRow() {

View File

@@ -25,6 +25,8 @@ class OptionsView {
def d
def p
def i
def u
def retryField
def updateField
def allowUntrustedCheckbox
@@ -35,6 +37,13 @@ class OptionsView {
def outboundLengthField
def outboundQuantityField
def lnfField
def monitorCheckbox
def fontField
def clearCancelledDownloadsCheckbox
def clearFinishedDownloadsCheckbox
def excludeLocalResultCheckbox
def buttonsPanel
def mainFrame
@@ -72,6 +81,22 @@ class OptionsView {
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
}
u = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1))
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1))
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2))
label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5))
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5))
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6))
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
}
buttonsPanel = builder.panel {
gridBagLayout()
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
@@ -81,8 +106,9 @@ class OptionsView {
void mvcGroupInit(Map<String,String> args) {
def tabbedPane = new JTabbedPane()
tabbedPane.addTab("MuWire Options", p)
tabbedPane.addTab("I2P Options", i)
tabbedPane.addTab("MuWire", p)
tabbedPane.addTab("I2P", i)
tabbedPane.addTab("GUI", u)
JPanel panel = new JPanel()
panel.setLayout(new BorderLayout())

View File

@@ -6,12 +6,19 @@ import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.DataHelper
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JTable
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.util.DataUtil
import java.awt.BorderLayout
import java.awt.Color
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.annotation.Nonnull
@@ -35,7 +42,7 @@ class SearchTabView {
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
tableModel(list: model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 50, type: String, read : {row -> DataHelper.formatSize2Decimal(row.size, false)+"B"})
closureColumn(header: "Size", preferredWidth: 50, type: Long, read : {row -> row.size})
closureColumn(header: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row ->
@@ -86,7 +93,18 @@ class SearchTabView {
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer)
resultsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
resultsTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
mvcGroup.parentGroup.controller.download()
}
})
}
def closeTab = {

View File

@@ -0,0 +1,29 @@
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 SizeRenderer extends DefaultTableCellRenderer {
SizeRenderer() {
setHorizontalAlignment(JLabel.CENTER)
}
@Override
JComponent getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
Long l = (Long) value
String formatted = DataHelper.formatSize2Decimal(l, false)+"B"
setText(formatted)
if (isSelected) {
setForeground(table.getSelectionForeground())
setBackground(table.getSelectionBackground())
} else {
setForeground(table.getForeground())
setBackground(table.getBackground())
}
this
}
}

View File

@@ -5,19 +5,30 @@ class UISettings {
String lnf
boolean showMonitor
String font
boolean clearCancelledDownloads
boolean clearFinishedDownloads
boolean excludeLocalResult
UISettings(Properties props) {
lnf = props.getProperty("lnf", "system")
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "true"))
font = props.getProperty("font",null)
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false"))
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false"))
}
void write(OutputStream out) throws IOException {
Properties props = new Properties()
props.setProperty("lnf", lnf)
props.setProperty("showMonitor", showMonitor)
props.setProperty("showMonitor", String.valueOf(showMonitor))
props.setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
if (font != null)
props.setProperty("font", font)
props.store(out, "UI Properties")
}
}