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 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ξ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 literal 0 HcmV?d00001