From eaea97b042f8744b2b2fdce7d45d668cbe37a8de Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 9 Jan 2007 15:06:29 +0000 Subject: [PATCH] webui added --- webui/WebUIApp-compile | 3 + webui/WebUIApp-shell | 3 + webui/json.py | 310 +++++++ webui/src/com/WebUI/WebUIApp.gwt.xml | 7 + webui/src/com/WebUI/client/WebUI.java | 497 +++++++++++ webui/src/com/WebUI/public/WebUI.css | 268 ++++++ webui/src/com/WebUI/public/WebUI.html | 58 ++ webui/webuiserver.py | 211 +++++ ...0953E833A2B9B18DAA93BB081DC8A7D.cache.html | 797 +++++++++++++++++ ...10953E833A2B9B18DAA93BB081DC8A7D.cache.xml | 10 + ...1D20AAC454AFEE8A60D7380D38B0693.cache.html | 800 ++++++++++++++++++ ...11D20AAC454AFEE8A60D7380D38B0693.cache.xml | 10 + ...A33FFDB2E9418B40D144BFD01370A52.cache.html | 797 +++++++++++++++++ ...6A33FFDB2E9418B40D144BFD01370A52.cache.xml | 10 + ...7527B531338C30501EFDC3455BB151E.cache.html | 797 +++++++++++++++++ ...C7527B531338C30501EFDC3455BB151E.cache.xml | 10 + webui/www/com.WebUI.WebUIApp/WebUI.css | 268 ++++++ webui/www/com.WebUI.WebUIApp/WebUI.html | 58 ++ .../com.WebUI.WebUIApp.nocache.html | 118 +++ webui/www/com.WebUI.WebUIApp/gwt.js | 578 +++++++++++++ webui/www/com.WebUI.WebUIApp/history.html | 20 + webui/www/com.WebUI.WebUIApp/tree_closed.gif | Bin 0 -> 82 bytes webui/www/com.WebUI.WebUIApp/tree_open.gif | Bin 0 -> 78 bytes webui/www/com.WebUI.WebUIApp/tree_white.gif | Bin 0 -> 61 bytes webui/xubuntu-6.10-desktop-i386.iso.torrent | Bin 0 -> 21365 bytes 25 files changed, 5630 insertions(+) create mode 100755 webui/WebUIApp-compile create mode 100755 webui/WebUIApp-shell create mode 100644 webui/json.py create mode 100644 webui/src/com/WebUI/WebUIApp.gwt.xml create mode 100644 webui/src/com/WebUI/client/WebUI.java create mode 100644 webui/src/com/WebUI/public/WebUI.css create mode 100644 webui/src/com/WebUI/public/WebUI.html create mode 100644 webui/webuiserver.py create mode 100644 webui/www/com.WebUI.WebUIApp/10953E833A2B9B18DAA93BB081DC8A7D.cache.html create mode 100644 webui/www/com.WebUI.WebUIApp/10953E833A2B9B18DAA93BB081DC8A7D.cache.xml create mode 100644 webui/www/com.WebUI.WebUIApp/11D20AAC454AFEE8A60D7380D38B0693.cache.html create mode 100644 webui/www/com.WebUI.WebUIApp/11D20AAC454AFEE8A60D7380D38B0693.cache.xml create mode 100644 webui/www/com.WebUI.WebUIApp/6A33FFDB2E9418B40D144BFD01370A52.cache.html create mode 100644 webui/www/com.WebUI.WebUIApp/6A33FFDB2E9418B40D144BFD01370A52.cache.xml create mode 100644 webui/www/com.WebUI.WebUIApp/C7527B531338C30501EFDC3455BB151E.cache.html create mode 100644 webui/www/com.WebUI.WebUIApp/C7527B531338C30501EFDC3455BB151E.cache.xml create mode 100644 webui/www/com.WebUI.WebUIApp/WebUI.css create mode 100644 webui/www/com.WebUI.WebUIApp/WebUI.html create mode 100644 webui/www/com.WebUI.WebUIApp/com.WebUI.WebUIApp.nocache.html create mode 100644 webui/www/com.WebUI.WebUIApp/gwt.js create mode 100644 webui/www/com.WebUI.WebUIApp/history.html create mode 100644 webui/www/com.WebUI.WebUIApp/tree_closed.gif create mode 100644 webui/www/com.WebUI.WebUIApp/tree_open.gif create mode 100644 webui/www/com.WebUI.WebUIApp/tree_white.gif create mode 100644 webui/xubuntu-6.10-desktop-i386.iso.torrent diff --git a/webui/WebUIApp-compile b/webui/WebUIApp-compile new file mode 100755 index 000000000..e79b59f8a --- /dev/null +++ b/webui/WebUIApp-compile @@ -0,0 +1,3 @@ +#!/bin/sh +APPDIR=`dirname $0`; +java -cp "$APPDIR/src:$APPDIR/bin:/home/alon/Temp/gwt-linux-1.2.22/gwt-user.jar:/home/alon/Temp/gwt-linux-1.2.22/gwt-dev-linux.jar" com.google.gwt.dev.GWTCompiler -out "$APPDIR/www" "$@" com.WebUI.WebUIApp; diff --git a/webui/WebUIApp-shell b/webui/WebUIApp-shell new file mode 100755 index 000000000..6c6ab19df --- /dev/null +++ b/webui/WebUIApp-shell @@ -0,0 +1,3 @@ +#!/bin/sh +APPDIR=`dirname $0`; +java -cp "$APPDIR/src:$APPDIR/bin:/home/alon/Temp/gwt-linux-1.2.22/gwt-user.jar:/home/alon/Temp/gwt-linux-1.2.22/gwt-dev-linux.jar" com.google.gwt.dev.GWTShell -out "$APPDIR/www" "$@" com.WebUI.WebUIApp/WebUIApp.html; diff --git a/webui/json.py b/webui/json.py new file mode 100644 index 000000000..a28a13e39 --- /dev/null +++ b/webui/json.py @@ -0,0 +1,310 @@ +import string +import types + +## json.py implements a JSON (http://json.org) reader and writer. +## Copyright (C) 2005 Patrick D. Logan +## Contact mailto:patrickdlogan@stardecisions.com +## +## This library is free software; you can redistribute it and/or +## modify it under the terms of the GNU Lesser General Public +## License as published by the Free Software Foundation; either +## version 2.1 of the License, or (at your option) any later version. +## +## This library is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public +## License along with this library; if not, write to the Free Software +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +class _StringGenerator(object): + def __init__(self, string): + self.string = string + self.index = -1 + def peek(self): + i = self.index + 1 + if i < len(self.string): + return self.string[i] + else: + return None + def next(self): + self.index += 1 + if self.index < len(self.string): + return self.string[self.index] + else: + raise StopIteration + def all(self): + return self.string + +class WriteException(Exception): + pass + +class ReadException(Exception): + pass + +class JsonReader(object): + hex_digits = {'A': 10,'B': 11,'C': 12,'D': 13,'E': 14,'F':15} + escapes = {'t':'\t','n':'\n','f':'\f','r':'\r','b':'\b'} + + def read(self, s): + self._generator = _StringGenerator(s) + result = self._read() + return result + + def _read(self): + self._eatWhitespace() + peek = self._peek() + if peek is None: + raise ReadException, "Nothing to read: '%s'" % self._generator.all() + if peek == '{': + return self._readObject() + elif peek == '[': + return self._readArray() + elif peek == '"': + return self._readString() + elif peek == '-' or peek.isdigit(): + return self._readNumber() + elif peek == 't': + return self._readTrue() + elif peek == 'f': + return self._readFalse() + elif peek == 'n': + return self._readNull() + elif peek == '/': + self._readComment() + return self._read() + else: + raise ReadException, "Input is not valid JSON: '%s'" % self._generator.all() + + def _readTrue(self): + self._assertNext('t', "true") + self._assertNext('r', "true") + self._assertNext('u', "true") + self._assertNext('e', "true") + return True + + def _readFalse(self): + self._assertNext('f', "false") + self._assertNext('a', "false") + self._assertNext('l', "false") + self._assertNext('s', "false") + self._assertNext('e', "false") + return False + + def _readNull(self): + self._assertNext('n', "null") + self._assertNext('u', "null") + self._assertNext('l', "null") + self._assertNext('l', "null") + return None + + def _assertNext(self, ch, target): + if self._next() != ch: + raise ReadException, "Trying to read %s: '%s'" % (target, self._generator.all()) + + def _readNumber(self): + isfloat = False + result = self._next() + peek = self._peek() + while peek is not None and (peek.isdigit() or peek == "."): + isfloat = isfloat or peek == "." + result = result + self._next() + peek = self._peek() + try: + if isfloat: + return float(result) + else: + return int(result) + except ValueError: + raise ReadException, "Not a valid JSON number: '%s'" % result + + def _readString(self): + result = "" + assert self._next() == '"' + try: + while self._peek() != '"': + ch = self._next() + if ch == "\\": + ch = self._next() + if ch in 'brnft': + ch = self.escapes[ch] + elif ch == "u": + ch4096 = self._next() + ch256 = self._next() + ch16 = self._next() + ch1 = self._next() + n = 4096 * self._hexDigitToInt(ch4096) + n += 256 * self._hexDigitToInt(ch256) + n += 16 * self._hexDigitToInt(ch16) + n += self._hexDigitToInt(ch1) + ch = unichr(n) + elif ch not in '"/\\': + raise ReadException, "Not a valid escaped JSON character: '%s' in %s" % (ch, self._generator.all()) + result = result + ch + except StopIteration: + raise ReadException, "Not a valid JSON string: '%s'" % self._generator.all() + assert self._next() == '"' + return result + + def _hexDigitToInt(self, ch): + try: + result = self.hex_digits[ch.upper()] + except KeyError: + try: + result = int(ch) + except ValueError: + raise ReadException, "The character %s is not a hex digit." % ch + return result + + def _readComment(self): + assert self._next() == "/" + second = self._next() + if second == "/": + self._readDoubleSolidusComment() + elif second == '*': + self._readCStyleComment() + else: + raise ReadException, "Not a valid JSON comment: %s" % self._generator.all() + + def _readCStyleComment(self): + try: + done = False + while not done: + ch = self._next() + done = (ch == "*" and self._peek() == "/") + if not done and ch == "/" and self._peek() == "*": + raise ReadException, "Not a valid JSON comment: %s, '/*' cannot be embedded in the comment." % self._generator.all() + self._next() + except StopIteration: + raise ReadException, "Not a valid JSON comment: %s, expected */" % self._generator.all() + + def _readDoubleSolidusComment(self): + try: + ch = self._next() + while ch != "\r" and ch != "\n": + ch = self._next() + except StopIteration: + pass + + def _readArray(self): + result = [] + assert self._next() == '[' + done = self._peek() == ']' + while not done: + item = self._read() + result.append(item) + self._eatWhitespace() + done = self._peek() == ']' + if not done: + ch = self._next() + if ch != ",": + raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch) + assert ']' == self._next() + return result + + def _readObject(self): + result = {} + assert self._next() == '{' + done = self._peek() == '}' + while not done: + key = self._read() + if type(key) is not types.StringType: + raise ReadException, "Not a valid JSON object key (should be a string): %s" % key + self._eatWhitespace() + ch = self._next() + if ch != ":": + raise ReadException, "Not a valid JSON object: '%s' due to: '%s'" % (self._generator.all(), ch) + self._eatWhitespace() + val = self._read() + result[key] = val + self._eatWhitespace() + done = self._peek() == '}' + if not done: + ch = self._next() + if ch != ",": + raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch) + assert self._next() == "}" + return result + + def _eatWhitespace(self): + p = self._peek() + while p is not None and p in string.whitespace or p == '/': + if p == '/': + self._readComment() + else: + self._next() + p = self._peek() + + def _peek(self): + return self._generator.peek() + + def _next(self): + return self._generator.next() + +class JsonWriter(object): + + def _append(self, s): + self._results.append(s) + + def write(self, obj, escaped_forward_slash=False): + self._escaped_forward_slash = escaped_forward_slash + self._results = [] + self._write(obj) + return "".join(self._results) + + def _write(self, obj): + ty = type(obj) + if ty is types.DictType: + n = len(obj) + self._append("{") + for k, v in obj.items(): + self._write(k) + self._append(":") + self._write(v) + n = n - 1 + if n > 0: + self._append(",") + self._append("}") + elif ty is types.ListType or ty is types.TupleType: + n = len(obj) + self._append("[") + for item in obj: + self._write(item) + n = n - 1 + if n > 0: + self._append(",") + self._append("]") + elif ty is types.StringType or ty is types.UnicodeType: + self._append('"') + obj = obj.replace('\\', r'\\') + if self._escaped_forward_slash: + obj = obj.replace('/', r'\/') + obj = obj.replace('"', r'\"') + obj = obj.replace('\b', r'\b') + obj = obj.replace('\f', r'\f') + obj = obj.replace('\n', r'\n') + obj = obj.replace('\r', r'\r') + obj = obj.replace('\t', r'\t') + self._append(obj) + self._append('"') + elif ty is types.IntType or ty is types.LongType: + self._append(str(obj)) + elif ty is types.FloatType: + self._append("%f" % obj) + elif obj is True: + self._append("true") + elif obj is False: + self._append("false") + elif obj is None: + self._append("null") + else: + raise WriteException, "Cannot write in JSON: %s" % repr(obj) + +def write(obj, escaped_forward_slash=False): + return JsonWriter().write(obj, escaped_forward_slash) + +def read(s): + return JsonReader().read(s) diff --git a/webui/src/com/WebUI/WebUIApp.gwt.xml b/webui/src/com/WebUI/WebUIApp.gwt.xml new file mode 100644 index 000000000..3134f92d0 --- /dev/null +++ b/webui/src/com/WebUI/WebUIApp.gwt.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/webui/src/com/WebUI/client/WebUI.java b/webui/src/com/WebUI/client/WebUI.java new file mode 100644 index 000000000..66e93dbd1 --- /dev/null +++ b/webui/src/com/WebUI/client/WebUI.java @@ -0,0 +1,497 @@ +/* + Copyright (c) 2006 Alon Zakai ('Kripken') + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package com.WebUI.client; + +import java.lang.Throwable; +import java.lang.System; + +//import java.util.Map; +//import java.util.HashMap; +import java.util.Iterator; + +//import com.google.gwt.user.client.ui.ClickListener; +import com.google.gwt.user.client.ui.Label; +//import com.google.gwt.user.client.HistoryListener; + +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Command; +import com.google.gwt.core.client.EntryPoint; + +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.MenuBar; +import com.google.gwt.user.client.ui.MenuItem; +import com.google.gwt.user.client.ui.DockPanel; +import com.google.gwt.user.client.ui.FlexTable; +import com.google.gwt.user.client.ui.SourcesTableEvents; +import com.google.gwt.user.client.ui.TableListener; + +import com.google.gwt.http.client.RequestBuilder; +import com.google.gwt.http.client.RequestException; +import com.google.gwt.http.client.RequestCallback; +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.Response; +import com.google.gwt.http.client.RequestTimeoutException; + +import com.google.gwt.json.client.JSONParser; +import com.google.gwt.json.client.JSONValue; +import com.google.gwt.json.client.JSONObject; + +public class WebUIUtilities { + public static String round(double value, int places) { + String ret = String.valueOf(value); + int temp = ret.indexOf("."); + + if (temp == -1) { + return ret; + } + + return (ret.substring(0, temp + places)); + } + + public static String getDataRate(double value) { + return (getDataAmount(value) + "/s"); + } + + public static String getDataAmount(double value) { + double val = 0; + String units; + + if (value < 1048576) { + val = value / 1024.0; + units = "KB"; + } else if (value < 1073741824) { + val = value / 1048576.0; + units = "MB"; + } else { + val = value / 1073741824.0; + units = "GB"; + } + + return (round(val, 2) + " " + units); + } + + public static native void gotoURL(String newURL) /*-{ + window.alert(location.href); + location.href = "www.cnn.com"; + window.alert(location.href); + }-*/; +} + +public class TorrentInfo { + public long unique_ID; + public long queue_pos; + public String name; + public double download_rate; + public double upload_rate; + public long total_seeds; + public long total_peers; + public long num_seeds; + public long num_peers; +} + +public class TorrentListAction implements TableListener { + private TorrentList mainList; + + public TorrentListAction(TorrentList list) { + mainList = list; + } + + public void onCellClicked(SourcesTableEvents sender, int row, int cell) { + // Select the row that was clicked (-1 to account for header row). + if (row > 0) + mainList.selectRow(row - 1); + } +} + +public class TorrentList extends FlexTable { + private int selectedRow = -1; + private TorrentListAction action = new TorrentListAction(this); + private TorrentInfo[] oldTorrents = null; + + public void init() { + setStyleName("torrentlist"); + setCellSpacing(0); + setCellPadding(2); + + // Headers row + setText(0, 0, "#"); // We need a hashmap for names to columns, for the future + setText(0, 1, "Name"); + setText(0, 2, "Seeds"); + setText(0, 3, "Peers"); + setText(0, 4, "Download"); + setText(0, 5, "Upload"); + getRowFormatter().addStyleName(0, "torrentList-Title"); + + addTableListener(action); + } + + public void applyTorrents(TorrentInfo[] torrents) { +//Window.alert("Applying torrents in tablelist"); + // Add new torrents and update existing ones + for (int x = 0; x < torrents.length; x++) { + int tempIndex = getIndexByUniqueID(torrents[x].unique_ID); + if (tempIndex == -1) { + tempIndex = getRowCount()-1; // (-1 because of headers) + } + updateTorrent(tempIndex, torrents[x]); + } + + // Delete torrents no longer with us. CHANGE selectedRow to -1 if the selected row is dead! + // ... + + // Save the new torrents, for the next comparison + oldTorrents = torrents; + + // Select a row, if none is currently selected + if (getRowCount() > 1 && selectedRow == -1) { +// Window.alert("Selecting 1"); + selectRow(0); + } else { +// Window.alert("No Selecting"); + } + } + + private int getIndexByUniqueID(long unique_ID) { + if (oldTorrents == null) { + return -1; + } + + for (int x = 0; x < oldTorrents.length; x++) { + if (oldTorrents[x].unique_ID == unique_ID) { + return x; + } + } + + return -1; + } + + private void updateTorrent(int row, TorrentInfo torrent) { + row = row + 1; +//Window.alert("Updating torrent in tablelist: " + String.valueOf(row)); + setText(row, 0, String.valueOf(torrent.queue_pos)+1); // Humans like queues starting at 1 + setText(row, 1, torrent.name); + setText(row, 2, String.valueOf(torrent.num_seeds) + " (" + + String.valueOf(torrent.total_seeds) + ")"); + setText(row, 3, String.valueOf(torrent.num_peers) + " (" + + String.valueOf(torrent.total_peers) + ")"); + setText(row, 4, WebUIUtilities.getDataRate(torrent.download_rate)); + setText(row, 5, WebUIUtilities.getDataRate(torrent.upload_rate)); + setWidth("100%"); + } + + public void selectRow(int row) { + styleRow(selectedRow, false); + styleRow(row, true); + + selectedRow = row; +// Mail.get().displayItem(item); + } + + private void styleRow(int row, boolean selected) { + if (row != -1) { + if (selected) + getRowFormatter().addStyleName(row + 1, "torrentList-SelectedRow"); + else + getRowFormatter().removeStyleName(row + 1, "torrentList-SelectedRow"); + } + } } + +public class WebUIApp implements EntryPoint { + + private static final int STATUS_CODE_OK = 200; + private static final int TIMEOUT = 3000; + private static final int TIMER = 2000; // SHOULD BE 1000 - but more allows for debug + private static final int MAX_TIMER = TIMEOUT/TIMER; + + private DockPanel panel = new DockPanel(); + private MenuBar menu = new MenuBar(); + private TorrentList torrentList = new TorrentList(); + private Label statusBar = new Label(); + + private Request currRequest; + private boolean waiting = false; + private int currTimer = 0; + private JSONValue torrentsJSON; + private TorrentInfo[] currTorrents; + + public void onModuleLoad() { +//Window.alert("Module Load"); + + // Buttons +// final Button button = new Button("Click here..."); + + // Menus + MenuBar menu0 = new MenuBar(true); + + menu0.addItem("Quit", true, new Command() { + public void execute() { + doPost("/", "quit", new RequestCallback() { + public void onResponseReceived(Request request, Response response) { + } + public void onError(Request request, Throwable e) { + } + }); + + // Move to new page, a "bye" page. + quit(); +// WebUIUtilities.gotoURL("about:blank"); +// Window.alert("Bye."); + } + }); + + menu.addItem(new MenuItem("File", menu0)); + menu.setWidth("100%"); + + // Table list + torrentList.init(); +// torrentList.addTorrent("a.torrent"); + +// torrentList.selectRow(0); + + // Timer + Timer t = new Timer() { + public void run() { + heartBeat(); + } + }; + + t.scheduleRepeating(TIMER); // + + // Set up main panel + RootPanel.get().setStyleName("webui-Info"); + + panel.add(torrentList, DockPanel.CENTER); +// panel.add(statusBar, DockPanel.SOUTH); + + RootPanel.get().add(menu); + RootPanel.get().add(panel); + RootPanel.get().add(statusBar); +//Window.alert("end Module Load"); + } + + private void quit() { + RootPanel.get().remove(menu); + RootPanel.get().remove(panel); + RootPanel.get().remove(statusBar); + } + + // Called once per TIMER tick (usually 1 second?) + private void heartBeat() { + + // FOR NOW, just do it to fill torrentsJSON, that's it +// if (torrentsJSON == null) { + + if (true) { // We always tick, don't we? + // Send and/or cancel current server request + if (!waiting || currTimer == MAX_TIMER) { + if (currRequest != null) { + currRequest.cancel(); + } + doPost("/", "list", new RequestCallback() { + public void onResponseReceived(Request request, Response response) { + waiting = false; + + if (STATUS_CODE_OK == response.getStatusCode()) { + setStatusBar("Server responding.");// + response.getText()); + torrentsJSON = JSONParser.parse(response.getText()); + updateTorrentList(); + } else { + setStatusBar("Server gives an error: " + response.getHeadersAsString() + "," + response.getStatusCode() + "," + response.getStatusText() + "," + response.getText()); + } + } + + public void onError(Request request, Throwable e) { + waiting = false; + + if (e instanceof RequestTimeoutException) { + setStatusBar("Server timed out.");// + e.getMessage()); + } else { + setStatusBar("Server gave an ODD error: " + e.getMessage()); + } + } + }); + +// setStatusBar("Sent request..."); + waiting = true; + currTimer = 0; + } else { +// setStatusBar("Still waiting..." + String.valueOf(currTimer)); + currTimer = currTimer + 1; + } + } else { + setStatusBar("Core off."); + } +// } // FOR NOW + + // Update torrent list? +// updateTorrentList(); + } + + private void setStatusBar(String text) { + statusBar.setText(String.valueOf(System.currentTimeMillis()) + ":" + text); +// statusBar.setText(String.valueOf(System.currentTimeMillis()) + ":" + text + "\r\n
" + statusBar.getText()); + } + + // Need to allow different callbacks from the post... + private void doPost(String url, String postData, RequestCallback callback) { + RequestBuilder builder = new RequestBuilder(RequestBuilder.POST, url); + builder.setTimeoutMillis(TIMEOUT); + + try { + currRequest = builder.sendRequest(postData, callback); + } catch (RequestException e) { + waiting = false; + setStatusBar("Failed to send a POST request: " + e.getMessage()); + } + } + + // Update torrent list, using torrentsJSON (which was updated in the POST callback) + private void updateTorrentList() { + if (torrentsJSON == null) { + return; + } +//Window.alert("UpdateTorrentList - got torrentsJSON"); + currTorrents = new TorrentInfo[torrentsJSON.isArray().size()]; + + JSONObject curr; + + for (int x = 0; x < torrentsJSON.isArray().size(); x++) { + curr = torrentsJSON.isArray().get(x).isObject(); + + currTorrents[x] = new TorrentInfo(); + currTorrents[x].unique_ID = (long) curr.get("unique_ID").isNumber().getValue(); + currTorrents[x].queue_pos = (long) curr.get("queue_pos").isNumber().getValue(); + currTorrents[x].name = curr.get("name").isString().stringValue(); + currTorrents[x].download_rate = curr.get("download_rate").isNumber().getValue(); + currTorrents[x].upload_rate = curr.get("upload_rate").isNumber().getValue(); + currTorrents[x].total_seeds = (long)curr.get("total_seeds").isNumber().getValue(); + currTorrents[x].total_peers = (long)curr.get("total_peers").isNumber().getValue(); + currTorrents[x].num_seeds = (long)curr.get("num_seeds").isNumber().getValue(); + currTorrents[x].num_peers = (long)curr.get("num_peers").isNumber().getValue(); + } + + torrentList.applyTorrents(currTorrents); +//Window.alert("end UpdateTorrentList"); + } + + // A debug convenience function + private void dumpJSON(JSONValue value) { + if (value.isArray() != null) { + Window.alert("Array; size: " + String.valueOf(value.isArray().size())); + + for (int x = 0; x < value.isArray().size(); x++) { + dumpJSON(value.isArray().get(x)); + } + } else if (value.isBoolean() != null) { + Window.alert("Boolean" + String.valueOf(value.isBoolean().booleanValue())); + } else if (value.isNull() != null) { + Window.alert("NULL"); + } else if (value.isNumber() != null) { + Window.alert("Number" + String.valueOf(value.isNumber().getValue())); + } else if (value.isObject() != null) { + Window.alert("Object size: " + String.valueOf(value.isObject().size())); + + Iterator it = value.isObject().keySet().iterator(); + String key; + for (int x = 0; x < value.isObject().size(); x++) { + key = String.valueOf(it.next()); + Window.alert("(Key:)" + key); + dumpJSON(value.isObject().get(key)); + } + } else if (value.isString() != null) { + Window.alert("String: " + value.isString().stringValue()); + } else { + Window.alert("WHAT IS THIS JSON?!"); + } + } + +} + + + + +/*public class MenuAction implements Command { + public void execute() { + Window.alert("Thank you for selecting a menu item."); +// Window.alert("Thank you for selecting a menu item."); + } +}*/ + +/* +Map phoneBook = new HashMap(); +phoneBook.put("Sally Smart", "555-9999"); +phoneBook.put("John Doe", "555-1212"); +phoneBook.put("J. Random Hacker", "555-1337"); + +The get method is used to access a key; for example, the value of the expression phoneBook.get("Sally Smart") is "555-9999". +*/ + + + + + +/* final Label label = new Label(); +// History.addHistoryListener(this); +// Window.alert("Nifty, eh?"); + + button.addClickListener(new ClickListener() { + public void onClick(Widget sender) { + if (label.getText().equals("")) + label.setText("Good, it works."); + else + label.setText(""); + } + }); + + // Assume that the host HTML has elements defined whose + // IDs are "slot1", "slot2". In a real app, you probably would not want + // to hard-code IDs. Instead, you could, for example, search for all + // elements with a particular CSS class and replace them with widgets. + // + RootPanel.get("slot1").add(button); + RootPanel.get("slot2").add(label); +*/ + + +/* private HashMap parsePythonDict(String pythonDict) { + HashMap ret = new HashMap(); + int startIndex, endIndex = 0; + String key, val; + + while (pythonDict.indexOf("'", endIndex + 1) != -1) { + startIndex = pythonDict.indexOf("'", endIndex + 1) + 1; + endIndex = pythonDict.indexOf("'", startIndex + 1); + key = pythonDict.substring(startIndex, endIndex); + + startIndex = endIndex + 3; + endIndex = pythonDict.indexOf(",", startIndex + 1); // BUG POTENTIAL + if (endIndex == -1) { + endIndex = pythonDict.lastIndexOf("}"); + } + val = pythonDict.substring(startIndex, endIndex); + + ret.put(key, val); + Window.alert(key + " :: " + val); + } + + return ret; + }*/ diff --git a/webui/src/com/WebUI/public/WebUI.css b/webui/src/com/WebUI/public/WebUI.css new file mode 100644 index 000000000..4f75caa73 --- /dev/null +++ b/webui/src/com/WebUI/public/WebUI.css @@ -0,0 +1,268 @@ +body { + background-color: white; + color: black; + font-family: Arial, sans-serif; + font-size: medium; + margin: 20px 20px 20px 20px; +} + +code { + font-size: small; +} + +a { + color: darkblue; +} + +a:visited { + color: darkblue; +} + +.gwt-BorderedPanel { +} + +.gwt-Button { +} + +.gwt-Canvas { +} + +.gwt-CheckBox { + font-size: smaller; +} + +.gwt-DialogBox { + sborder: 8px solid #C3D9FF; + border: 2px outset; + background-color: white; +} + +.gwt-DialogBox .Caption { + background-color: #C3D9FF; + padding: 3px; + margin: 2px; + font-weight: bold; + cursor: default; +} + +.gwt-FileUpload { +} + +.gwt-Frame { +} + +.gwt-HorizontalSplitter .Bar { + width: 8px; + background-color: #C3D9FF; +} + +.gwt-VerticalSplitter .Bar { + height: 8px; + background-color: #C3D9FF; +} + +.gwt-HTML { + font-size: small; +} + +.gwt-Hyperlink { +} + +.gwt-Image { +} + +.gwt-Label { + font-size: medium; +} + +.gwt-ListBox { +} + +.gwt-MenuBar { + background-color: #C3D9FF; + border: 1px solid #87B3FF; + cursor: default; +} + +.gwt-MenuBar .gwt-MenuItem { + padding: 1px 4px 1px 4px; + font-size: medium; + cursor: default; +} + +.gwt-MenuBar .gwt-MenuItem-selected { + background-color: #E8EEF7; +} + +.gwt-PasswordTextBox { +} + +.gwt-RadioButton { + font-size: smaller; +} + +.gwt-TabPanel { +} + +.gwt-TabPanelBottom { + border-left: 1px solid #87B3FF; +} + +.gwt-TabBar { + background-color: #C3D9FF; + font-size: smaller; +} + +.gwt-TabBar .gwt-TabBarFirst { + height: 100%; + border-bottom: 1px solid #87B3FF; + padding-left: 3px; +} + +.gwt-TabBar .gwt-TabBarRest { + border-bottom: 1px solid #87B3FF; + padding-right: 3px; +} + +.gwt-TabBar .gwt-TabBarItem { + border-top: 1px solid #C3D9FF; + border-bottom: 1px solid #87B3FF; + padding: 2px; + cursor: pointer; + cursor: hand; +} + +.gwt-TabBar .gwt-TabBarItem-selected { + font-weight: bold; + background-color: #E8EEF7; + border-top: 1px solid #87B3FF; + border-left: 1px solid #87B3FF; + border-right: 1px solid #87B3FF; + border-bottom: 1px solid #E8EEF7; + padding: 2px; + cursor: default; +} + +.gwt-TextArea { +} + +.gwt-TextBox { +} + +.gwt-Tree { +} + +.gwt-Tree .gwt-TreeItem { + font-size: smaller; +} + +.gwt-Tree .gwt-TreeItem-selected { + background-color: #C3D9FF; +} + +.gwt-StackPanel { +} + +.gwt-StackPanel .gwt-StackPanelItem { + background-color: #C3D9FF; + cursor: pointer; + cursor: hand; +} + +.gwt-StackPanel .gwt-StackPanelItem-selected { +} + +.webui-Info { + background-color: #C3D9FF; + padding: 10px 10px 2px 10px; + font-size: smaller; +} + +/* -------------------------------------------------------------------------- +.ks-Sink { + border: 8px solid #C3D9FF; + background-color: #E8EEF7; + width: 100%; + height: 24em; +} + + +.ks-List { + margin-top: 8px; + margin-bottom: 8px; + font-size: smaller; +} + +.ks-List .ks-SinkItem { + width: 100%; + padding: 0.3em; + padding-right: 16px; + cursor: pointer; + cursor: hand; +} + +.ks-List .ks-SinkItem-selected { + background-color: #C3D9FF; +} + +.ks-images-Image { + margin: 8px; +} + +.ks-images-Button { + margin: 8px; + cursor: pointer; + cursor: hand; +} + +.ks-layouts { + margin: 8px; +} + +.ks-layouts-Label { + background-color: #C3D9FF; + font-weight: bold; + margin-top: 1em; + padding: 2px 0px 2px 0px; + width: 100%; +} + +.ks-layouts-Scroller { + height: 128px; + border: 2px solid #C3D9FF; + padding: 8px; + margin: 8px; +} + +.ks-popups-Popup { + background-color: white; + border: 1px solid #87B3FF; + padding: 4px; +} + +.infoProse { + margin: 8px; +} + +------*/ + +table.torrentlist { + margin: 1ex 0ex 1ex 0ex; +// border-spacing: 5.5ex 5.5ex 6.5ex 5.5ex; + border: medium solid black; +// border-color: black; +} + +table.torrentlist td { +// border: 50em; + padding: 1ex; +} + +.torrentList-Title { + background-color: rgb(175,175,255); +} + +.torrentList-SelectedRow { + background-color: rgb(140,140,255); +} + diff --git a/webui/src/com/WebUI/public/WebUI.html b/webui/src/com/WebUI/public/WebUI.html new file mode 100644 index 000000000..296c6b368 --- /dev/null +++ b/webui/src/com/WebUI/public/WebUI.html @@ -0,0 +1,58 @@ + + + + + + + WebUI 0.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webui/webuiserver.py b/webui/webuiserver.py new file mode 100644 index 000000000..0a3c60759 --- /dev/null +++ b/webui/webuiserver.py @@ -0,0 +1,211 @@ +# +# Copyright (c) 2006 Alon Zakai ('Kripken') +# +# 2006-15-9 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# + +# Docs: +# +# All httpserver-related issues are done through GET (html, javascript, css, +# etc.). All torrentcore issues are doen through POST. +# + +import time +import BaseHTTPServer +import sys, os +import webbrowser + +sys.path.append("/media/sda2/svn/deluge-trac/trunk/library") + +import flood # or whatever the core is renamed to be +import json + +# Constants + +HOST_NAME = 'localhost' +PORT_NUMBER = 9999 + +HTML_DIR = "www/com.WebUI.WebUIApp" + +HEADERS_TEXT = "text/plain" +HEADERS_HTML = "text/html" +HEADERS_CSS = "text/css" +HEADERS_JS = "text/javascript" + +manager = None +httpd = None + +class webuiServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): + def get_secret_str(self): + return "?" + secret + + def pulse(self): + global manager + + manager.handle_events() + + def write_headers(self, content_type, data_length=None): + self.send_response(200) + self.send_header("Content-type", content_type) + if data_length is not None: + self.send_header("Content-length", data_length) + self.end_headers() + + def do_POST(self): + global manager + + input_length = int(self.headers.get('Content-length')) + command = self.rfile.read(input_length) + print "POST command:", command + + if command == "quit": + httpd.ready = False + +# self.write_headers(HEADERS_TEXT) +# self.wfile.write("OK: quit") + # List torrents, and pulse the heartbeat + elif command == "list": + self.pulse() # Start by ticking the clock + + data = [] + unique_IDs = manager.get_unique_IDs() + for unique_ID in unique_IDs: + temp = manager.get_torrent_state(unique_ID) + temp["unique_ID"] = unique_ID # We add the unique_ID ourselves + data.append(temp) + + self.write_headers(HEADERS_TEXT) + self.wfile.write(json.write(data)) + else: + # Basically we can just send Python commands, to be run in exec(command)... but that + # would be slow, I guess + print "UNKNOWN POST COMMAND:", command + + def do_GET(self): +# self.wfile.write("

webuiServer 0.5.1.1

") + + print "Contacted from:", self.client_address + + if "?" in self.path: + command = self.path[1:self.path.find("?")] + else: + command = self.path[1:] + + if command == "": + command = "WebUI.html" + + if not self.path[-(len(self.get_secret_str())):] == self.get_secret_str(): + self.write_headers(HEADERS_HTML) + self.wfile.write("webuiServer") + self.wfile.write("

Invalid access. Run 'webuiserver SECRET', then access 'localhost:9999/?SECRET'.

") + self.wfile.write("") + return + + if "." in command: + extension = command[command.rfind("."):] + else: + extension = "" + + print "Handling: ", self.path, ":", command, ":", extension + + try: + filey = open("./" + HTML_DIR + "/" + command, 'rb') + lines = filey.readlines() + filey.close() + + data = "".join(lines) + + if extension == ".html": + self.write_headers(HEADERS_HTML, len(data)) + elif extension == ".js": + self.write_headers(HEADERS_JS, len(data)) + elif extension == ".css": + self.write_headers(HEADERS_CSS, len(data)) + else: + print "What is this?", extension + + self.wfile.write(data) + except IOError: + self.write_headers(HEADERS_HTML) + self.wfile.write("webuiServer") + self.wfile.write("

webuiServer 0.5.1.1

") + self.wfile.write("No such command: " + command) + + +class webuiServer(BaseHTTPServer.HTTPServer): + def serve_forever(self): + self.ready = True + while self.ready: + self.handle_request() + self.server_close() + +######## +# Main # +######## + +print "-------------------" +print "webuiServer 0.5.1.1" +print "-------------------" +print "" + +try: + secret = sys.argv[1] +except IndexError: + print "USAGE: 'webuiserver.py S', where S is the secret password used to access via a browser" + secret = "" + +if not secret == "": + +# manager.add_torrent("xubuntu-6.10-desktop-i386.iso.torrent", +# os.path.expanduser("~") + "/Temp", True) + + httpd = webuiServer((HOST_NAME, PORT_NUMBER), webuiServerHandler) + print time.asctime(), "HTTP Server Started - %s:%s" % (HOST_NAME, PORT_NUMBER) + + manager = flood.manager("FL", "0500", "webui", + os.path.expanduser("~") + "/Temp")#, blank_slate=True) + + webbrowser.open("localhost:9999/?" + secret) + + try: + httpd.serve_forever() + except KeyboardInterrupt: + pass + + print time.asctime(), "HTTP Server Stopped - %s:%s" % (HOST_NAME, PORT_NUMBER) + + print "Shutting down manager..." + manager.quit() + + +### OLD +# # Check if the manager is running +# if not command == "init": +# if manager is None: +# self.write_headers(HEADERS_TEXT) +# self.wfile.write("ERROR: manager is None") +# return +# +# if command == "init": +# if manager is not None: +# print "ERROR: Trying to init, but already active" +# return +# +# manager = webui.manager("FL", "0500", "webui", +# os.path.expanduser("~") + "/Temp")#, blank_slate=True) +# self.write_headers(HEADERS_TEXT) +# self.wfile.write("OK: init") diff --git a/webui/www/com.WebUI.WebUIApp/10953E833A2B9B18DAA93BB081DC8A7D.cache.html b/webui/www/com.WebUI.WebUIApp/10953E833A2B9B18DAA93BB081DC8A7D.cache.html new file mode 100644 index 000000000..5163e1d44 --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/10953E833A2B9B18DAA93BB081DC8A7D.cache.html @@ -0,0 +1,797 @@ + + + +This script is part of module +com.WebUI.WebUIApp + diff --git a/webui/www/com.WebUI.WebUIApp/10953E833A2B9B18DAA93BB081DC8A7D.cache.xml b/webui/www/com.WebUI.WebUIApp/10953E833A2B9B18DAA93BB081DC8A7D.cache.xml new file mode 100644 index 000000000..03ee61345 --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/10953E833A2B9B18DAA93BB081DC8A7D.cache.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webui/www/com.WebUI.WebUIApp/11D20AAC454AFEE8A60D7380D38B0693.cache.html b/webui/www/com.WebUI.WebUIApp/11D20AAC454AFEE8A60D7380D38B0693.cache.html new file mode 100644 index 000000000..41b3db173 --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/11D20AAC454AFEE8A60D7380D38B0693.cache.html @@ -0,0 +1,800 @@ + + + +This script is part of module +com.WebUI.WebUIApp + diff --git a/webui/www/com.WebUI.WebUIApp/11D20AAC454AFEE8A60D7380D38B0693.cache.xml b/webui/www/com.WebUI.WebUIApp/11D20AAC454AFEE8A60D7380D38B0693.cache.xml new file mode 100644 index 000000000..ad65ed482 --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/11D20AAC454AFEE8A60D7380D38B0693.cache.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webui/www/com.WebUI.WebUIApp/6A33FFDB2E9418B40D144BFD01370A52.cache.html b/webui/www/com.WebUI.WebUIApp/6A33FFDB2E9418B40D144BFD01370A52.cache.html new file mode 100644 index 000000000..a94f4e078 --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/6A33FFDB2E9418B40D144BFD01370A52.cache.html @@ -0,0 +1,797 @@ + + + +This script is part of module +com.WebUI.WebUIApp + diff --git a/webui/www/com.WebUI.WebUIApp/6A33FFDB2E9418B40D144BFD01370A52.cache.xml b/webui/www/com.WebUI.WebUIApp/6A33FFDB2E9418B40D144BFD01370A52.cache.xml new file mode 100644 index 000000000..cc77b668d --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/6A33FFDB2E9418B40D144BFD01370A52.cache.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webui/www/com.WebUI.WebUIApp/C7527B531338C30501EFDC3455BB151E.cache.html b/webui/www/com.WebUI.WebUIApp/C7527B531338C30501EFDC3455BB151E.cache.html new file mode 100644 index 000000000..5db1aff47 --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/C7527B531338C30501EFDC3455BB151E.cache.html @@ -0,0 +1,797 @@ + + + +This script is part of module +com.WebUI.WebUIApp + diff --git a/webui/www/com.WebUI.WebUIApp/C7527B531338C30501EFDC3455BB151E.cache.xml b/webui/www/com.WebUI.WebUIApp/C7527B531338C30501EFDC3455BB151E.cache.xml new file mode 100644 index 000000000..6534357ad --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/C7527B531338C30501EFDC3455BB151E.cache.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webui/www/com.WebUI.WebUIApp/WebUI.css b/webui/www/com.WebUI.WebUIApp/WebUI.css new file mode 100644 index 000000000..4f75caa73 --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/WebUI.css @@ -0,0 +1,268 @@ +body { + background-color: white; + color: black; + font-family: Arial, sans-serif; + font-size: medium; + margin: 20px 20px 20px 20px; +} + +code { + font-size: small; +} + +a { + color: darkblue; +} + +a:visited { + color: darkblue; +} + +.gwt-BorderedPanel { +} + +.gwt-Button { +} + +.gwt-Canvas { +} + +.gwt-CheckBox { + font-size: smaller; +} + +.gwt-DialogBox { + sborder: 8px solid #C3D9FF; + border: 2px outset; + background-color: white; +} + +.gwt-DialogBox .Caption { + background-color: #C3D9FF; + padding: 3px; + margin: 2px; + font-weight: bold; + cursor: default; +} + +.gwt-FileUpload { +} + +.gwt-Frame { +} + +.gwt-HorizontalSplitter .Bar { + width: 8px; + background-color: #C3D9FF; +} + +.gwt-VerticalSplitter .Bar { + height: 8px; + background-color: #C3D9FF; +} + +.gwt-HTML { + font-size: small; +} + +.gwt-Hyperlink { +} + +.gwt-Image { +} + +.gwt-Label { + font-size: medium; +} + +.gwt-ListBox { +} + +.gwt-MenuBar { + background-color: #C3D9FF; + border: 1px solid #87B3FF; + cursor: default; +} + +.gwt-MenuBar .gwt-MenuItem { + padding: 1px 4px 1px 4px; + font-size: medium; + cursor: default; +} + +.gwt-MenuBar .gwt-MenuItem-selected { + background-color: #E8EEF7; +} + +.gwt-PasswordTextBox { +} + +.gwt-RadioButton { + font-size: smaller; +} + +.gwt-TabPanel { +} + +.gwt-TabPanelBottom { + border-left: 1px solid #87B3FF; +} + +.gwt-TabBar { + background-color: #C3D9FF; + font-size: smaller; +} + +.gwt-TabBar .gwt-TabBarFirst { + height: 100%; + border-bottom: 1px solid #87B3FF; + padding-left: 3px; +} + +.gwt-TabBar .gwt-TabBarRest { + border-bottom: 1px solid #87B3FF; + padding-right: 3px; +} + +.gwt-TabBar .gwt-TabBarItem { + border-top: 1px solid #C3D9FF; + border-bottom: 1px solid #87B3FF; + padding: 2px; + cursor: pointer; + cursor: hand; +} + +.gwt-TabBar .gwt-TabBarItem-selected { + font-weight: bold; + background-color: #E8EEF7; + border-top: 1px solid #87B3FF; + border-left: 1px solid #87B3FF; + border-right: 1px solid #87B3FF; + border-bottom: 1px solid #E8EEF7; + padding: 2px; + cursor: default; +} + +.gwt-TextArea { +} + +.gwt-TextBox { +} + +.gwt-Tree { +} + +.gwt-Tree .gwt-TreeItem { + font-size: smaller; +} + +.gwt-Tree .gwt-TreeItem-selected { + background-color: #C3D9FF; +} + +.gwt-StackPanel { +} + +.gwt-StackPanel .gwt-StackPanelItem { + background-color: #C3D9FF; + cursor: pointer; + cursor: hand; +} + +.gwt-StackPanel .gwt-StackPanelItem-selected { +} + +.webui-Info { + background-color: #C3D9FF; + padding: 10px 10px 2px 10px; + font-size: smaller; +} + +/* -------------------------------------------------------------------------- +.ks-Sink { + border: 8px solid #C3D9FF; + background-color: #E8EEF7; + width: 100%; + height: 24em; +} + + +.ks-List { + margin-top: 8px; + margin-bottom: 8px; + font-size: smaller; +} + +.ks-List .ks-SinkItem { + width: 100%; + padding: 0.3em; + padding-right: 16px; + cursor: pointer; + cursor: hand; +} + +.ks-List .ks-SinkItem-selected { + background-color: #C3D9FF; +} + +.ks-images-Image { + margin: 8px; +} + +.ks-images-Button { + margin: 8px; + cursor: pointer; + cursor: hand; +} + +.ks-layouts { + margin: 8px; +} + +.ks-layouts-Label { + background-color: #C3D9FF; + font-weight: bold; + margin-top: 1em; + padding: 2px 0px 2px 0px; + width: 100%; +} + +.ks-layouts-Scroller { + height: 128px; + border: 2px solid #C3D9FF; + padding: 8px; + margin: 8px; +} + +.ks-popups-Popup { + background-color: white; + border: 1px solid #87B3FF; + padding: 4px; +} + +.infoProse { + margin: 8px; +} + +------*/ + +table.torrentlist { + margin: 1ex 0ex 1ex 0ex; +// border-spacing: 5.5ex 5.5ex 6.5ex 5.5ex; + border: medium solid black; +// border-color: black; +} + +table.torrentlist td { +// border: 50em; + padding: 1ex; +} + +.torrentList-Title { + background-color: rgb(175,175,255); +} + +.torrentList-SelectedRow { + background-color: rgb(140,140,255); +} + diff --git a/webui/www/com.WebUI.WebUIApp/WebUI.html b/webui/www/com.WebUI.WebUIApp/WebUI.html new file mode 100644 index 000000000..296c6b368 --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/WebUI.html @@ -0,0 +1,58 @@ + + + + + + + WebUI 0.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webui/www/com.WebUI.WebUIApp/com.WebUI.WebUIApp.nocache.html b/webui/www/com.WebUI.WebUIApp/com.WebUI.WebUIApp.nocache.html new file mode 100644 index 000000000..693952658 --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/com.WebUI.WebUIApp.nocache.html @@ -0,0 +1,118 @@ + + + +This script is part of module com.WebUI.WebUIApp + + diff --git a/webui/www/com.WebUI.WebUIApp/gwt.js b/webui/www/com.WebUI.WebUIApp/gwt.js new file mode 100644 index 000000000..916359fdd --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/gwt.js @@ -0,0 +1,578 @@ +// Copyright 2006 Google Inc. All Rights Reserved. +// This startup script should be included in host pages either just after +// or inside the after module tags. +// + +////////////////////////////////////////////////////////////////////////////// +// DynamicResources +// + +function DynamicResources() { + this.pendingElemsBySrc_ = {}; + this.pendingScriptElems_ = new Array(); +} +DynamicResources.prototype = {}; + +// The array is set up such that, pairwise, the entries are (src, readyFnStr). +// Called once for each module that is attached to the host page. +// It is theoretically possible that addScripts() could be called reentrantly +// if the browser event loop is pumped during this function and an iframe loads; +// we may want to enhance this method in the future to support that case. +DynamicResources.prototype.addScripts = function(scriptArray, insertBeforeElem) { + var wasEmpty = (this.pendingScriptElems_.length == 0); + var anyAdded = false; + for (var i = 0, n = scriptArray.length; i < n; i += 2) { + var src = scriptArray[i]; + if (this.pendingElemsBySrc_[src]) { + // Don't load the same script twice. + continue; + } + // Set up the element but don't add it to the DOM until its turn. + anyAdded = true; + var e = document.createElement("script"); + this.pendingElemsBySrc_[src] = e; + var readyFn; + eval("readyFn = " + scriptArray[i+1]); + e.__readyFn = readyFn; + e.type = "text/javascript"; + e.src = src; + e.__insertBeforeElem = insertBeforeElem; + this.pendingScriptElems_ = this.pendingScriptElems_.concat(e); + } + + if (wasEmpty && anyAdded) { + // Kickstart. + this.injectScript(this.pendingScriptElems_[0]); + } +} + +DynamicResources.prototype.injectScript = function(scriptElem) { + var parentElem = scriptElem.__insertBeforeElem.parentNode; + parentElem.insertBefore(scriptElem, scriptElem.__insertBeforeElem); +} + +DynamicResources.prototype.addStyles = function(styleSrcArray, insertBeforeElem) { + var parent = insertBeforeElem.parentNode; + for (var i = 0, n = styleSrcArray.length; i < n; ++i) { + var src = styleSrcArray[i]; + if (this.pendingElemsBySrc_[src]) + continue; + var e = document.createElement("link"); + this.pendingElemsBySrc_[src] = e; + e.type = "text/css"; + e.rel = "stylesheet"; + e.href = src; + parent.insertBefore(e, insertBeforeElem); + } +} + +DynamicResources.prototype.isReady = function() { + var elems = this.pendingScriptElems_; + if (elems.length > 0) { + var e = elems[0]; + if (!e.__readyFn()) { + // The pending script isn't ready yet. + return false; + } + + // The pending script has now finished loading. Enqueue the next, if any. + e.__readyFn = null; + elems.shift(); + if (elems.length > 0) { + // There is another script. + this.injectScript(elems[0]); + return false; + } + } + + // There are no more pending scripts. + return true; +} + +////////////////////////////////////////////////////////////////////////////// +// ModuleControlBlock +// +function ModuleControlBlock(metaElem, rawName) { + var parts = ["", rawName]; + var i = rawName.lastIndexOf("="); + if (i != -1) { + parts[0] = rawName.substring(0, i) + '/'; + parts[1] = rawName.substring(i+1); + } + + this.metaElem_ = metaElem; + this.baseUrl_ = parts[0]; + this.name_ = parts[1]; + this.compilationLoaded_ = false; + this.frameWnd_ = null; +} +ModuleControlBlock.prototype = {}; + +/** + * Determines whether this module is fully loaded and ready to run. + */ +ModuleControlBlock.prototype.isReady = function() { + return this.compilationLoaded_; +}; + +/** + * Called when the compilation for this module is loaded. + */ +ModuleControlBlock.prototype.compilationLoaded = function(frameWnd) { + this.frameWnd_ = frameWnd; + this.compilationLoaded_ = true; +} + +/** + * Gets the logical module name, not including a base url prefix if one was + * specified. + */ +ModuleControlBlock.prototype.getName = function() { + return this.name_; +} + +/** + * Gets the base URL of the module, guaranteed to end with a slash. + */ +ModuleControlBlock.prototype.getBaseURL = function() { + return this.baseUrl_; +} + +/** + * Gets the window of the module's frame. + */ +ModuleControlBlock.prototype.getModuleFrameWindow = function() { + return this.frameWnd_; +} + +/** + * Injects a set of dynamic scripts. + * The array is set up such that, pairwise, the entries are (src, readyFnStr). + */ +ModuleControlBlock.prototype.addScripts = function(scriptSrcArray) { + return ModuleControlBlocks.dynamicResources_.addScripts(scriptSrcArray, this.metaElem_); +} + +/** + * Injects a set of dynamic styles. + */ +ModuleControlBlock.prototype.addStyles = function(styleSrcArray) { + return ModuleControlBlocks.dynamicResources_.addStyles(styleSrcArray, this.metaElem_); +} + +////////////////////////////////////////////////////////////////////////////// +// ModuleControlBlocks +// +function ModuleControlBlocks() { + this.blocks_ = []; +} +ModuleControlBlocks.dynamicResources_ = new DynamicResources(); // "static" +ModuleControlBlocks.prototype = {}; + +/** + * Adds a module control control block for the named module. + * @param metaElem the meta element that caused the module to be added + * @param name the name of the module being added, optionally preceded by + * an alternate base url of the form "_path_=_module_". + */ +ModuleControlBlocks.prototype.add = function(metaElem, name) { + var mcb = new ModuleControlBlock(metaElem, name); + this.blocks_ = this.blocks_.concat(mcb); +}; + +/** + * Determines whether all the modules are loaded and ready to run. + */ +ModuleControlBlocks.prototype.isReady = function() { + for (var i = 0, n = this.blocks_.length; i < n; ++i) { + var mcb = this.blocks_[i]; + if (!mcb.isReady()) { + return false; + } + } + + // Are there any pending dynamic resources (e.g. styles, scripts)? + if (!ModuleControlBlocks.dynamicResources_.isReady()) { + // No, we're still waiting on one or more dynamic resources. + return false; + } + + return true; +} + +/** + * Determines whether there are any module control blocks. + */ +ModuleControlBlocks.prototype.isEmpty = function() { + return this.blocks_.length == 0; +} + +/** + * Gets the module control block at the specified index. + */ +ModuleControlBlocks.prototype.get = function(index) { + return this.blocks_[index]; +} + +/** + * Injects an iframe for each module. + */ +ModuleControlBlocks.prototype.injectFrames = function() { + for (var i = 0, n = this.blocks_.length; i < n; ++i) { + var mcb = this.blocks_[i]; + + // Insert an iframe for the module + var iframe = document.createElement("iframe"); + var selectorUrl = mcb.getBaseURL() + mcb.getName() + ".nocache.html"; + selectorUrl += "?" + (__gwt_isHosted() ? "h&" : "" ) + i; + var unique = new Date().getTime(); + selectorUrl += "&" + unique; + iframe.style.border = '0px'; + iframe.style.width = '0px'; + iframe.style.height = '0px'; + + // Fragile browser-specific ordering issues below + +/*@cc_on + // prevent extra clicky noises on IE + iframe.src = selectorUrl; +@*/ + + if (document.body.firstChild) { + document.body.insertBefore(iframe, document.body.firstChild); + } else { + document.body.appendChild(iframe); + } + +/*@cc_on + // prevent extra clicky noises on IE + return; +@*/ + + if (iframe.contentWindow) { + // Older Mozilla has a caching bug for the iframe and won't reload the nocache. + iframe.contentWindow.location.replace(selectorUrl); + } else { + // Older Safari doesn't have a contentWindow. + iframe.src = selectorUrl; + } + } +} + +/** + * Runs the entry point for each module. + */ +ModuleControlBlocks.prototype.run = function() { + for (var i = 0, n = this.blocks_.length; i < n; ++i) { + var mcb = this.blocks_[i]; + var name = mcb.getName(); + var frameWnd = mcb.getModuleFrameWindow(); + if (__gwt_isHosted()) { + if (!window.external.gwtOnLoad(frameWnd, name)) { + // Module failed to load. + if (__gwt_onLoadError) { + __gwt_onLoadError(name); + } else { + window.alert("Failed to load module '" + name + + "'.\nPlease see the log in the development shell for details."); + } + } + } else { + // The compilation itself handles calling the error function. + frameWnd.gwtOnLoad(__gwt_onLoadError, name); + } + } +} + +////////////////////////////////////////////////////////////////////////////// +// Globals +// + +var __gwt_retryWaitMillis = 10; +var __gwt_isHostPageLoaded = false; +var __gwt_metaProps = {}; +var __gwt_onPropertyError = null; +var __gwt_onLoadError = null; +var __gwt_moduleControlBlocks = new ModuleControlBlocks(); + +////////////////////////////////////////////////////////////////////////////// +// Common +// + +/** + * Determines whether or not the page is being loaded in the GWT hosted browser. + */ +function __gwt_isHosted() { + if (window.external && window.external.gwtOnLoad) { + // gwt.hybrid makes the hosted browser pretend not to be + if (document.location.href.indexOf("gwt.hybrid") == -1) { + return true; + } + } + return false; +} + +/** + * Tries to get a module control block based on a query string passed in from + * the caller. Used by iframes to get references back to their mcbs. + * @param queryString the entire query string as returned by location.search, + * which notably includes the leading '?' if one is specified + * @return the relevant module control block, or null if it cannot + * be derived based on queryString + */ +function __gwt_tryGetModuleControlBlock(queryString) { + if (queryString.length > 0) { + // The pattern is ?[h&][&] + var queryString = queryString.substring(1); + if (queryString.indexOf("h&") == 0) { + // Ignore the hosted mode flag here; only GWTShellServlet cares about it. + queryString = queryString.substring(2); + } + var pos = queryString.indexOf("&"); + if (pos >= 0) { + queryString = queryString.substring(0, pos); + } + var mcbIndex = parseInt(queryString); + if (!isNaN(mcbIndex)) { + var mcb = __gwt_moduleControlBlocks.get(mcbIndex); + return mcb; + } + // Ignore the unique number that remains on the query string. + } + return null; +} + +/** + * Parses meta tags from the host html. + * + * + * causes the specified module to be loaded + * + * + * statically defines a deferred binding client property + * + * + * specifies the name of a function to call if a client property is set to + * an invalid value (meaning that no matching compilation will be found) + * + * + * specifies the name of a function to call if an exception happens during + * bootstrapping or if a module throws an exception out of onModuleLoad(); + * the function should take a message parameter + */ +function __gwt_processMetas() { + var metas = document.getElementsByTagName("meta"); + for (var i = 0, n = metas.length; i < n; ++i) { + var meta = metas[i]; + var name = meta.getAttribute("name"); + if (name) { + if (name == "gwt:module") { + var moduleName = meta.getAttribute("content"); + if (moduleName) { + __gwt_moduleControlBlocks.add(meta, moduleName); + } + } else if (name == "gwt:property") { + var content = meta.getAttribute("content"); + if (content) { + var name = content, value = ""; + var eq = content.indexOf("="); + if (eq != -1) { + name = content.substring(0, eq); + value = content.substring(eq+1); + } + __gwt_metaProps[name] = value; + } + } else if (name == "gwt:onPropertyErrorFn") { + var content = meta.getAttribute("content"); + if (content) { + try { + __gwt_onPropertyError = eval(content); + } catch (e) { + window.alert("Bad handler \"" + content + + "\" for \"gwt:onPropertyErrorFn\""); + } + } + } else if (name == "gwt:onLoadErrorFn") { + var content = meta.getAttribute("content"); + if (content) { + try { + __gwt_onLoadError = eval(content); + } catch (e) { + window.alert("Bad handler \"" + content + + "\" for \"gwt:onLoadErrorFn\""); + } + } + } + } + } +} + +/** + * Determines the value of a deferred binding client property specified + * statically in host html. + */ +function __gwt_getMetaProperty(name) { + var value = __gwt_metaProps[name]; + if (value) { + return value; + } else { + return null; + } +} + +/** + * Determines whether or not a particular property value is allowed. + * @param wnd the caller's window object (not $wnd!) + * @param propName the name of the property being checked + * @param propValue the property value being tested + */ +function __gwt_isKnownPropertyValue(wnd, propName, propValue) { + return propValue in wnd["values$" + propName]; +} + +/** + * Called by the selection script when a property has a bad value or is missing. + * 'allowedValues' is an array of strings. Can be hooked in the host page using + * gwt:onPropertyErrorFn. + */ +function __gwt_onBadProperty(moduleName, propName, allowedValues, badValue) { + if (__gwt_onPropertyError) { + __gwt_onPropertyError(moduleName, propName, allowedValues, badValue); + return; + } else { + var msg = "While attempting to load module \"" + moduleName + "\", "; + if (badValue != null) { + msg += "property \"" + propName + "\" was set to the unexpected value \"" + + badValue + "\""; + } else { + msg += "property \"" + propName + "\" was not specified"; + } + + msg += "\n\nAllowed values: " + allowedValues; + + window.alert(msg); + } +} + +/** + * Called directly from compiled code. + */ +function __gwt_initHandlers(resize, beforeunload, unload) { + var oldOnResize = window.onresize; + window.onresize = function() { + resize(); + if (oldOnResize) + oldOnResize(); + }; + + var oldOnBeforeUnload = window.onbeforeunload; + window.onbeforeunload = function() { + var ret = beforeunload(); + + var oldRet; + if (oldOnBeforeUnload) + oldRet = oldOnBeforeUnload(); + + if (ret !== null) + return ret; + return oldRet; + }; + + var oldOnUnload = window.onunload; + window.onunload = function() { + unload(); + if (oldOnUnload) + oldOnUnload(); + }; +} + +////////////////////////////////////////////////////////////////////////////// +// Hosted Mode +// +function __gwt_onUnloadHostedMode() { + window.external.gwtOnLoad(null, null); + if (__gwt_onUnloadHostedMode.oldUnloadHandler) { + __gwt_onUnloadHostedMode.oldUnloadHandler(); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Bootstrap +// + +/** + * Waits until all startup preconditions are satisfied, then launches the + * user-defined startup code for each module. + */ +function __gwt_latchAndLaunch() { + var ready = true; + + // Are there any compilations still pending? + if (ready && !__gwt_moduleControlBlocks.isReady()) { + // Yes, we're still waiting on one or more compilations. + ready = false; + } + + // Has the host html onload event fired? + if (ready && !__gwt_isHostPageLoaded) { + // No, the host html page hasn't fully loaded. + ready = false; + } + + // Are we ready to run user code? + if (ready) { + // Yes: run entry points. + __gwt_moduleControlBlocks.run(); + } else { + // No: try again soon. + window.setTimeout(__gwt_latchAndLaunch, __gwt_retryWaitMillis); + } +} + +/** + * Starts the module-loading sequence after meta tags have been processed and + * the body element exists. + */ +function __gwt_loadModules() { + // Make sure the body element exists before starting. + if (!document.body) { + // Try again soon. + window.setTimeout(__gwt_loadModules, __gwt_retryWaitMillis); + return; + } + + // Inject a frame for each module. + __gwt_moduleControlBlocks.injectFrames(); + + // Try to launch module entry points once everything is ready. + __gwt_latchAndLaunch(); +} + +/** + * The very first thing to run, and it runs exactly once unconditionally. + */ +function __gwt_bootstrap() { + // Hook onunload for hosted mode. + if (__gwt_isHosted()) { + __gwt_onUnloadHostedMode.oldUnloadHandler = window.onunload; + window.onunload = __gwt_onUnloadHostedMode; + } + + // Hook the current window onload handler. + var oldHandler = window.onload; + window.onload = function() { + __gwt_isHostPageLoaded = true; + if (oldHandler) { + oldHandler(); + } + }; + + // Parse meta tags from host html. + __gwt_processMetas(); + + // Load any modules. + __gwt_loadModules(); +} + +// Go. +__gwt_bootstrap(); diff --git a/webui/www/com.WebUI.WebUIApp/history.html b/webui/www/com.WebUI.WebUIApp/history.html new file mode 100644 index 000000000..409eaf2dc --- /dev/null +++ b/webui/www/com.WebUI.WebUIApp/history.html @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/webui/www/com.WebUI.WebUIApp/tree_closed.gif b/webui/www/com.WebUI.WebUIApp/tree_closed.gif new file mode 100644 index 0000000000000000000000000000000000000000..1348f450c3a551ec6be38b327a8a8f60a38253bc GIT binary patch literal 82 zcmZ?wbhEHb6krfwn8?I%=FAxe28RFt|1&T!DE?$&WME)o&|zR;U;xQ8Fey*zUwQg% le#4w%#%^DW@Y0R05e}0MwbwkH*%&iBN`&>QSqKAzH2}e{8Ik}1 literal 0 HcmV?d00001 diff --git a/webui/www/com.WebUI.WebUIApp/tree_open.gif b/webui/www/com.WebUI.WebUIApp/tree_open.gif new file mode 100644 index 0000000000000000000000000000000000000000..c1ff81f40fc28b6dd7d9928c2175f8c8d102bb4f GIT binary patch literal 78 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