Compare commits

...

20 Commits

Author SHA1 Message Date
Zlatin Balevsky
041fc3bef3 Release 0.2.1 2019-06-16 09:37:53 +01:00
Zlatin Balevsky
03c3b1ebf1 fix copying of hash if search results are sorted 2019-06-16 09:30:52 +01:00
Zlatin Balevsky
aece390daa right-click menu on the search results tab 2019-06-16 09:17:17 +01:00
Zlatin Balevsky
cf63be68e8 copy search to clipboard 2019-06-16 08:38:47 +01:00
Zlatin Balevsky
88ece4dc23 add option to show search hashes in monitor 2019-06-16 08:29:03 +01:00
Zlatin Balevsky
13767d58f2 detect if a query is hash, get rid of radio buttons 2019-06-16 08:09:51 +01:00
Zlatin Balevsky
05a1ccd3d8 update todo 2019-06-16 07:31:01 +01:00
Zlatin Balevsky
6807c14a5f add copy hash to clipboard 2019-06-16 07:23:22 +01:00
Zlatin Balevsky
684be0c50e start of work on directory watcher 2019-06-16 07:03:16 +01:00
Zlatin Balevsky
6655c262c6 more todo items 2019-06-16 07:01:50 +01:00
Zlatin Balevsky
b1ccd55030 more todo items 2019-06-16 06:26:03 +01:00
Zlatin Balevsky
a3becd0f7e update TODO 2019-06-16 06:19:28 +01:00
Zlatin Balevsky
af2f3e0ebf in/out direction done 2019-06-16 05:56:56 +01:00
Zlatin Balevsky
e2b7ffa1db direction in monitor tab 2019-06-16 05:52:23 +01:00
Zlatin Balevsky
0e0176acfc add web UI to TODO list 2019-06-16 05:35:05 +01:00
Zlatin Balevsky
7f09bb079c Beginnings of a TODO list 2019-06-16 05:28:42 +01:00
Zlatin Balevsky
77e48b01bb Release 0.2.0 2019-06-15 21:10:11 +01:00
Zlatin Balevsky
12db6857c1 disable unshare files popup until implemented 2019-06-15 12:12:08 +01:00
Zlatin Balevsky
acd67733a5 sort the downloads table on updates 2019-06-15 12:08:29 +01:00
Zlatin Balevsky
8d3ce7aa8e use the same sorted row selection logic in downloads table 2019-06-15 09:57:12 +01:00
15 changed files with 224 additions and 41 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 Core core
try { try {
core = new Core(props, home, "0.1.13") core = new Core(props, home, "0.2.1")
} catch (Exception bad) { } catch (Exception bad) {
bad.printStackTrace(System.out) bad.printStackTrace(System.out)
println "Failed to initialize core, exiting" println "Failed to initialize core, exiting"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,9 +49,20 @@ class MainFrameController {
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params) def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group 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 def searchEvent
if (model.hashSearch) { if (hashSearch) {
searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid) searchEvent = new SearchEvent(searchHash : root, uuid : uuid)
} else { } else {
// this can be improved a lot // this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ") def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
@@ -95,10 +106,11 @@ class MainFrameController {
} }
private int selectedDownload() { 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 def sortEvt = mvcGroup.view.lastDownloadSortEvent
if (sortEvt != null) if (sortEvt != null)
selected = sortEvt.convertPreviousRowIndexToModel(selected) selected = downloadsTable.rowSorter.convertRowIndexToModel(selected)
selected selected
} }
@@ -174,16 +186,6 @@ class MainFrameController {
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted) markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
} }
@ControllerAction
void keywordSearch() {
model.hashSearch = false
}
@ControllerAction
void hashSearch() {
model.hashSearch = true
}
void unshareSelectedFiles() { void unshareSelectedFiles() {
println "unsharing selected files" println "unsharing selected files"
} }

View File

@@ -93,6 +93,10 @@ class OptionsController {
model.excludeLocalResult = excludeLocalResult model.excludeLocalResult = excludeLocalResult
uiSettings.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") File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream { uiSettingsFile.withOutputStream {
uiSettings.write(it) uiSettings.write(it)

View File

@@ -34,6 +34,7 @@ import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.transform.FXObservable import griffon.transform.FXObservable
import griffon.transform.Observable import griffon.transform.Observable
import net.i2p.data.Base64
import net.i2p.data.Destination import net.i2p.data.Destination
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
@@ -56,8 +57,6 @@ class MainFrameModel {
def trusted = [] def trusted = []
def distrusted = [] def distrusted = []
boolean hashSearch
@Observable int connections @Observable int connections
@Observable String me @Observable String me
@Observable boolean searchButtonsEnabled @Observable boolean searchButtonsEnabled
@@ -69,6 +68,8 @@ class MainFrameModel {
volatile Core core volatile Core core
private long lastRetryTime = System.currentTimeMillis() private long lastRetryTime = System.currentTimeMillis()
UISettings uiSettings
void updateTablePreservingSelection(String tableName) { void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName) def downloadTable = builder.getVariable(tableName)
@@ -79,7 +80,7 @@ class MainFrameModel {
void mvcGroupInit(Map<String, Object> args) { 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 timer = new Timer("download-pumper", true)
timer.schedule({ timer.schedule({
@@ -177,7 +178,8 @@ class MainFrameModel {
topPanel.getLayout().show(topPanel, "top-search-panel") 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") JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
} }
@@ -192,7 +194,8 @@ class MainFrameModel {
topPanel.getLayout().show(topPanel, "top-connect-panel") 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") JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
} }
@@ -258,14 +261,23 @@ class MainFrameModel {
void onQueryEvent(QueryEvent e) { void onQueryEvent(QueryEvent e) {
if (e.replyTo == core.me.destination) if (e.replyTo == core.me.destination)
return return
StringBuilder sb = new StringBuilder()
e.searchEvent.searchTerms?.each { def search
sb.append(it) if (e.searchEvent.searchHash != null) {
sb.append(" ") 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 { runInsideUIAsync {
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator)) searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
while(searches.size() > 200) while(searches.size() > 200)
@@ -303,4 +315,22 @@ class MainFrameModel {
table.model.fireTableDataChanged() 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 clearCancelledDownloads
@Observable boolean clearFinishedDownloads @Observable boolean clearFinishedDownloads
@Observable boolean excludeLocalResult @Observable boolean excludeLocalResult
@Observable boolean showSearchHashes
void mvcGroupInit(Map<String, String> args) { void mvcGroupInit(Map<String, String> args) {
MuWireSettings settings = application.context.get("muwire-settings") MuWireSettings settings = application.context.get("muwire-settings")
@@ -48,5 +49,6 @@ class OptionsModel {
clearCancelledDownloads = uiSettings.clearCancelledDownloads clearCancelledDownloads = uiSettings.clearCancelledDownloads
clearFinishedDownloads = uiSettings.clearFinishedDownloads clearFinishedDownloads = uiSettings.clearFinishedDownloads
excludeLocalResult = uiSettings.excludeLocalResult excludeLocalResult = uiSettings.excludeLocalResult
showSearchHashes = uiSettings.showSearchHashes
} }
} }

View File

@@ -3,6 +3,7 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import net.i2p.data.DataHelper import net.i2p.data.DataHelper
import javax.swing.BorderFactory import javax.swing.BorderFactory
@@ -13,6 +14,7 @@ import javax.swing.JLabel
import javax.swing.JMenuItem import javax.swing.JMenuItem
import javax.swing.JPopupMenu import javax.swing.JPopupMenu
import javax.swing.JSplitPane import javax.swing.JSplitPane
import javax.swing.JTable
import javax.swing.ListSelectionModel import javax.swing.ListSelectionModel
import javax.swing.SwingConstants import javax.swing.SwingConstants
import javax.swing.border.Border import javax.swing.border.Border
@@ -28,6 +30,8 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints import java.awt.GridBagConstraints
import java.awt.GridBagLayout import java.awt.GridBagLayout
import java.awt.Insets import java.awt.Insets
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@@ -43,6 +47,7 @@ class MainFrameView {
def downloadsTable def downloadsTable
def lastDownloadSortEvent def lastDownloadSortEvent
def lastSharedSortEvent
void initUI() { void initUI() {
UISettings settings = application.context.get("ui-settings") UISettings settings = application.context.get("ui-settings")
@@ -85,12 +90,6 @@ class MainFrameView {
} }
panel( constraints: BorderLayout.EAST) { 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) button(text: "Search", searchAction)
} }
} }
@@ -187,7 +186,13 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) { scrollPane(constraints : BorderLayout.CENTER) {
table(id : "connections-table") { table(id : "connections-table") {
tableModel(list : model.connectionList) { 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,21 @@ class MainFrameView {
downloadsTable.setDefaultRenderer(Integer.class, centerRenderer) downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt}) downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
downloadsTable.rowSorter.setSortsOnUpdates(true)
// shared files table // shared files table
def sharedFilesTable = builder.getVariable("shared-files-table") def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer()) sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu sharedFilesMenu = new JPopupMenu() JPopupMenu sharedFilesMenu = new JPopupMenu()
JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files") // JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFiles()}) // unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFiles()})
sharedFilesMenu.add(unshareSelectedFiles) JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard(sharedFilesTable)})
sharedFilesMenu.add(copyHashToClipboard)
sharedFilesTable.addMouseListener(new MouseAdapter() { sharedFilesTable.addMouseListener(new MouseAdapter() {
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
@@ -320,11 +331,53 @@ class MainFrameView {
showPopupMenu(sharedFilesMenu, e) 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) { def showPopupMenu(JPopupMenu menu, MouseEvent event) {
menu.show(event.getComponent(), event.getX(), event.getY()) 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 selectedDownloaderRow() {
int selected = builder.getVariable("downloads-table").getSelectedRow() int selected = builder.getVariable("downloads-table").getSelectedRow()

View File

@@ -43,6 +43,7 @@ class OptionsView {
def clearCancelledDownloadsCheckbox def clearCancelledDownloadsCheckbox
def clearFinishedDownloadsCheckbox def clearFinishedDownloadsCheckbox
def excludeLocalResultCheckbox def excludeLocalResultCheckbox
def showSearchHashesCheckbox
def buttonsPanel def buttonsPanel
@@ -96,6 +97,8 @@ class OptionsView {
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, 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)) 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)) 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 { buttonsPanel = builder.panel {
gridBagLayout() gridBagLayout()

View File

@@ -4,10 +4,13 @@ import griffon.core.artifact.GriffonView
import griffon.core.mvc.MVCGroup import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import net.i2p.data.DataHelper import net.i2p.data.DataHelper
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JLabel import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JTable import javax.swing.JTable
import javax.swing.ListSelectionModel import javax.swing.ListSelectionModel
import javax.swing.SwingConstants import javax.swing.SwingConstants
@@ -17,6 +20,8 @@ import com.muwire.core.util.DataUtil
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Color import java.awt.Color
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
@@ -99,12 +104,27 @@ class SearchTabView {
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt}) resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
resultsTable.rowSorter.setSortsOnUpdates(true) 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() { resultsTable.addMouseListener(new MouseAdapter() {
@Override @Override
public void mouseClicked(MouseEvent e) { 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() 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.parentGroup.model.searchButtonsEnabled = false
mvcGroup.destroy() 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 clearCancelledDownloads
boolean clearFinishedDownloads boolean clearFinishedDownloads
boolean excludeLocalResult boolean excludeLocalResult
boolean showSearchHashes
UISettings(Properties props) { UISettings(Properties props) {
lnf = props.getProperty("lnf", "system") lnf = props.getProperty("lnf", "system")
@@ -16,6 +17,7 @@ class UISettings {
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false")) clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false"))
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false")) clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false")) excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false"))
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","false"))
} }
void write(OutputStream out) throws IOException { void write(OutputStream out) throws IOException {
@@ -25,6 +27,7 @@ class UISettings {
props.setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads)) props.setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads)) props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult)) props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
props.setProperty("showSearchHashes", String.valueOf(showSearchHashes))
if (font != null) if (font != null)
props.setProperty("font", font) props.setProperty("font", font)