Compare commits

..

19 Commits

Author SHA1 Message Date
Zlatin Balevsky
2b04374e23 add option to disable browsing of files, make the dialog bigger 2019-10-19 00:53:13 +01:00
Zlatin Balevsky
383addbc37 implement view comment from browse window 2019-10-19 00:30:03 +01:00
Zlatin Balevsky
cc39cd7f8e implement downloading from browse window 2019-10-19 00:23:43 +01:00
Zlatin Balevsky
83665d7524 wip on browse host 2019-10-18 23:55:07 +01:00
Zlatin Balevsky
94340480b4 wip on browse host 2019-10-18 23:25:26 +01:00
Zlatin Balevsky
8850d49c63 wip on browse host 2019-10-18 23:16:37 +01:00
Zlatin Balevsky
f0f9d840f0 wip on browse host 2019-10-18 22:35:17 +01:00
Zlatin Balevsky
7f4cd4f331 wip on browse host 2019-10-18 21:17:34 +01:00
Zlatin Balevsky
e6162503f6 wip on browse host 2019-10-18 20:29:39 +01:00
Zlatin Balevsky
7a5d71dc36 add copy name to clipboard option 2019-10-17 19:01:53 +01:00
Zlatin Balevsky
6fa39a5e35 turn off logging if there is no config file 2019-10-17 18:39:28 +01:00
Zlatin Balevsky
c5ae804f61 Implement automatic font sizing; set all font properties on change of font 2019-10-17 18:15:04 +01:00
Zlatin Balevsky
d7695b448d remove my DS_Store 2019-10-17 05:50:29 +01:00
Zlatin Balevsky
946d9c8f32 disable sharing of hidden files by default, add option to enable 2019-10-17 05:46:27 +01:00
Zlatin Balevsky
02441ca1e3 add option to disable searching in comments 2019-10-16 19:57:18 +01:00
Zlatin Balevsky
5fa21b2360 keep tree expanded on modifications 2019-10-16 14:42:40 +01:00
Zlatin Balevsky
d4c08f4fe6 only remove from index if no more files have the same comment pt.2 2019-10-16 14:23:12 +01:00
Zlatin Balevsky
942de287c6 only remove from index if no more files have the same comment 2019-10-16 14:21:50 +01:00
Zlatin Balevsky
d0299f80c6 search through comments 2019-10-16 14:06:11 +01:00
32 changed files with 785 additions and 134 deletions

View File

@@ -28,6 +28,7 @@ import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.HasherService import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService import com.muwire.core.files.PersisterService
import com.muwire.core.files.UICommentEvent
import com.muwire.core.files.UIPersistFilesEvent import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.files.AllFilesLoadedEvent import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent import com.muwire.core.files.DirectoryUnsharedEvent
@@ -36,11 +37,13 @@ import com.muwire.core.hostcache.CacheClient
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.hostcache.HostDiscoveredEvent import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.mesh.MeshManager import com.muwire.core.mesh.MeshManager
import com.muwire.core.search.BrowseManager
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.ResultsSender import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchManager import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIBrowseEvent
import com.muwire.core.search.UIResultBatchEvent import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
@@ -216,6 +219,7 @@ public class Core {
eventBus.register(FileUnsharedEvent.class, fileManager) eventBus.register(FileUnsharedEvent.class, fileManager)
eventBus.register(SearchEvent.class, fileManager) eventBus.register(SearchEvent.class, fileManager)
eventBus.register(DirectoryUnsharedEvent.class, fileManager) eventBus.register(DirectoryUnsharedEvent.class, fileManager)
eventBus.register(UICommentEvent.class, fileManager)
log.info("initializing mesh manager") log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager, home, props) MeshManager meshManager = new MeshManager(fileManager, home, props)
@@ -253,7 +257,7 @@ public class Core {
I2PConnector i2pConnector = new I2PConnector(socketManager) I2PConnector i2pConnector = new I2PConnector(socketManager)
log.info "initializing results sender" log.info "initializing results sender"
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me) ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props)
log.info "initializing search manager" log.info "initializing search manager"
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender) SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
@@ -279,7 +283,7 @@ public class Core {
log.info("initializing acceptor") log.info("initializing acceptor")
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager) I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props, connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher) i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher)
log.info("initializing directory watcher") log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props) directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
@@ -288,7 +292,7 @@ public class Core {
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher) eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
log.info("initializing hasher service") log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus, fileManager) hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
eventBus.register(FileSharedEvent.class, hasherService) eventBus.register(FileSharedEvent.class, hasherService)
eventBus.register(FileUnsharedEvent.class, hasherService) eventBus.register(FileUnsharedEvent.class, hasherService)
eventBus.register(DirectoryUnsharedEvent.class, hasherService) eventBus.register(DirectoryUnsharedEvent.class, hasherService)
@@ -302,6 +306,11 @@ public class Core {
contentManager = new ContentManager() contentManager = new ContentManager()
eventBus.register(ContentControlEvent.class, contentManager) eventBus.register(ContentControlEvent.class, contentManager)
eventBus.register(QueryEvent.class, contentManager) eventBus.register(QueryEvent.class, contentManager)
log.info("initializing browse manager")
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus)
eventBus.register(UIBrowseEvent.class, browseManager)
} }
public void startServices() { public void startServices() {

View File

@@ -24,6 +24,9 @@ class MuWireSettings {
File downloadLocation File downloadLocation
CrawlerResponse crawlerResponse CrawlerResponse crawlerResponse
boolean shareDownloadedFiles boolean shareDownloadedFiles
boolean shareHiddenFiles
boolean searchComments
boolean browseFiles
Set<String> watchedDirectories Set<String> watchedDirectories
float downloadSequentialRatio float downloadSequentialRatio
int hostClearInterval, hostHopelessInterval, hostRejectInterval int hostClearInterval, hostHopelessInterval, hostRejectInterval
@@ -52,6 +55,7 @@ class MuWireSettings {
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true")) autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
updateType = props.getProperty("updateType","jar") updateType = props.getProperty("updateType","jar")
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true")) shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false"))
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8")) downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15")) hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440")) hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
@@ -60,6 +64,8 @@ class MuWireSettings {
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false")) embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
inBw = Integer.valueOf(props.getProperty("inBw","256")) inBw = Integer.valueOf(props.getProperty("inBw","256"))
outBw = Integer.valueOf(props.getProperty("outBw","128")) outBw = Integer.valueOf(props.getProperty("outBw","128"))
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
watchedDirectories = readEncodedSet(props, "watchedDirectories") watchedDirectories = readEncodedSet(props, "watchedDirectories")
watchedKeywords = readEncodedSet(props, "watchedKeywords") watchedKeywords = readEncodedSet(props, "watchedKeywords")
@@ -90,6 +96,7 @@ class MuWireSettings {
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate)) props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
props.setProperty("updateType",String.valueOf(updateType)) props.setProperty("updateType",String.valueOf(updateType))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles)) props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("shareHiddenFiles", String.valueOf(shareHiddenFiles))
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio)) props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval)) props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval)) props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
@@ -98,6 +105,8 @@ class MuWireSettings {
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter)) props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("inBw", String.valueOf(inBw)) props.setProperty("inBw", String.valueOf(inBw))
props.setProperty("outBw", String.valueOf(outBw)) props.setProperty("outBw", String.valueOf(outBw))
props.setProperty("searchComments", String.valueOf(searchComments))
props.setProperty("browseFiles", String.valueOf(browseFiles))
writeEncodedSet(watchedDirectories, "watchedDirectories", props) writeEncodedSet(watchedDirectories, "watchedDirectories", props)
writeEncodedSet(watchedKeywords, "watchedKeywords", props) writeEncodedSet(watchedKeywords, "watchedKeywords", props)

View File

@@ -132,6 +132,7 @@ abstract class Connection implements Closeable {
query.firstHop = e.firstHop query.firstHop = e.firstHop
query.keywords = e.searchEvent.getSearchTerms() query.keywords = e.searchEvent.getSearchTerms()
query.oobInfohash = e.searchEvent.oobInfohash query.oobInfohash = e.searchEvent.oobInfohash
query.searchComments = e.searchEvent.searchComments
if (e.searchEvent.searchHash != null) if (e.searchEvent.searchHash != null)
query.infohash = Base64.encode(e.searchEvent.searchHash) query.infohash = Base64.encode(e.searchEvent.searchHash)
query.replyTo = e.replyTo.toBase64() query.replyTo = e.replyTo.toBase64()
@@ -209,11 +210,15 @@ abstract class Connection implements Closeable {
boolean oob = false boolean oob = false
if (search.oobInfohash != null) if (search.oobInfohash != null)
oob = search.oobInfohash oob = search.oobInfohash
boolean searchComments = false
if (search.searchComments != null)
searchComments = search.searchComments
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords, SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash, searchHash : infohash,
uuid : uuid, uuid : uuid,
oobInfohash : oob) oobInfohash : oob,
searchComments : searchComments)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent, QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo, replyTo : replyTo,
originator : originator, originator : originator,

View File

@@ -10,6 +10,7 @@ import java.util.zip.InflaterInputStream
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.files.FileManager
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustLevel import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
@@ -17,6 +18,7 @@ import com.muwire.core.upload.UploadManager
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import com.muwire.core.search.InvalidSearchResultException import com.muwire.core.search.InvalidSearchResultException
import com.muwire.core.search.ResultsParser import com.muwire.core.search.ResultsParser
import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchManager import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIResultBatchEvent import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
@@ -37,6 +39,7 @@ class ConnectionAcceptor {
final TrustService trustService final TrustService trustService
final SearchManager searchManager final SearchManager searchManager
final UploadManager uploadManager final UploadManager uploadManager
final FileManager fileManager
final ConnectionEstablisher establisher final ConnectionEstablisher establisher
final ExecutorService acceptorThread final ExecutorService acceptorThread
@@ -47,7 +50,7 @@ class ConnectionAcceptor {
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager, ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache, MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
TrustService trustService, SearchManager searchManager, UploadManager uploadManager, TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
ConnectionEstablisher establisher) { FileManager fileManager, ConnectionEstablisher establisher) {
this.eventBus = eventBus this.eventBus = eventBus
this.manager = manager this.manager = manager
this.settings = settings this.settings = settings
@@ -55,6 +58,7 @@ class ConnectionAcceptor {
this.hostCache = hostCache this.hostCache = hostCache
this.trustService = trustService this.trustService = trustService
this.searchManager = searchManager this.searchManager = searchManager
this.fileManager = fileManager
this.uploadManager = uploadManager this.uploadManager = uploadManager
this.establisher = establisher this.establisher = establisher
@@ -129,6 +133,9 @@ class ConnectionAcceptor {
case (byte)'T': case (byte)'T':
processTRUST(e) processTRUST(e)
break break
case (byte)'B':
processBROWSE(e)
break
default: default:
throw new Exception("Invalid read $read") throw new Exception("Invalid read $read")
} }
@@ -247,7 +254,47 @@ class ConnectionAcceptor {
} }
} }
private void processBROWSE(Endpoint e) {
try {
byte [] rowse = new byte[7]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(rowse)
if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid BROWSE connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
OutputStream os = e.getOutputStream()
if (!settings.browseFiles) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
e.close()
return
}
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
def sharedFiles = fileManager.getSharedFiles().values()
os.write("Count: ${sharedFiles.size()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(os)
JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each {
def obj = ResultsSender.sharedFileToObj(it, false)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
}
dos.flush()
} finally {
e.close()
}
}
private void processTRUST(Endpoint e) { private void processTRUST(Endpoint e) {
try {
byte[] RUST = new byte[6] byte[] RUST = new byte[6]
DataInputStream dis = new DataInputStream(e.getInputStream()) DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(RUST) dis.readFully(RUST)
@@ -283,7 +330,9 @@ class ConnectionAcceptor {
} }
dos.flush() dos.flush()
} finally {
e.close() e.close()
} }
}
} }

View File

@@ -8,8 +8,10 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchIndex import com.muwire.core.search.SearchIndex
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64
@Log @Log
class FileManager { class FileManager {
@@ -20,6 +22,7 @@ class FileManager {
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>()) final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>()) final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
final Map<String, Set<File>> nameToFiles = new HashMap<>() final Map<String, Set<File>> nameToFiles = new HashMap<>()
final Map<String, Set<File>> commentToFile = new HashMap<>()
final SearchIndex index = new SearchIndex() final SearchIndex index = new SearchIndex()
FileManager(EventBus eventBus, MuWireSettings settings) { FileManager(EventBus eventBus, MuWireSettings settings) {
@@ -62,6 +65,18 @@ class FileManager {
} }
existingFiles.add(sf.getFile()) existingFiles.add(sf.getFile())
String comment = sf.getComment()
if (comment != null) {
comment = DataUtil.readi18nString(Base64.decode(comment))
index.add(comment)
Set<File> existingComment = commentToFile.get(comment)
if(existingComment == null) {
existingComment = new HashSet<>()
commentToFile.put(comment, existingComment)
}
existingComment.add(sf.getFile())
}
index.add(name) index.add(name)
} }
@@ -87,9 +102,45 @@ class FileManager {
} }
} }
String comment = sf.getComment()
if (comment != null) {
Set<File> existingComment = commentToFile.get(comment)
if (existingComment != null) {
existingComment.remove(sf.getFile())
if (existingComment.isEmpty()) {
commentToFile.remove(comment)
index.remove(comment)
}
}
}
index.remove(name) index.remove(name)
} }
void onUICommentEvent(UICommentEvent e) {
if (e.oldComment != null) {
def comment = DataUtil.readi18nString(Base64.decode(e.oldComment))
Set<File> existingFiles = commentToFile.get(comment)
existingFiles.remove(e.sharedFile.getFile())
if (existingFiles.isEmpty()) {
commentToFile.remove(comment)
index.remove(comment)
}
}
String comment = e.sharedFile.getComment()
comment = DataUtil.readi18nString(Base64.decode(comment))
if (comment != null) {
index.add(comment)
Set<File> existingComment = commentToFile.get(comment)
if(existingComment == null) {
existingComment = new HashSet<>()
commentToFile.put(comment, existingComment)
}
existingComment.add(e.sharedFile.getFile())
}
}
Map<File, SharedFile> getSharedFiles() { Map<File, SharedFile> getSharedFiles() {
synchronized(fileToSharedFile) { synchronized(fileToSharedFile) {
return new HashMap<>(fileToSharedFile) return new HashMap<>(fileToSharedFile)
@@ -112,10 +163,15 @@ class FileManager {
} else { } else {
def names = index.search e.searchTerms def names = index.search e.searchTerms
Set<File> files = new HashSet<>() Set<File> files = new HashSet<>()
names.each { files.addAll nameToFiles.getOrDefault(it, []) } names.each {
files.addAll nameToFiles.getOrDefault(it, [])
if (e.searchComments)
files.addAll commentToFile.getOrDefault(it, [])
}
Set<SharedFile> sharedFiles = new HashSet<>() Set<SharedFile> sharedFiles = new HashSet<>()
files.each { sharedFiles.add fileToSharedFile[it] } files.each { sharedFiles.add fileToSharedFile[it] }
files = filter(sharedFiles, e.oobInfohash) files = filter(sharedFiles, e.oobInfohash)
if (!sharedFiles.isEmpty()) if (!sharedFiles.isEmpty())
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e) re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)

View File

@@ -4,6 +4,7 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
class HasherService { class HasherService {
@@ -12,12 +13,14 @@ class HasherService {
final EventBus eventBus final EventBus eventBus
final FileManager fileManager final FileManager fileManager
final Set<File> hashed = new HashSet<>() final Set<File> hashed = new HashSet<>()
final MuWireSettings settings
Executor executor Executor executor
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) { HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager, MuWireSettings settings) {
this.hasher = hasher this.hasher = hasher
this.eventBus = eventBus this.eventBus = eventBus
this.fileManager = fileManager this.fileManager = fileManager
this.settings = settings
} }
void start() { void start() {
@@ -26,6 +29,8 @@ class HasherService {
void onFileSharedEvent(FileSharedEvent evt) { void onFileSharedEvent(FileSharedEvent evt) {
File canonical = evt.file.getCanonicalFile() File canonical = evt.file.getCanonicalFile()
if (!settings.shareHiddenFiles && canonical.isHidden())
return
if (fileManager.fileToSharedFile.containsKey(canonical)) if (fileManager.fileToSharedFile.containsKey(canonical))
return return
if (hashed.add(canonical)) if (hashed.add(canonical))

View File

@@ -0,0 +1,9 @@
package com.muwire.core.files
import com.muwire.core.Event
import com.muwire.core.SharedFile
class UICommentEvent extends Event {
SharedFile sharedFile
String oldComment
}

View File

@@ -0,0 +1,86 @@
package com.muwire.core.search
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.logging.Level
@Log
class BrowseManager {
private final I2PConnector connector
private final EventBus eventBus
private final Executor browserThread = Executors.newSingleThreadExecutor()
BrowseManager(I2PConnector connector, EventBus eventBus) {
this.connector = connector
this.eventBus = eventBus
}
void onUIBrowseEvent(UIBrowseEvent e) {
browserThread.execute({
Endpoint endpoint = null
try {
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
endpoint = connector.connect(e.host.destination)
OutputStream os = endpoint.getOutputStream()
os.write("BROWSE\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
InputStream is = endpoint.getInputStream()
String code = DataUtil.readTillRN(is)
if (!code.startsWith("200"))
throw new IOException("Invalid code")
// parse all headers
Map<String,String> headers = new HashMap<>()
String header
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
int colon = header.indexOf(':')
if (colon == -1 || colon == header.length() - 1)
throw new IOException("invalid header $header")
String key = header.substring(0, colon)
String value = header.substring(colon + 1)
headers[key] = value.trim()
}
if (!headers.containsKey("Count"))
throw new IOException("No count header")
int results = Integer.parseInt(headers['Count'])
// at this stage, start pulling the results
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FETCHING))
JsonSlurper slurper = new JsonSlurper()
DataInputStream dis = new DataInputStream(is)
UUID uuid = UUID.randomUUID()
for (int i = 0; i < results; i++) {
int size = dis.readUnsignedShort()
byte [] tmp = new byte[size]
dis.readFully(tmp)
def json = slurper.parse(tmp)
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
eventBus.publish(result)
}
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FINISHED))
} catch (Exception bad) {
log.log(Level.WARNING, "browse failed", bad)
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FAILED))
} finally {
endpoint?.close()
}
} as Runnable)
}
}

View File

@@ -0,0 +1,5 @@
package com.muwire.core.search;
public enum BrowseStatus {
CONNECTING, FETCHING, FINISHED, FAILED
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.search
import com.muwire.core.Event
class BrowseStatusEvent extends Event {
BrowseStatus status
}

View File

@@ -95,6 +95,10 @@ class ResultsParser {
if (json.comment != null) if (json.comment != null)
comment = DataUtil.readi18nString(Base64.decode(json.comment)) comment = DataUtil.readi18nString(Base64.decode(json.comment))
boolean browse = false
if (json.browse != null)
browse = true
return new UIResultEvent( sender : p, return new UIResultEvent( sender : p,
name : name, name : name,
size : size, size : size,
@@ -102,6 +106,7 @@ class ResultsParser {
pieceSize : pieceSize, pieceSize : pieceSize,
sources : sources, sources : sources,
comment : comment, comment : comment,
browse : browse,
uuid: uuid) uuid: uuid)
} catch (Exception e) { } catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e) throw new InvalidSearchResultException("parsing search result failed",e)

View File

@@ -18,6 +18,7 @@ import java.util.stream.Collectors
import com.muwire.core.DownloadedFile import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import groovy.json.JsonOutput import groovy.json.JsonOutput
import groovy.util.logging.Log import groovy.util.logging.Log
@@ -43,11 +44,13 @@ class ResultsSender {
private final I2PConnector connector private final I2PConnector connector
private final Persona me private final Persona me
private final EventBus eventBus private final EventBus eventBus
private final MuWireSettings settings
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me) { ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings) {
this.connector = connector; this.connector = connector;
this.eventBus = eventBus this.eventBus = eventBus
this.me = me this.me = me
this.settings = settings
} }
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) { void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) {
@@ -91,7 +94,6 @@ class ResultsSender {
@Override @Override
public void run() { public void run() {
try { try {
byte [] tmp = new byte[InfoHash.SIZE]
JsonOutput jsonOutput = new JsonOutput() JsonOutput jsonOutput = new JsonOutput()
Endpoint endpoint = null; Endpoint endpoint = null;
try { try {
@@ -101,36 +103,7 @@ class ResultsSender {
me.write(os) me.write(os)
os.writeShort((short)results.length) os.writeShort((short)results.length)
results.each { results.each {
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8) def obj = sharedFileToObj(it, settings.browseFiles)
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.writeShort((short) name.length)
daos.write(name)
daos.flush()
String encodedName = Base64.encode(baos.toByteArray())
def obj = [:]
obj.type = "Result"
obj.version = oobInfohash ? 2 : 1
obj.name = encodedName
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
obj.size = it.getFile().length()
obj.pieceSize = it.getPieceSize()
if (!oobInfohash) {
byte [] hashList = it.getInfoHash().getHashList()
def hashListB64 = []
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
hashListB64 << Base64.encode(tmp)
}
obj.hashList = hashListB64
}
if (it instanceof DownloadedFile)
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
if (it.getComment() != null)
obj.comment = it.getComment()
def json = jsonOutput.toJson(obj) def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length()) os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII)) os.write(json.getBytes(StandardCharsets.US_ASCII))
@@ -144,4 +117,30 @@ class ResultsSender {
} }
} }
} }
public static def sharedFileToObj(SharedFile sf, boolean browseFiles) {
byte [] name = sf.getFile().getName().getBytes(StandardCharsets.UTF_8)
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.writeShort((short) name.length)
daos.write(name)
daos.flush()
String encodedName = Base64.encode(baos.toByteArray())
def obj = [:]
obj.type = "Result"
obj.version = 2
obj.name = encodedName
obj.infohash = Base64.encode(sf.getInfoHash().getRoot())
obj.size = sf.getCachedLength()
obj.pieceSize = sf.getPieceSize()
if (sf instanceof DownloadedFile)
obj.sources = sf.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
if (sf.getComment() != null)
obj.comment = sf.getComment()
obj.browse = browseFiles
obj
}
} }

View File

@@ -9,11 +9,12 @@ class SearchEvent extends Event {
byte [] searchHash byte [] searchHash
UUID uuid UUID uuid
boolean oobInfohash boolean oobInfohash
boolean searchComments
String toString() { String toString() {
def infoHash = null def infoHash = null
if (searchHash != null) if (searchHash != null)
infoHash = new InfoHash(searchHash) infoHash = new InfoHash(searchHash)
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash" "searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash searchComments:$searchComments"
} }
} }

View File

@@ -0,0 +1,8 @@
package com.muwire.core.search
import com.muwire.core.Event
import com.muwire.core.Persona
class UIBrowseEvent extends Event {
Persona host
}

View File

@@ -15,6 +15,7 @@ class UIResultEvent extends Event {
InfoHash infohash InfoHash infohash
int pieceSize int pieceSize
String comment String comment
boolean browse
@Override @Override
public String toString() { public String toString() {

View File

@@ -25,7 +25,8 @@ class HasherServiceTest {
void before() { void before() {
eventBus = new EventBus() eventBus = new EventBus()
hasher = new FileHasher() hasher = new FileHasher()
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings())) def props = new MuWireSettings()
service = new HasherService(hasher, eventBus, new FileManager(eventBus, props), props)
eventBus.register(FileHashedEvent.class, listener) eventBus.register(FileHashedEvent.class, listener)
eventBus.register(FileSharedEvent.class, service) eventBus.register(FileSharedEvent.class, service)
service.start() service.start()

Binary file not shown.

View File

@@ -56,4 +56,9 @@ mvcGroups {
view = 'com.muwire.gui.AddCommentView' view = 'com.muwire.gui.AddCommentView'
controller = 'com.muwire.gui.AddCommentController' controller = 'com.muwire.gui.AddCommentController'
} }
'browse' {
model = 'com.muwire.gui.BrowseModel'
view = 'com.muwire.gui.BrowseView'
controller = 'com.muwire.gui.BrowseController'
}
} }

View File

@@ -8,6 +8,8 @@ import net.i2p.data.Base64
import javax.annotation.Nonnull import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.files.UICommentEvent
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
@ArtifactProviderFor(GriffonController) @ArtifactProviderFor(GriffonController)
@@ -17,6 +19,8 @@ class AddCommentController {
@MVCMember @Nonnull @MVCMember @Nonnull
AddCommentView view AddCommentView view
Core core
@ControllerAction @ControllerAction
void save() { void save() {
String comment = view.textarea.getText() String comment = view.textarea.getText()
@@ -25,7 +29,9 @@ class AddCommentController {
else else
comment = Base64.encode(DataUtil.encodei18nString(comment)) comment = Base64.encode(DataUtil.encodei18nString(comment))
model.selectedFiles.each { model.selectedFiles.each {
def event = new UICommentEvent(sharedFile : it, oldComment : it.getComment())
it.setComment(comment) it.setComment(comment)
core.eventBus.publish(event)
} }
mvcGroup.parentGroup.view.refreshSharedFiles() mvcGroup.parentGroup.view.refreshSharedFiles()
cancel() cancel()

View File

@@ -0,0 +1,95 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull
import com.muwire.core.EventBus
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.BrowseStatusEvent
import com.muwire.core.search.UIBrowseEvent
import com.muwire.core.search.UIResultEvent
@ArtifactProviderFor(GriffonController)
class BrowseController {
@MVCMember @Nonnull
BrowseModel model
@MVCMember @Nonnull
BrowseView view
EventBus eventBus
void register() {
eventBus.register(BrowseStatusEvent.class, this)
eventBus.register(UIResultEvent.class, this)
eventBus.publish(new UIBrowseEvent(host : model.host))
}
void mvcGroupDestroy() {
eventBus.unregister(BrowseStatusEvent.class, this)
eventBus.unregister(UIResultEvent.class, this)
}
void onBrowseStatusEvent(BrowseStatusEvent e) {
runInsideUIAsync {
model.status = e.status
}
}
void onUIResultEvent(UIResultEvent e) {
runInsideUIAsync {
model.results << e
view.resultsTable.model.fireTableDataChanged()
}
}
@ControllerAction
void dismiss() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
@ControllerAction
void download() {
def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.isEmpty())
return
selectedResults.removeAll {
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash)
}
selectedResults.each { result ->
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
eventBus.publish(new UIDownloadEvent(
result : [result],
sources : [model.host.destination],
target : file,
sequential : mvcGroup.parentGroup.view.sequentialDownloadCheckbox.model.isSelected()
))
}
mvcGroup.parentGroup.parentGroup.view.showDownloadsWindow.call()
dismiss()
}
@ControllerAction
void viewComment() {
def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.size() != 1)
return
def result = selectedResults[0]
if (result.comment == null)
return
String groupId = Base64.encode(result.infohash.getRoot())
Map<String,Object> params = new HashMap<>()
params['result'] = result
mvcGroup.createMVCGroup("show-comment", groupId, params)
}
}

View File

@@ -85,7 +85,8 @@ class MainFrameController {
def terms = replaced.split(" ") def terms = replaced.split(" ")
def nonEmpty = [] def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it } terms.each { if (it.length() > 0) nonEmpty << it }
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true) searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments)
} }
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop, core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
@@ -288,6 +289,7 @@ class MainFrameController {
Map<String, Object> params = new HashMap<>() Map<String, Object> params = new HashMap<>()
params['selectedFiles'] = selectedFiles params['selectedFiles'] = selectedFiles
params['core'] = core
mvcGroup.createMVCGroup("add-comment", "Add Comment", params) mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
} }

View File

@@ -70,6 +70,10 @@ class OptionsController {
model.updateCheckInterval = text model.updateCheckInterval = text
settings.updateCheckInterval = Integer.valueOf(text) settings.updateCheckInterval = Integer.valueOf(text)
boolean searchComments = view.searchCommentsCheckbox.model.isSelected()
model.searchComments = searchComments
settings.searchComments = searchComments
boolean autoDownloadUpdate = view.autoDownloadUpdateCheckbox.model.isSelected() boolean autoDownloadUpdate = view.autoDownloadUpdateCheckbox.model.isSelected()
model.autoDownloadUpdate = autoDownloadUpdate model.autoDownloadUpdate = autoDownloadUpdate
settings.autoDownloadUpdate = autoDownloadUpdate settings.autoDownloadUpdate = autoDownloadUpdate
@@ -79,6 +83,14 @@ class OptionsController {
model.shareDownloadedFiles = shareDownloaded model.shareDownloadedFiles = shareDownloaded
settings.shareDownloadedFiles = shareDownloaded settings.shareDownloadedFiles = shareDownloaded
boolean shareHidden = view.shareHiddenCheckbox.model.isSelected()
model.shareHiddenFiles = shareHidden
settings.shareHiddenFiles = shareHidden
boolean browseFiles = view.browseFilesCheckbox.model.isSelected()
model.browseFiles = browseFiles
settings.browseFiles = browseFiles
String downloadLocation = model.downloadLocation String downloadLocation = model.downloadLocation
settings.downloadLocation = new File(downloadLocation) settings.downloadLocation = new File(downloadLocation)
@@ -124,9 +136,8 @@ class OptionsController {
model.font = text model.font = text
uiSettings.font = text uiSettings.font = text
// boolean showMonitor = view.monitorCheckbox.model.isSelected() uiSettings.autoFontSize = model.automaticFontSize
// model.showMonitor = showMonitor uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
// uiSettings.showMonitor = showMonitor
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected() boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
model.clearCancelledDownloads = clearCancelledDownloads model.clearCancelledDownloads = clearCancelledDownloads
@@ -140,10 +151,6 @@ 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)
@@ -168,4 +175,15 @@ class OptionsController {
if (rv == JFileChooser.APPROVE_OPTION) if (rv == JFileChooser.APPROVE_OPTION)
model.downloadLocation = chooser.getSelectedFile().getAbsolutePath() model.downloadLocation = chooser.getSelectedFile().getAbsolutePath()
} }
@ControllerAction
void automaticFontAction() {
model.automaticFontSize = true
model.customFontSize = 12
}
@ControllerAction
void customFontAction() {
model.automaticFontSize = false
}
} }

View File

@@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull import javax.annotation.Nonnull
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
@@ -85,4 +86,19 @@ class SearchTabController {
def sender = model.senders[row] def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL)) 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['eventBus'] = core.eventBus
mvcGroup.createMVCGroup("browse", groupId, params)
}
} }

View File

@@ -10,15 +10,19 @@ import com.muwire.gui.UISettings
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import javax.swing.JLabel
import javax.swing.JTable import javax.swing.JTable
import javax.swing.LookAndFeel import javax.swing.LookAndFeel
import javax.swing.UIManager import javax.swing.UIManager
import javax.swing.plaf.FontUIResource
import static griffon.util.GriffonApplicationUtils.isMacOSX import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel import static groovy.swing.SwingBuilder.lookAndFeel
import java.awt.Font import java.awt.Font
import java.awt.Toolkit
import java.util.logging.Level import java.util.logging.Level
import java.util.logging.LogManager
@Log @Log
class Initialize extends AbstractLifecycleHandler { class Initialize extends AbstractLifecycleHandler {
@@ -29,6 +33,16 @@ class Initialize extends AbstractLifecycleHandler {
@Override @Override
void execute() { void execute() {
if (System.getProperty("java.util.logging.config.file") == null) {
log.info("No config file specified, so turning off logging")
def names = LogManager.getLogManager().getLoggerNames()
while(names.hasMoreElements()) {
def name = names.nextElement()
LogManager.getLogManager().getLogger(name).setLevel(Level.OFF)
}
}
log.info "Loading home dir" log.info "Loading home dir"
def portableHome = System.getProperty("portable.home") def portableHome = System.getProperty("portable.home")
def home = portableHome == null ? def home = portableHome == null ?
@@ -52,25 +66,43 @@ class Initialize extends AbstractLifecycleHandler {
guiPropsFile.withInputStream { props.load(it) } guiPropsFile.withInputStream { props.load(it) }
uiSettings = new UISettings(props) uiSettings = new UISettings(props)
def lnf
log.info("settting user-specified lnf $uiSettings.lnf") log.info("settting user-specified lnf $uiSettings.lnf")
try { try {
lookAndFeel(uiSettings.lnf) lnf = lookAndFeel(uiSettings.lnf)
} catch (Throwable bad) { } catch (Throwable bad) {
log.log(Level.WARNING,"couldn't set desired look and feeel, switching to defaults", bad) log.log(Level.WARNING,"couldn't set desired look and feel, switching to defaults", bad)
uiSettings.lnf = lookAndFeel("system","gtk","metal").getID() lnf = lookAndFeel("system","gtk","metal")
uiSettings.lnf = lnf.getID()
} }
if (uiSettings.font != null) { if (uiSettings.font != null || uiSettings.autoFontSize || uiSettings.fontSize > 0) {
log.info("setting user-specified font $uiSettings.font")
Font font = new Font(uiSettings.font, Font.PLAIN, 12) FontUIResource defaultFont = lnf.getDefaults().getFont("Label.font")
def defaults = UIManager.getDefaults()
defaults.put("Button.font", font) String fontName
defaults.put("RadioButton.font", font) if (uiSettings.font != null)
defaults.put("Label.font", font) fontName = uiSettings.font
defaults.put("CheckBox.font", font) else
defaults.put("Table.font", font) fontName = defaultFont.getName()
defaults.put("TableHeader.font", font)
// TODO: add others int fontSize = defaultFont.getSize()
if (uiSettings.autoFontSize) {
int resolution = Toolkit.getDefaultToolkit().getScreenResolution()
fontSize = resolution / 9;
} else {
fontSize = uiSettings.fontSize
}
FontUIResource font = new FontUIResource(fontName, Font.PLAIN, fontSize)
def keys = lnf.getDefaults().keys()
while(keys.hasMoreElements()) {
def key = keys.nextElement()
def value = lnf.getDefaults().get(key)
if (value instanceof FontUIResource)
lnf.getDefaults().put(key, font)
}
} }
} else { } else {
Properties props = new Properties() Properties props = new Properties()

View File

@@ -0,0 +1,19 @@
package com.muwire.gui
import com.muwire.core.Persona
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
import com.muwire.core.search.BrowseStatus
@ArtifactProviderFor(GriffonModel)
class BrowseModel {
Persona host
@Observable BrowseStatus status
@Observable boolean downloadActionEnabled
@Observable boolean viewCommentActionEnabled
def results = []
}

View File

@@ -13,7 +13,10 @@ class OptionsModel {
@Observable String updateCheckInterval @Observable String updateCheckInterval
@Observable boolean autoDownloadUpdate @Observable boolean autoDownloadUpdate
@Observable boolean shareDownloadedFiles @Observable boolean shareDownloadedFiles
@Observable boolean shareHiddenFiles
@Observable String downloadLocation @Observable String downloadLocation
@Observable boolean searchComments
@Observable boolean browseFiles
// i2p options // i2p options
@Observable String inboundLength @Observable String inboundLength
@@ -27,6 +30,8 @@ class OptionsModel {
@Observable boolean showMonitor @Observable boolean showMonitor
@Observable String lnf @Observable String lnf
@Observable String font @Observable String font
@Observable boolean automaticFontSize
@Observable int customFontSize
@Observable boolean clearCancelledDownloads @Observable boolean clearCancelledDownloads
@Observable boolean clearFinishedDownloads @Observable boolean clearFinishedDownloads
@Observable boolean excludeLocalResult @Observable boolean excludeLocalResult
@@ -49,7 +54,10 @@ class OptionsModel {
updateCheckInterval = settings.updateCheckInterval updateCheckInterval = settings.updateCheckInterval
autoDownloadUpdate = settings.autoDownloadUpdate autoDownloadUpdate = settings.autoDownloadUpdate
shareDownloadedFiles = settings.shareDownloadedFiles shareDownloadedFiles = settings.shareDownloadedFiles
shareHiddenFiles = settings.shareHiddenFiles
downloadLocation = settings.downloadLocation.getAbsolutePath() downloadLocation = settings.downloadLocation.getAbsolutePath()
searchComments = settings.searchComments
browseFiles = settings.browseFiles
Core core = application.context.get("core") Core core = application.context.get("core")
inboundLength = core.i2pOptions["inbound.length"] inboundLength = core.i2pOptions["inbound.length"]
@@ -63,6 +71,8 @@ class OptionsModel {
showMonitor = uiSettings.showMonitor showMonitor = uiSettings.showMonitor
lnf = uiSettings.lnf lnf = uiSettings.lnf
font = uiSettings.font font = uiSettings.font
automaticFontSize = uiSettings.autoFontSize
customFontSize = uiSettings.fontSize
clearCancelledDownloads = uiSettings.clearCancelledDownloads clearCancelledDownloads = uiSettings.clearCancelledDownloads
clearFinishedDownloads = uiSettings.clearFinishedDownloads clearFinishedDownloads = uiSettings.clearFinishedDownloads
excludeLocalResult = uiSettings.excludeLocalResult excludeLocalResult = uiSettings.excludeLocalResult

View File

@@ -21,6 +21,7 @@ class SearchTabModel {
@Observable boolean downloadActionEnabled @Observable boolean downloadActionEnabled
@Observable boolean trustButtonsEnabled @Observable boolean trustButtonsEnabled
@Observable boolean browseActionEnabled
Core core Core core
UISettings uiSettings UISettings uiSettings

View File

@@ -0,0 +1,132 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JLabel
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.search.UIResultEvent
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class BrowseView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
BrowseModel model
@MVCMember @Nonnull
BrowseController controller
def mainFrame
def dialog
def p
def resultsTable
def lastSortEvent
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, model.host.getHumanReadableName(), true)
dialog.setResizable(true)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label(text: "Status:")
label(text: bind {model.status.toString()})
}
scrollPane (constraints : BorderLayout.CENTER){
resultsTable = table(autoCreateRowSorter : true) {
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: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
button(text : "Dismiss", dismissAction)
}
}
def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
resultsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
resultsTable.rowSorter.addRowSorterListener({evt -> lastSortEvent = evt})
resultsTable.rowSorter.setSortsOnUpdates(true)
def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
selectionModel.addListSelectionListener({
int[] rows = resultsTable.getSelectedRows()
if (rows.length == 0) {
model.downloadActionEnabled = false
model.viewCommentActionEnabled = false
return
}
if (lastSortEvent != null) {
for (int i = 0; i < rows.length; i ++) {
rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
boolean downloadActionEnabled = true
if (rows.length == 1 && model.results[rows[0]].comment != null)
model.viewCommentActionEnabled = true
else
model.viewCommentActionEnabled = false
rows.each {
downloadActionEnabled &= mvcGroup.parentGroup.parentGroup.model.canDownload(model.results[it].infohash)
}
model.downloadActionEnabled = downloadActionEnabled
})
}
void mvcGroupInit(Map<String,String> args) {
controller.register()
dialog.getContentPane().add(p)
dialog.setSize(700, 400)
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
def selectedResults() {
int [] rows = resultsTable.getSelectedRows()
if (rows.length == 0)
return null
if (lastSortEvent != null) {
for (int i = 0; i < rows.length; i ++) {
rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
List<UIResultEvent> rv = new ArrayList<>()
for (Integer i : rows)
rv << model.results[i]
rv
}
}

View File

@@ -217,7 +217,7 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) { scrollPane(constraints : BorderLayout.CENTER) {
def jtree = new JTree(model.sharedTree) def jtree = new JTree(model.sharedTree)
jtree.setCellRenderer(new SharedTreeRenderer()) jtree.setCellRenderer(new SharedTreeRenderer())
tree(id : "shared-files-tree", rootVisible : false, jtree) tree(id : "shared-files-tree", rootVisible : false, expandsSelectedPaths: true, jtree)
} }
} }
} }
@@ -852,7 +852,7 @@ class MainFrameView {
def shareFiles = { def shareFiles = {
def chooser = new JFileChooser() def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false) chooser.setFileHidingEnabled(!model.core.muOptions.shareHiddenFiles)
chooser.setDialogTitle("Select file to share") chooser.setDialogTitle("Select file to share")
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY) chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
chooser.setMultiSelectionEnabled(true) chooser.setMultiSelectionEnabled(true)
@@ -876,7 +876,10 @@ class MainFrameView {
} }
public void refreshSharedFiles() { public void refreshSharedFiles() {
def tree = builder.getVariable("shared-files-tree")
TreePath[] selectedPaths = tree.getSelectionPaths()
model.sharedTree.nodeStructureChanged(model.treeRoot) model.sharedTree.nodeStructureChanged(model.treeRoot)
tree.setSelectionPaths(selectedPaths)
builder.getVariable("shared-files-table").model.fireTableDataChanged() builder.getVariable("shared-files-table").model.fireTableDataChanged()
} }
} }

View File

@@ -12,6 +12,7 @@ import javax.swing.SwingConstants
import com.muwire.core.Core import com.muwire.core.Core
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.GridBagConstraints
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
@@ -35,6 +36,9 @@ class OptionsView {
def updateField def updateField
def autoDownloadUpdateCheckbox def autoDownloadUpdateCheckbox
def shareDownloadedCheckbox def shareDownloadedCheckbox
def shareHiddenCheckbox
def searchCommentsCheckbox
def browseFilesCheckbox
def inboundLengthField def inboundLengthField
def inboundQuantityField def inboundQuantityField
@@ -46,6 +50,7 @@ class OptionsView {
def lnfField def lnfField
def monitorCheckbox def monitorCheckbox
def fontField def fontField
def fontSizeField
def clearCancelledDownloadsCheckbox def clearCancelledDownloadsCheckbox
def clearFinishedDownloadsCheckbox def clearFinishedDownloadsCheckbox
def excludeLocalResultCheckbox def excludeLocalResultCheckbox
@@ -69,23 +74,32 @@ class OptionsView {
d.setResizable(false) d.setResizable(false)
p = builder.panel { p = builder.panel {
gridBagLayout() gridBagLayout()
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0)) label(text : "Search in comments", constraints:gbc(gridx: 0, gridy:0))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0)) searchCommentsCheckbox = checkBox(selected : bind {model.searchComments}, constraints : gbc(gridx:1, gridy:0))
label(text : "seconds", constraints : gbc(gridx : 2, gridy: 0))
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1)) label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 1))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1)) retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 1))
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1)) label(text : "seconds", constraints : gbc(gridx : 2, gridy: 1))
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 2)) label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 2))
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate}, constraints : gbc(gridx:1, gridy : 2)) updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 2))
label(text : "hours", constraints : gbc(gridx: 2, gridy : 2))
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:3)) label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 3))
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:3)) autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate}, constraints : gbc(gridx:1, gridy : 3))
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:4)) label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:4))
button(text : "Choose", constraints : gbc(gridx : 1, gridy:4), downloadLocationAction) shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:4))
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:0, gridy:5, gridwidth:2))
label(text : "Share hidden files", constraints : gbc(gridx : 0, gridy:5))
shareHiddenCheckbox = checkBox(selected : bind {model.shareHiddenFiles}, constraints : gbc(gridx :1, gridy:5))
label(text : "Allow browsing", constraints : gbc(gridx : 0, gridy : 6))
browseFilesCheckbox = checkBox(selected : bind {model.browseFiles}, constraints : gbc(gridx : 1, gridy : 6))
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:7))
button(text : "Choose", constraints : gbc(gridx : 1, gridy:7), downloadLocationAction)
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:0, gridy:8, gridwidth:2))
} }
i = builder.panel { i = builder.panel {
@@ -113,19 +127,28 @@ class OptionsView {
gridBagLayout() gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2)) 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)) 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)) lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_START))
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2)) label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2)) fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2, anchor : GridBagConstraints.LINE_START))
// label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
// monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3)) label(text : "Font Size", constraints : gbc(gridx: 0, gridy : 3))
label(text : "Automatically Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4)) buttonGroup(id: "fontSizeGroup")
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4)) radioButton(text: "Automatic", selected : bind {model.automaticFontSize}, buttonGroup : fontSizeGroup,
label(text : "Automatically Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5)) constraints : gbc(gridx : 1, gridy: 3, anchor : GridBagConstraints.LINE_START), automaticFontAction)
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5)) radioButton(text: "Custom", selected : bind {!model.automaticFontSize}, buttonGroup : fontSizeGroup,
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6)) constraints : gbc(gridx : 1, gridy: 4, anchor : GridBagConstraints.LINE_START), customFontAction)
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6)) fontSizeField = textField(text : bind {model.customFontSize}, enabled : bind {!model.automaticFontSize}, constraints : gbc(gridx : 2, gridy : 4))
// 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)) label(text : "Automatically Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:5))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads},
constraints : gbc(gridx : 1, gridy:5, anchor : GridBagConstraints.LINE_START))
label(text : "Automatically Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:6))
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads},
constraints : gbc(gridx : 1, gridy:6, anchor : GridBagConstraints.LINE_START))
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:7))
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult},
constraints : gbc(gridx: 1, gridy : 7, anchor : GridBagConstraints.LINE_START))
} }
bandwidth = builder.panel { bandwidth = builder.panel {
gridBagLayout() gridBagLayout()

View File

@@ -63,6 +63,7 @@ class SearchTabView {
tableModel(list : model.senders) { tableModel(list : model.senders) {
closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()}) 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 : "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 -> closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row ->
model.core.trustService.getLevel(row.destination).toString() model.core.trustService.getLevel(row.destination).toString()
}) })
@@ -70,11 +71,17 @@ class SearchTabView {
} }
} }
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
gridLayout(rows: 1, cols : 2)
panel {
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
}
panel {
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction) button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction) button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction) button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
} }
} }
}
panel { panel {
borderLayout() borderLayout()
scrollPane (constraints : BorderLayout.CENTER) { scrollPane (constraints : BorderLayout.CENTER) {
@@ -193,12 +200,14 @@ class SearchTabView {
int row = selectedSenderRow() int row = selectedSenderRow()
if (row < 0) { if (row < 0) {
model.trustButtonsEnabled = false model.trustButtonsEnabled = false
model.browseActionEnabled = false
return return
} else { } else {
Persona sender = model.senders[row]
model.browseActionEnabled = model.sendersBucket[sender].first().browse
model.trustButtonsEnabled = true model.trustButtonsEnabled = true
model.results.clear() model.results.clear()
Persona p = model.senders[row] model.results.addAll(model.sendersBucket[sender])
model.results.addAll(model.sendersBucket[p])
resultsTable.model.fireTableDataChanged() resultsTable.model.fireTableDataChanged()
} }
}) })
@@ -226,6 +235,9 @@ class SearchTabView {
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard") JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()}) copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard) menu.add(copyHashToClipboard)
JMenuItem copyNameToClipboard = new JMenuItem("Copy name to clipboard")
copyNameToClipboard.addActionListener({mvcGroup.view.copyNameToClipboard()})
menu.add(copyNameToClipboard)
showMenu = true showMenu = true
// show comment if any // show comment if any
@@ -242,19 +254,35 @@ class SearchTabView {
menu.show(e.getComponent(), e.getX(), e.getY()) menu.show(e.getComponent(), e.getX(), e.getY())
} }
def copyHashToClipboard() { private UIResultEvent getSelectedResult() {
int[] selectedRows = resultsTable.getSelectedRows() int[] selectedRows = resultsTable.getSelectedRows()
if (selectedRows.length != 1) if (selectedRows.length != 1)
return return null
int selected = selectedRows[0] int selected = selectedRows[0]
if (lastSortEvent != null) if (lastSortEvent != null)
selected = resultsTable.rowSorter.convertRowIndexToModel(selected) selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
String hash = Base64.encode(model.results[selected].infohash.getRoot()) model.results[selected]
}
def copyHashToClipboard() {
def result = getSelectedResult()
if (result == null)
return
String hash = Base64.encode(result.infohash.getRoot())
StringSelection selection = new StringSelection(hash) StringSelection selection = new StringSelection(hash)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard() def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null) clipboard.setContents(selection, null)
} }
def copyNameToClipboard() {
def result = getSelectedResult()
if (result == null)
return
StringSelection selection = new StringSelection(result.getName())
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
def showComment() { def showComment() {
int selectedRow = resultsTable.getSelectedRow() int selectedRow = resultsTable.getSelectedRow()
if (selectedRow < 0) if (selectedRow < 0)

View File

@@ -5,6 +5,8 @@ class UISettings {
String lnf String lnf
boolean showMonitor boolean showMonitor
String font String font
boolean autoFontSize
int fontSize
boolean clearCancelledDownloads boolean clearCancelledDownloads
boolean clearFinishedDownloads boolean clearFinishedDownloads
boolean excludeLocalResult boolean excludeLocalResult
@@ -18,6 +20,8 @@ class UISettings {
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false")) clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","true")) excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","true"))
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true")) showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true"))
autoFontSize = Boolean.parseBoolean(props.getProperty("autoFontSize","false"))
fontSize = Integer.parseInt(props.getProperty("fontSize","12"))
} }
void write(OutputStream out) throws IOException { void write(OutputStream out) throws IOException {
@@ -28,6 +32,8 @@ class UISettings {
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)) props.setProperty("showSearchHashes", String.valueOf(showSearchHashes))
props.setProperty("autoFontSize", String.valueOf(autoFontSize))
props.setProperty("fontSize", String.valueOf(fontSize))
if (font != null) if (font != null)
props.setProperty("font", font) props.setProperty("font", font)