Compare commits

...

27 Commits

Author SHA1 Message Date
7718dc0821 force UTF8 for json serialization 2019-06-16 12:13:00 +01:00
e295aa67d5 proper log statement 2019-06-16 10:59:11 +01:00
a9f5625dc3 fix popup menu on failed downloads 2019-06-16 10:50:21 +01:00
cc0af5b9ed add context menu to downloads table 2019-06-16 10:29:28 +01:00
041fc3bef3 Release 0.2.1 2019-06-16 09:37:53 +01:00
03c3b1ebf1 fix copying of hash if search results are sorted 2019-06-16 09:30:52 +01:00
aece390daa right-click menu on the search results tab 2019-06-16 09:17:17 +01:00
cf63be68e8 copy search to clipboard 2019-06-16 08:38:47 +01:00
88ece4dc23 add option to show search hashes in monitor 2019-06-16 08:29:03 +01:00
13767d58f2 detect if a query is hash, get rid of radio buttons 2019-06-16 08:09:51 +01:00
05a1ccd3d8 update todo 2019-06-16 07:31:01 +01:00
6807c14a5f add copy hash to clipboard 2019-06-16 07:23:22 +01:00
684be0c50e start of work on directory watcher 2019-06-16 07:03:16 +01:00
6655c262c6 more todo items 2019-06-16 07:01:50 +01:00
b1ccd55030 more todo items 2019-06-16 06:26:03 +01:00
a3becd0f7e update TODO 2019-06-16 06:19:28 +01:00
af2f3e0ebf in/out direction done 2019-06-16 05:56:56 +01:00
e2b7ffa1db direction in monitor tab 2019-06-16 05:52:23 +01:00
0e0176acfc add web UI to TODO list 2019-06-16 05:35:05 +01:00
7f09bb079c Beginnings of a TODO list 2019-06-16 05:28:42 +01:00
77e48b01bb Release 0.2.0 2019-06-15 21:10:11 +01:00
12db6857c1 disable unshare files popup until implemented 2019-06-15 12:12:08 +01:00
acd67733a5 sort the downloads table on updates 2019-06-15 12:08:29 +01:00
8d3ce7aa8e use the same sorted row selection logic in downloads table 2019-06-15 09:57:12 +01:00
0eb5870e9b Release 0.1.13 2019-06-15 09:19:19 +01:00
051efbfaba prevent empty searches 2019-06-15 09:11:42 +01:00
6b38d7bffb fix sorting bug try 2 2019-06-15 08:58:51 +01:00
17 changed files with 295 additions and 46 deletions

42
TODO.md Normal file
View File

@ -0,0 +1,42 @@
# TODO List
Not in any particular order yet
### Big Items
##### Alternate Locations
This helps peers discover new sources for a file while the download is in progress. Also makes sharing of partial files possible.
##### Bloom Filters
This reduces query traffic by not sending last hop queries to peers that definitely do not have the file
##### Two-tier Topology
This helps with scalability
##### Trust List Sharing
For helping users make better decisions whom to trust
##### Content Control Panel
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple
##### Packaging With JRE, Embedded Router
For ease of deployment for new users, and so that users do not need to run a separate I2P router
##### Web UI, REST Interface, etc.
Basically any non-gui non-cli user interface
### Small Items
* Detect if router is dead and show warning or exit
* Wrapper of some kind for in-place upgrades
* Download file sequentially
* Unsharing of files
* Multiple-selection download, Ctrl-A
* Automatic sharing of new files in shared directories (more like medium item)

View File

@ -34,7 +34,7 @@ class Cli {
Core core
try {
core = new Core(props, home, "0.1.12")
core = new Core(props, home, "0.2.1")
} 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.12")
core = new Core(props, home, "0.2.1")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@ -268,7 +268,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.1.12")
Core core = new Core(props, home, "0.2.1")
core.startServices()
// ... at the end, sleep or execute script

View File

@ -13,6 +13,7 @@ class MuWireSettings {
String sharedFiles
CrawlerResponse crawlerResponse
boolean shareDownloadedFiles
boolean watchSharedDirectories
MuWireSettings() {
this(new Properties())
@ -29,6 +30,7 @@ class MuWireSettings {
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
watchSharedDirectories = Boolean.parseBoolean(props.getProperty("watchSharedDirectories","true"))
}
void write(OutputStream out) throws IOException {
@ -41,6 +43,7 @@ class MuWireSettings {
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("watchSharedDirectories", String.valueOf(watchSharedDirectories))
if (sharedFiles != null)
props.setProperty("sharedFiles", sharedFiles)
props.store(out, "")

View File

@ -1,6 +1,7 @@
package com.muwire.core.connection
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import groovy.util.logging.Log
import net.i2p.data.Destination
@ -24,7 +25,7 @@ class Endpoint implements Closeable {
@Override
public void close() {
if (!closed.compareAndSet(false, true)) {
log.warning("Close loop detected for ${destination.toBase32()}", new Exception())
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
return
}
if (inputStream != null) {

View File

@ -2,6 +2,7 @@ package com.muwire.core.connection
import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.StandardCharsets
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
@ -66,7 +67,7 @@ class PeerConnection extends Connection {
protected void write(Object message) {
byte[] payload
if (message instanceof Map) {
payload = JsonOutput.toJson(message).bytes
payload = JsonOutput.toJson(message).getBytes(StandardCharsets.UTF_8)
DataUtil.packHeader(payload.length, writeHeader)
log.fine "$name writing message type ${message.type} length $payload.length"
writeHeader[0] &= (byte)0x7F

View File

@ -0,0 +1,4 @@
package com.muwire.core.files
class DirectoryWatcher {
}

View File

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

View File

@ -39,6 +39,9 @@ class MainFrameController {
cardsPanel.getLayout().show(cardsPanel, "search window")
def search = builder.getVariable("search-field").text
search = search.trim()
if (search.length() == 0)
return
def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
params["search-terms"] = search
@ -46,9 +49,20 @@ class MainFrameController {
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
boolean hashSearch = false
byte [] root = null
if (search.length() == 44 && search.indexOf(" ") < 0) {
try {
root = Base64.decode(search)
hashSearch = true
} catch (Exception e) {
// not a hash search
}
}
def searchEvent
if (model.hashSearch) {
searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid)
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid)
} else {
// this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
@ -86,16 +100,17 @@ class MainFrameController {
return
def sortEvt = group.view.lastSortEvent
if (sortEvt != null) {
row = sortEvt.convertPreviousRowIndexToModel(row)
}
row = group.view.resultsTable.rowSorter.convertRowIndexToModel(row)
}
group.model.results[row]
}
private int selectedDownload() {
def selected = builder.getVariable("downloads-table").getSelectedRow()
def downloadsTable = builder.getVariable("downloads-table")
def selected = downloadsTable.getSelectedRow()
def sortEvt = mvcGroup.view.lastDownloadSortEvent
if (sortEvt != null)
selected = sortEvt.convertPreviousRowIndexToModel(selected)
selected = downloadsTable.rowSorter.convertRowIndexToModel(selected)
selected
}
@ -171,16 +186,6 @@ class MainFrameController {
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
}
@ControllerAction
void keywordSearch() {
model.hashSearch = false
}
@ControllerAction
void hashSearch() {
model.hashSearch = true
}
void unshareSelectedFiles() {
println "unsharing selected files"
}

View File

@ -93,6 +93,10 @@ class OptionsController {
model.excludeLocalResult = excludeLocalResult
uiSettings.excludeLocalResult = excludeLocalResult
boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected()
model.showSearchHashes = showSearchHashes
uiSettings.showSearchHashes = showSearchHashes
File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream {
uiSettings.write(it)

View File

@ -34,6 +34,7 @@ import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.transform.FXObservable
import griffon.transform.Observable
import net.i2p.data.Base64
import net.i2p.data.Destination
import griffon.metadata.ArtifactProviderFor
@ -56,8 +57,6 @@ class MainFrameModel {
def trusted = []
def distrusted = []
boolean hashSearch
@Observable int connections
@Observable String me
@Observable boolean searchButtonsEnabled
@ -69,6 +68,8 @@ class MainFrameModel {
volatile Core core
private long lastRetryTime = System.currentTimeMillis()
UISettings uiSettings
void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName)
@ -79,7 +80,7 @@ class MainFrameModel {
void mvcGroupInit(Map<String, Object> args) {
UISettings uiSettings = application.context.get("ui-settings")
uiSettings = application.context.get("ui-settings")
Timer timer = new Timer("download-pumper", true)
timer.schedule({
@ -177,7 +178,8 @@ class MainFrameModel {
topPanel.getLayout().show(topPanel, "top-search-panel")
}
connectionList.add(e.endpoint.destination)
UIConnection con = new UIConnection(destination : e.endpoint.destination, incoming : e.incoming)
connectionList.add(con)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
@ -192,7 +194,8 @@ class MainFrameModel {
topPanel.getLayout().show(topPanel, "top-connect-panel")
}
connectionList.remove(e.destination)
UIConnection con = new UIConnection(destination : e.destination)
connectionList.remove(con)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
@ -258,14 +261,23 @@ class MainFrameModel {
void onQueryEvent(QueryEvent e) {
if (e.replyTo == core.me.destination)
return
StringBuilder sb = new StringBuilder()
e.searchEvent.searchTerms?.each {
sb.append(it)
sb.append(" ")
def search
if (e.searchEvent.searchHash != null) {
if (!uiSettings.showSearchHashes) {
return
}
search = Base64.encode(e.searchEvent.searchHash)
} else {
StringBuilder sb = new StringBuilder()
e.searchEvent.searchTerms?.each {
sb.append(it)
sb.append(" ")
}
search = sb.toString()
if (search.trim().size() == 0)
return
}
def search = sb.toString()
if (search.trim().size() == 0)
return
runInsideUIAsync {
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
while(searches.size() > 200)
@ -303,4 +315,22 @@ class MainFrameModel {
table.model.fireTableDataChanged()
}
}
private static class UIConnection {
Destination destination
boolean incoming
@Override
public int hashCode() {
destination.hashCode()
}
@Override
public boolean equals(Object o) {
if (!(o instanceof UIConnection))
return false
UIConnection other = (UIConnection) o
return destination == other.destination
}
}
}

View File

@ -27,6 +27,7 @@ class OptionsModel {
@Observable boolean clearCancelledDownloads
@Observable boolean clearFinishedDownloads
@Observable boolean excludeLocalResult
@Observable boolean showSearchHashes
void mvcGroupInit(Map<String, String> args) {
MuWireSettings settings = application.context.get("muwire-settings")
@ -48,5 +49,6 @@ class OptionsModel {
clearCancelledDownloads = uiSettings.clearCancelledDownloads
clearFinishedDownloads = uiSettings.clearFinishedDownloads
excludeLocalResult = uiSettings.excludeLocalResult
showSearchHashes = uiSettings.showSearchHashes
}
}

View File

@ -3,6 +3,7 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import net.i2p.data.DataHelper
import javax.swing.BorderFactory
@ -13,6 +14,7 @@ import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JSplitPane
import javax.swing.JTable
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.border.Border
@ -28,6 +30,8 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.Insets
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.nio.charset.StandardCharsets
@ -43,6 +47,7 @@ class MainFrameView {
def downloadsTable
def lastDownloadSortEvent
def lastSharedSortEvent
void initUI() {
UISettings settings = application.context.get("ui-settings")
@ -85,12 +90,6 @@ class MainFrameView {
}
panel( constraints: BorderLayout.EAST) {
panel {
buttonGroup(id : "searchButtonGroup")
radioButton(text : "Keywords", selected : true, buttonGroup : searchButtonGroup, keywordSearchAction)
radioButton(text : "Hash", selected : false, buttonGroup : searchButtonGroup, hashSearchAction)
}
button(text: "Search", searchAction)
}
}
@ -187,7 +186,13 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "connections-table") {
tableModel(list : model.connectionList) {
closureColumn(header : "Destination", type: String, read : { row -> row.toBase32() })
closureColumn(header : "Destination", preferredWidth: 250, type: String, read : { row -> row.destination.toBase32() })
closureColumn(header : "Direction", preferredWidth: 20, type: String, read : { row ->
if (row.incoming)
return "In"
else
return "Out"
})
}
}
}
@ -299,15 +304,34 @@ class MainFrameView {
downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
downloadsTable.rowSorter.setSortsOnUpdates(true)
downloadsTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showDownloadsMenu(e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showDownloadsMenu(e)
}
})
// shared files table
def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu sharedFilesMenu = new JPopupMenu()
JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFiles()})
sharedFilesMenu.add(unshareSelectedFiles)
// JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
// unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFiles()})
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard(sharedFilesTable)})
sharedFilesMenu.add(copyHashToClipboard)
sharedFilesTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
@ -320,11 +344,53 @@ class MainFrameView {
showPopupMenu(sharedFilesMenu, e)
}
})
// searches table
def searchesTable = builder.getVariable("searches-table")
JPopupMenu searchTableMenu = new JPopupMenu()
JMenuItem copySearchToClipboard = new JMenuItem("Copy search to clipboard")
copySearchToClipboard.addActionListener({mvcGroup.view.copySearchToClipboard(searchesTable)})
searchTableMenu.add(copySearchToClipboard)
searchesTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e)
}
})
}
def showPopupMenu(JPopupMenu menu, MouseEvent event) {
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
menu.show(event.getComponent(), event.getX(), event.getY())
}
def copyHashToClipboard(JTable sharedFilesTable) {
int selected = sharedFilesTable.getSelectedRow()
if (selected < 0)
return
if (lastSharedSortEvent != null)
selected = sharedFilesTable.rowSorter.convertRowIndexToModel(selected)
String root = Base64.encode(model.shared[selected].infoHash.getRoot())
StringSelection selection = new StringSelection(root)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
def copySearchToClipboard(JTable searchesTable) {
int selected = searchesTable.getSelectedRow()
if (selected < 0)
return
String search = model.searches[selected].search
StringSelection selection = new StringSelection(search)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
int selectedDownloaderRow() {
int selected = builder.getVariable("downloads-table").getSelectedRow()
@ -333,6 +399,54 @@ class MainFrameView {
selected
}
def showDownloadsMenu(MouseEvent e) {
int selected = selectedDownloaderRow()
if (selected < 0)
return
boolean cancelEnabled = false
boolean retryEnabled = false
Downloader downloader = model.downloads[selected].downloader
switch(downloader.currentState) {
case Downloader.DownloadState.DOWNLOADING:
case Downloader.DownloadState.HASHLIST:
case Downloader.DownloadState.CONNECTING:
cancelEnabled = true
retryEnabled = false
break
case Downloader.DownloadState.FAILED:
cancelEnabled = true
retryEnabled = true
break
default :
cancelEnabled = false
retryEnabled = false
}
JPopupMenu menu = new JPopupMenu()
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({
String hash = Base64.encode(downloader.infoHash.getRoot())
StringSelection selection = new StringSelection(hash)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
})
menu.add(copyHashToClipboard)
if (cancelEnabled) {
JMenuItem cancel = new JMenuItem("Cancel")
cancel.addActionListener({mvcGroup.controller.cancel()})
menu.add(cancel)
}
if (retryEnabled) {
JMenuItem retry = new JMenuItem("Retry")
retry.addActionListener({mvcGroup.controller.resume()})
menu.add(retry)
}
showPopupMenu(menu, e)
}
def showSearchWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")

View File

@ -43,6 +43,7 @@ class OptionsView {
def clearCancelledDownloadsCheckbox
def clearFinishedDownloadsCheckbox
def excludeLocalResultCheckbox
def showSearchHashesCheckbox
def buttonsPanel
@ -96,6 +97,8 @@ class OptionsView {
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))
label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7))
}
buttonsPanel = builder.panel {
gridBagLayout()

View File

@ -4,10 +4,13 @@ import griffon.core.artifact.GriffonView
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import net.i2p.data.DataHelper
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JTable
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
@ -17,6 +20,8 @@ import com.muwire.core.util.DataUtil
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
@ -99,12 +104,27 @@ class SearchTabView {
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
resultsTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu menu = new JPopupMenu()
JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.parentGroup.controller.download()})
menu.add(download)
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard)
resultsTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e)
else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
mvcGroup.parentGroup.controller.download()
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e)
}
})
}
@ -114,4 +134,21 @@ class SearchTabView {
mvcGroup.parentGroup.model.searchButtonsEnabled = false
mvcGroup.destroy()
}
def showPopupMenu(JPopupMenu menu, MouseEvent e) {
println "showing popup menu"
menu.show(e.getComponent(), e.getX(), e.getY())
}
def copyHashToClipboard() {
int selected = resultsTable.getSelectedRow()
if (selected < 0)
return
if (lastSortEvent != null)
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
String hash = Base64.encode(model.results[selected].infohash.getRoot())
StringSelection selection = new StringSelection(hash)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
}

View File

@ -8,6 +8,7 @@ class UISettings {
boolean clearCancelledDownloads
boolean clearFinishedDownloads
boolean excludeLocalResult
boolean showSearchHashes
UISettings(Properties props) {
lnf = props.getProperty("lnf", "system")
@ -16,6 +17,7 @@ class UISettings {
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false"))
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false"))
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","false"))
}
void write(OutputStream out) throws IOException {
@ -25,6 +27,7 @@ class UISettings {
props.setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
props.setProperty("showSearchHashes", String.valueOf(showSearchHashes))
if (font != null)
props.setProperty("font", font)