Pulled in and merged most recent revision with iframes-on-demand changes.
41
NEWS
|
@ -1,6 +1,45 @@
|
|||
1.5.9:
|
||||
1.5.13: January ??, 2009
|
||||
* Fixed various bugs related to the subscription page.
|
||||
* Switching between notebooks and loading notebooks is now even faster.
|
||||
|
||||
1.5.12: December 30, 2008
|
||||
* Fixed a bug in which clicking on the notebook rename text field ended the
|
||||
renaming prematurely.
|
||||
* Potential fix for a bug in which product downloads and attached file
|
||||
downloads occasionally did not complete in Internet Explorer.
|
||||
* Added a 30-day free trial to all Luminotes subscription plans, and updated
|
||||
the pricing page accordingly.
|
||||
|
||||
1.5.11: December 27, 2008
|
||||
* Added a font selection button to the toolbar.
|
||||
* Decreased the default note text font size, so now you can see more of your
|
||||
note text at once.
|
||||
* Added rounded corners to several display elements.
|
||||
* Improved the layout on low-resolution displays (1024x768 and below).
|
||||
* Fixed a Luminotes Desktop bug in which creating and then clicking on a new
|
||||
note link sometimes caused a red error message.
|
||||
* Fixed a bug in which yellow pulldowns that were opened towards the bottom
|
||||
of the page appeared partially off the page.
|
||||
* Fixed a bug in which forum post permalinks didn't work on posts after the
|
||||
first ten in a particular thread.
|
||||
|
||||
1.5.10: December 4, 2008
|
||||
* Fixed a bug in which certain new installations of Luminotes Desktop
|
||||
on Windows yielded an "uh oh" error on initial launch. This bug did
|
||||
not occur during upgrades. It only affected new installations.
|
||||
|
||||
1.5.9: December 3, 2008
|
||||
* When you hover the mouse over a link and a link pulldown appears, that
|
||||
pulldown will now automatically disappear soon after you move the mouse
|
||||
away.
|
||||
* Changed the "new note" key from ctrl-N to ctrl-M so as not to conflict with
|
||||
the "new browser window" key used in most web browsers.
|
||||
* Fixed a Chrome/Safari bug in which ending a link didn't always work.
|
||||
* Fixed a rare Chrome/Safari bug in which pressing backspace sometimes made
|
||||
the text cursor vanish.
|
||||
* Fixed an Internet Explorer bug in which backspace sometimes didn't work,
|
||||
such as when backspacing an empty list element.
|
||||
|
||||
1.5.8: November 24, 2008
|
||||
* Fixed a bug that prevented notes from being automatically saved in certain
|
||||
notebooks.
|
||||
|
|
3
README
|
@ -27,7 +27,8 @@ click "Keep Blocking".
|
|||
|
||||
* File storage: In case you're curious, your notes are stored within the
|
||||
%APPDATA%\Luminotes folder in a database file. Attached files are stored with
|
||||
the %APPDATA\Luminotes\files folder.
|
||||
the %APPDATA\Luminotes\files folder. (It is not recommended that you modify
|
||||
or copy these files.)
|
||||
|
||||
* USB drive: If you'd like to run Luminotes from a USB drive, then when the
|
||||
Luminotes Desktop installer prompts you to select the installation destination
|
||||
|
|
|
@ -26,7 +26,7 @@ settings = {
|
|||
"session_filter.storage_type": "ram",
|
||||
"session_filter.timeout": 60 * 24 * 365, # one year
|
||||
"static_filter.root": os.getcwd(),
|
||||
"server.log_to_screen": False,
|
||||
"server.log_to_screen": True,
|
||||
"server.log_file": os.path.join( gettempdir(), "luminotes_error%s.log" % username_postfix ),
|
||||
"server.log_access_file": os.path.join( gettempdir(), "luminotes%s.log" % username_postfix ),
|
||||
"server.log_tracebacks": True,
|
||||
|
|
|
@ -1 +1 @@
|
|||
VERSION = u"1.5.9"
|
||||
VERSION = u"1.5.13"
|
||||
|
|
|
@ -69,11 +69,17 @@ class Database( object ):
|
|||
from pytz import utc
|
||||
|
||||
TIMESTAMP_PATTERN = re.compile( "^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d).(\d+)(?:\+\d\d:\d\d$)?" )
|
||||
MICROSECONDS_PER_SECOND = 1000000
|
||||
|
||||
def convert_timestamp( value ):
|
||||
( year, month, day, hours, minutes, seconds, fractional_seconds ) = \
|
||||
TIMESTAMP_PATTERN.search( value ).groups( 0 )
|
||||
microseconds = int( float ( "0." + fractional_seconds ) * 1000000 )
|
||||
|
||||
# convert fractional seconds (with an arbitrary number of decimal places) to microseconds
|
||||
microseconds = int( fractional_seconds )
|
||||
while microseconds > MICROSECONDS_PER_SECOND:
|
||||
fractional_seconds = fractional_seconds[ : -1 ]
|
||||
microseconds = int( fractional_seconds or 0 )
|
||||
|
||||
# ignore time zone in timestamp and assume UTC
|
||||
return datetime(
|
||||
|
|
|
@ -15,7 +15,7 @@ from Expose import expose
|
|||
from Validate import validate, Valid_int, Valid_bool, Validation_error
|
||||
from Database import Valid_id, end_transaction
|
||||
from Users import grab_user_id, Access_error
|
||||
from Expire import strongly_expire
|
||||
from Expire import strongly_expire, weakly_expire
|
||||
from model.File import File
|
||||
from model.User import User
|
||||
from model.Notebook import Notebook
|
||||
|
@ -262,6 +262,7 @@ class Files( object ):
|
|||
self.__download_products = download_products
|
||||
|
||||
@expose()
|
||||
@weakly_expire
|
||||
@end_transaction
|
||||
@grab_user_id
|
||||
@validate(
|
||||
|
@ -323,6 +324,7 @@ class Files( object ):
|
|||
return stream()
|
||||
|
||||
@expose()
|
||||
@weakly_expire
|
||||
@end_transaction
|
||||
@validate(
|
||||
access_id = Valid_id(),
|
||||
|
@ -409,6 +411,7 @@ class Files( object ):
|
|||
)
|
||||
|
||||
@expose()
|
||||
@weakly_expire
|
||||
@end_transaction
|
||||
@grab_user_id
|
||||
@validate(
|
||||
|
@ -471,6 +474,7 @@ class Files( object ):
|
|||
return stream( image_buffer )
|
||||
|
||||
@expose()
|
||||
@weakly_expire
|
||||
@end_transaction
|
||||
@grab_user_id
|
||||
@validate(
|
||||
|
|
|
@ -212,7 +212,11 @@ class Forum( object ):
|
|||
|
||||
# if a single note was requested, just return that one note
|
||||
if note_id:
|
||||
result[ "notes" ] = [ note for note in result[ "notes" ] if note.object_id == note_id ]
|
||||
note = self.__database.load( Note, note_id )
|
||||
if note:
|
||||
result[ "notes" ] = [ note ]
|
||||
else:
|
||||
result[ "notes" ] = []
|
||||
|
||||
return result
|
||||
|
||||
|
|
|
@ -463,7 +463,7 @@ class Users( object ):
|
|||
)
|
||||
|
||||
return dict(
|
||||
form = button % user_id,
|
||||
form = button % ( user_id, 0 ) # 0 = new subscription, 1 = modify an existing subscription
|
||||
)
|
||||
|
||||
@expose()
|
||||
|
@ -1450,7 +1450,12 @@ class Users( object ):
|
|||
if mc_gross and mc_gross not in ( fee, yearly_fee ):
|
||||
raise Payment_error( u"invalid mc_gross", params )
|
||||
|
||||
# verify mc_amount3
|
||||
# verify mc_amount1 (free 30-day trial)
|
||||
mc_amount1 = params.get( u"mc_amount1" )
|
||||
if mc_amount1 and mc_amount1 != "0.00":
|
||||
raise Payment_error( u"invalid mc_amount1", params )
|
||||
|
||||
# verify mc_amount3 (actual payment)
|
||||
mc_amount3 = params.get( u"mc_amount3" )
|
||||
if mc_amount3 and mc_amount3 not in ( fee, yearly_fee ):
|
||||
raise Payment_error( u"invalid mc_amount3", params )
|
||||
|
@ -1460,9 +1465,14 @@ class Users( object ):
|
|||
if item_name and item_name.lower() != u"luminotes " + rate_plan[ u"name" ].lower():
|
||||
raise Payment_error( u"invalid item_name", params )
|
||||
|
||||
# verify period1 and period2 (should not be present)
|
||||
if params.get( u"period1" ) or params.get( u"period2" ):
|
||||
raise Payment_error( u"invalid period", params )
|
||||
# verify period1 (free 30-day trial)
|
||||
period1 = params.get( u"period1" )
|
||||
if period1 and period1 != "30 D":
|
||||
raise Payment_error( u"invalid period1", params )
|
||||
|
||||
# verify period2 (should not be present)
|
||||
if params.get( u"period2" ):
|
||||
raise Payment_error( u"invalid period2", params )
|
||||
|
||||
# verify period3
|
||||
period3 = params.get( u"period3" )
|
||||
|
@ -1509,7 +1519,7 @@ class Users( object ):
|
|||
self.__database.save( user, commit = False )
|
||||
self.update_groups( user )
|
||||
self.__database.commit()
|
||||
elif txn_type in ( u"subscr_payment", u"subscr_failed" ):
|
||||
elif txn_type in ( u"subscr_payment", u"subscr_failed", "subscr_eot" ):
|
||||
pass # for now, ignore payments and let paypal handle them
|
||||
else:
|
||||
raise Payment_error( "unknown txn_type", params )
|
||||
|
@ -1588,15 +1598,18 @@ class Users( object ):
|
|||
|
||||
# if there's no rate plan or we've retried too many times, give up and display an error
|
||||
RETRY_TIMEOUT = 15
|
||||
if rate_plan is None or retry_count > RETRY_TIMEOUT:
|
||||
if retry_count > RETRY_TIMEOUT:
|
||||
note = Thanks_error_note()
|
||||
# if the rate plan of the subscription matches the user's current rate plan, success
|
||||
elif rate_plan == result[ u"user" ].rate_plan:
|
||||
note = Thanks_note( self.__rate_plans[ rate_plan ][ u"name" ].capitalize() )
|
||||
result[ "conversion" ] = "subscribe_%s" % rate_plan
|
||||
# otherwise, display an auto-reloading "processing..." page
|
||||
else:
|
||||
# if a rate plan is given, display an auto-reloading "processing..." page
|
||||
elif rate_plan is not None:
|
||||
note = Processing_note( rate_plan, retry_count )
|
||||
# otherwise, assume that this is a free trial and default to a generic thanks page
|
||||
else:
|
||||
note = Thanks_note()
|
||||
|
||||
result[ "notebook" ] = main_notebook
|
||||
result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() )
|
||||
|
|
|
@ -80,8 +80,8 @@ class Test_controller( object ):
|
|||
u"included_users": 1,
|
||||
u"fee": 1.99,
|
||||
u"yearly_fee": 19.90,
|
||||
u"button": u"[subscribe here user %s!] button",
|
||||
u"yearly_button": u"[yearly subscribe here user %s!] button",
|
||||
u"button": u"[subscribe here user %s!] button (modify=%s)",
|
||||
u"yearly_button": u"[yearly subscribe here user %s!] button (modify=%s)",
|
||||
},
|
||||
{
|
||||
u"name": "extra super",
|
||||
|
@ -91,8 +91,8 @@ class Test_controller( object ):
|
|||
u"included_users": 3,
|
||||
u"fee": 9.00,
|
||||
u"yearly_fee": 90.00,
|
||||
u"button": u"[or here user %s!] button",
|
||||
u"yearly_button": u"[yearly or here user %s!] button",
|
||||
u"button": u"[or here user %s!] button (modify=%s)",
|
||||
u"yearly_button": u"[yearly or here user %s!] button (modify=%s)",
|
||||
},
|
||||
],
|
||||
"luminotes.download_products": [
|
||||
|
|
|
@ -66,6 +66,40 @@ class Test_database( object ):
|
|||
assert obj.revision.replace( tzinfo = utc ) == original_revision
|
||||
assert obj.value == basic_obj.value
|
||||
|
||||
def test_select_datetime( self ):
|
||||
# this revision (with .504099) happens to test for a bug caused by floating point rounding errors
|
||||
original_revision = "2008-01-01 01:00:42.504099+00:00"
|
||||
basic_obj = Stub_object( object_id = "5", revision = original_revision, value = 1 )
|
||||
|
||||
self.database.save( basic_obj )
|
||||
obj = self.database.select_one( Stub_object, Stub_object.sql_load( basic_obj.object_id ) )
|
||||
|
||||
assert obj.object_id == basic_obj.object_id
|
||||
assert str( obj.revision.replace( tzinfo = utc ) ) == original_revision
|
||||
assert obj.value == basic_obj.value
|
||||
|
||||
def test_select_datetime_with_many_fractional_digits( self ):
|
||||
original_revision = "2008-01-01 01:00:42.5032429489284+00:00"
|
||||
basic_obj = Stub_object( object_id = "5", revision = original_revision, value = 1 )
|
||||
|
||||
self.database.save( basic_obj )
|
||||
obj = self.database.select_one( Stub_object, Stub_object.sql_load( basic_obj.object_id ) )
|
||||
|
||||
assert obj.object_id == basic_obj.object_id
|
||||
assert str( obj.revision.replace( tzinfo = utc ) ) == "2008-01-01 01:00:42.503242+00:00"
|
||||
assert obj.value == basic_obj.value
|
||||
|
||||
def test_select_datetime_with_zero_fractional_seconds( self ):
|
||||
original_revision = "2008-01-01 01:00:42.0+00:00"
|
||||
basic_obj = Stub_object( object_id = "5", revision = original_revision, value = 1 )
|
||||
|
||||
self.database.save( basic_obj )
|
||||
obj = self.database.select_one( Stub_object, Stub_object.sql_load( basic_obj.object_id ) )
|
||||
|
||||
assert obj.object_id == basic_obj.object_id
|
||||
assert str( obj.revision.replace( tzinfo = utc ) ) == "2008-01-01 01:00:42+00:00"
|
||||
assert obj.value == basic_obj.value
|
||||
|
||||
def test_select_one_tuple( self ):
|
||||
obj = self.database.select_one( tuple, Stub_object.sql_tuple() )
|
||||
|
||||
|
@ -185,7 +219,7 @@ class Test_database( object ):
|
|||
self.connection.rollback()
|
||||
assert self.database.load( Stub_object, next_id ) == None
|
||||
|
||||
def test_next_id_with_explit_commit( self ):
|
||||
def test_next_id_with_explicit_commit( self ):
|
||||
next_id = self.database.next_id( Stub_object, commit = False )
|
||||
self.database.commit()
|
||||
assert next_id
|
||||
|
|
|
@ -268,6 +268,54 @@ class Test_forums( Test_controller ):
|
|||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.storage_bytes == 0
|
||||
|
||||
def test_general_thread_default_with_unknown_note_id( self ):
|
||||
result = self.http_get( "/forums/general/%s?note_id=unknownid" % self.general_thread.object_id )
|
||||
|
||||
assert result.get( u"user" ).object_id == self.anonymous.object_id
|
||||
assert len( result.get( u"notebooks" ) ) == 4
|
||||
assert result.get( u"notebooks" )[ 0 ].object_id == self.anon_notebook.object_id
|
||||
assert result.get( u"login_url" )
|
||||
assert result.get( u"logout_url" )
|
||||
assert result.get( u"rate_plan" )
|
||||
assert result.get( u"notebook" ).object_id == self.general_thread.object_id
|
||||
assert len( result.get( u"startup_notes" ) ) == 0
|
||||
assert result.get( u"notes" ) == []
|
||||
assert result.get( u"parent_id" ) == None
|
||||
assert result.get( u"note_read_write" ) in ( None, True )
|
||||
assert result.get( u"total_notes_count" ) == 0
|
||||
|
||||
invites = result[ "invites" ]
|
||||
assert len( invites ) == 0
|
||||
|
||||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.storage_bytes == 0
|
||||
|
||||
def test_general_thread_default_with_note_id( self ):
|
||||
self.__make_notes()
|
||||
|
||||
result = self.http_get( "/forums/general/%s?note_id=%s" % ( self.general_thread.object_id, self.note.object_id ) )
|
||||
|
||||
assert result.get( u"user" ).object_id == self.anonymous.object_id
|
||||
assert len( result.get( u"notebooks" ) ) == 4
|
||||
assert result.get( u"notebooks" )[ 0 ].object_id == self.anon_notebook.object_id
|
||||
assert result.get( u"login_url" )
|
||||
assert result.get( u"logout_url" )
|
||||
assert result.get( u"rate_plan" )
|
||||
assert result.get( u"notebook" ).object_id == self.general_thread.object_id
|
||||
assert len( result.get( u"startup_notes" ) ) == 3
|
||||
assert len( result.get( u"notes" ) ) == 1
|
||||
assert result.get( u"notes" )[ 0 ].object_id == self.note.object_id
|
||||
assert result[ u"notes" ][ 0 ].title == u"foo"
|
||||
assert result.get( u"parent_id" ) == None
|
||||
assert result.get( u"note_read_write" ) in ( None, True )
|
||||
assert result.get( u"total_notes_count" ) == 3
|
||||
|
||||
invites = result[ "invites" ]
|
||||
assert len( invites ) == 0
|
||||
|
||||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.storage_bytes == 0
|
||||
|
||||
def test_general_thread_default_with_login( self ):
|
||||
self.login()
|
||||
|
||||
|
|
|
@ -542,7 +542,7 @@ class Test_users( Test_controller ):
|
|||
form = result.get( u"form" )
|
||||
plan = self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ]
|
||||
|
||||
assert form == plan[ u"button" ] % self.user.object_id
|
||||
assert form == plan[ u"button" ] % ( self.user.object_id, 0 )
|
||||
|
||||
def test_subscribe_yearly( self ):
|
||||
self.login()
|
||||
|
@ -555,7 +555,7 @@ class Test_users( Test_controller ):
|
|||
form = result.get( u"form" )
|
||||
plan = self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ]
|
||||
|
||||
assert form == plan[ u"yearly_button" ] % self.user.object_id
|
||||
assert form == plan[ u"yearly_button" ] % ( self.user.object_id, 0 )
|
||||
|
||||
def test_subscribe_with_free_rate_plan( self ):
|
||||
self.login()
|
||||
|
@ -3351,6 +3351,43 @@ class Test_users( Test_controller ):
|
|||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.rate_plan == 0
|
||||
|
||||
def test_paypal_notify_payment_with_trial( self ):
|
||||
data = dict( self.PAYMENT_DATA )
|
||||
data[ u"custom" ] = self.user.object_id
|
||||
data[ u"mc_amount1" ] = u"0.00"
|
||||
data[ u"period1" ] = u"30 D"
|
||||
result = self.http_post( "/users/paypal_notify", data );
|
||||
|
||||
assert len( result ) == 1
|
||||
assert result.get( u"session_id" )
|
||||
assert Stub_urllib2.result == u"VERIFIED"
|
||||
assert Stub_urllib2.headers.get( u"Content-type" ) == u"application/x-www-form-urlencoded"
|
||||
assert Stub_urllib2.url.startswith( "https://" )
|
||||
assert u"paypal.com" in Stub_urllib2.url
|
||||
assert Stub_urllib2.encoded_params
|
||||
|
||||
# being notified of a mere payment should not change the user's rate plan
|
||||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.rate_plan == 0
|
||||
|
||||
def test_paypal_notify_payment_with_trial_invalid_period( self ):
|
||||
data = dict( self.PAYMENT_DATA )
|
||||
data[ u"custom" ] = self.user.object_id
|
||||
data[ u"mc_amount1" ] = u"0.00"
|
||||
data[ u"period1" ] = u"31 D"
|
||||
result = self.http_post( "/users/paypal_notify", data );
|
||||
|
||||
assert result.get( u"error" )
|
||||
|
||||
def test_paypal_notify_payment_with_trial_invalid_amount( self ):
|
||||
data = dict( self.PAYMENT_DATA )
|
||||
data[ u"custom" ] = self.user.object_id
|
||||
data[ u"mc_amount1" ] = u"2.50"
|
||||
data[ u"period1" ] = u"30 D"
|
||||
result = self.http_post( "/users/paypal_notify", data );
|
||||
|
||||
assert result.get( u"error" )
|
||||
|
||||
def test_paypal_notify_payment_invalid( self ):
|
||||
data = dict( self.PAYMENT_DATA )
|
||||
data[ u"custom" ] = self.user.object_id
|
||||
|
@ -4962,7 +4999,7 @@ class Test_users( Test_controller ):
|
|||
assert result[ u"notes" ][ 0 ].title == u"thank you"
|
||||
assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
|
||||
assert u"Thank you" in result[ u"notes" ][ 0 ].contents
|
||||
assert u"confirmation" in result[ u"notes" ][ 0 ].contents
|
||||
assert u"confirmation" not in result[ u"notes" ][ 0 ].contents
|
||||
|
||||
def test_thanks_download( self ):
|
||||
access_id = u"wheeaccessid"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
DROP VIEW notebook_current;
|
||||
CREATE VIEW notebook_current AS
|
||||
SELECT id, revision, name, trash_id, deleted, user_id FROM notebook WHERE (notebook.revision IN (SELECT max(sub_notebook.revision) AS max FROM notebook sub_notebook WHERE (sub_notebook.id = notebook.id))) and notebook.name is not null;
|
|
@ -104,7 +104,7 @@ CREATE TABLE notebook (
|
|||
);
|
||||
|
||||
CREATE VIEW notebook_current AS
|
||||
SELECT id, revision, name, trash_id, deleted, user_id FROM notebook WHERE (notebook.revision IN (SELECT max(sub_notebook.revision) AS max FROM notebook sub_notebook WHERE (sub_notebook.id = notebook.id)));
|
||||
SELECT id, revision, name, trash_id, deleted, user_id FROM notebook WHERE (notebook.revision IN (SELECT max(sub_notebook.revision) AS max FROM notebook sub_notebook WHERE (sub_notebook.id = notebook.id))) and notebook.name is not null;
|
||||
|
||||
CREATE TABLE password_reset (
|
||||
id text NOT NULL,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
body {
|
||||
padding: 1em;
|
||||
font-size: 90%;
|
||||
background-color: #fafafa;
|
||||
line-height: 140%;
|
||||
font-family: sans-serif;
|
||||
|
@ -13,6 +14,7 @@ body {
|
|||
}
|
||||
|
||||
.note_frame {
|
||||
-moz-border-radius: 5px;
|
||||
text-align: left;
|
||||
margin: 0em;
|
||||
padding: 1.5em;
|
||||
|
|
|
@ -71,3 +71,7 @@
|
|||
margin-bottom: 0.25em;
|
||||
padding: 0.25em 0.25em 0 0.5em;
|
||||
}
|
||||
|
||||
#current_notebook_wrapper {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
body {
|
||||
padding: 1em;
|
||||
font-size: 90%;
|
||||
line-height: 140%;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
@ -128,7 +129,7 @@ ol li {
|
|||
|
||||
.small_text {
|
||||
padding-top: 0.5em;
|
||||
font-size: 72%;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.radio_label {
|
||||
|
|
|
@ -40,10 +40,10 @@
|
|||
background-image: url(/static/images/toolbar/strikethrough_button.png);
|
||||
}
|
||||
|
||||
#title_button_preload {
|
||||
#font_button_preload {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
background-image: url(/static/images/toolbar/title_button.png);
|
||||
background-image: url(/static/images/toolbar/font_button.png);
|
||||
}
|
||||
|
||||
#bullet_list_button_preload {
|
||||
|
@ -65,6 +65,8 @@
|
|||
}
|
||||
|
||||
.hook_area {
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
padding-top: 1.5em;
|
||||
padding-bottom: 1.5em;
|
||||
width: 100%;
|
||||
|
@ -93,6 +95,8 @@
|
|||
background-color: #ffff99;
|
||||
font-weight: bold;
|
||||
padding: 1em;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.hook_action {
|
||||
|
@ -156,6 +160,8 @@
|
|||
}
|
||||
|
||||
.thumbnail_area {
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
background-color: #fffece;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
@ -301,6 +307,10 @@
|
|||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.lighter_text {
|
||||
color: #267dff;
|
||||
}
|
||||
|
||||
.upgrade_question {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -322,6 +332,12 @@
|
|||
|
||||
.upgrade_table_area {
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.luminotes_online_link_area {
|
||||
margin-top: 1em;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#upgrade_table {
|
||||
|
@ -337,43 +353,80 @@
|
|||
}
|
||||
|
||||
#upgrade_table th {
|
||||
padding: 0.5em;
|
||||
padding: 0.5em 1.5em 0.5em 1.5em;
|
||||
border: 1px solid #999999;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
}
|
||||
|
||||
.plan_width {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.download_plan_width {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
#upgrade_table td {
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
padding: 0.5em;
|
||||
border: 1px solid #999999;
|
||||
padding: 0.25em;
|
||||
border: none;
|
||||
border-left: 1px solid #999999;
|
||||
border-right: 1px solid #999999;
|
||||
}
|
||||
|
||||
#upgrade_table .plan_name_area {
|
||||
text-align: center;
|
||||
background-color: #d0e0f0;
|
||||
}
|
||||
|
||||
#upgrade_table .focused_plan_name_area {
|
||||
text-align: center;
|
||||
background-color: #dde6f0;
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
}
|
||||
|
||||
.plan_name_area a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.spacer_row {
|
||||
height: 0.5em;
|
||||
}
|
||||
|
||||
#upgrade_table .plan_name {
|
||||
text-align: center;
|
||||
background-color: #d0e0f0;
|
||||
font-size: 125%;
|
||||
}
|
||||
|
||||
#upgrade_table ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#upgrade_table .feature_name {
|
||||
font-size: 82%;
|
||||
text-align: left;
|
||||
background-color: #fafafa;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
#upgrade_table .feature_description {
|
||||
font-size: 82%;
|
||||
text-align: left;
|
||||
background-color: #fafafa;
|
||||
padding: 0.25em;
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
#upgrade_table .feature_value {
|
||||
font-size: 82%;
|
||||
font-size: 95%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#upgrade_table .focused_feature_value {
|
||||
background-color: #ffffff;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
}
|
||||
|
||||
#upgrade_table .focused_border_bottom {
|
||||
border-bottom: 2px solid #000000;
|
||||
}
|
||||
|
||||
#upgrade_table .focused_text {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #ff6600;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#upgrade_table_small {
|
||||
|
@ -386,26 +439,68 @@
|
|||
}
|
||||
|
||||
#upgrade_table_small th {
|
||||
padding: 0.5em;
|
||||
padding: 0.5em 1.5em 0.5em 1.5em;
|
||||
border: 1px solid #999999;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
}
|
||||
|
||||
#upgrade_table_small tr {
|
||||
border: 0px solid #999999;
|
||||
}
|
||||
|
||||
#upgrade_table_small td {
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
padding: 0.5em;
|
||||
padding: 0.25em;
|
||||
border: none;
|
||||
border-left: 1px solid #999999;
|
||||
border-right: 1px solid #999999;
|
||||
}
|
||||
|
||||
#upgrade_table_small .plan_name {
|
||||
#upgrade_table_small .plan_name_area {
|
||||
text-align: center;
|
||||
background-color: #d0e0f0;
|
||||
}
|
||||
|
||||
#upgrade_table_small .focused_plan_name_area {
|
||||
text-align: center;
|
||||
background-color: #dde6f0;
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
}
|
||||
|
||||
#upgrade_table_small .plan_name {
|
||||
font-size: 125%;
|
||||
}
|
||||
|
||||
#upgrade_table_small .feature_value {
|
||||
font-size: 82%;
|
||||
font-size: 95%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#upgrade_table_small .focused_feature_value {
|
||||
background-color: #ffffff;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
}
|
||||
|
||||
#upgrade_table_small .focused_border_bottom {
|
||||
border-bottom: 2px solid #000000;
|
||||
}
|
||||
|
||||
#upgrade_table_small .focused_text {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.subscribe_button_area {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.download_button_area {
|
||||
padding-top: 0.5em;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.yearly_link {
|
||||
|
@ -413,7 +508,8 @@
|
|||
}
|
||||
|
||||
.price_text {
|
||||
color: #ff6600;
|
||||
margin-top: 0.25em;
|
||||
color: #267dff;
|
||||
}
|
||||
|
||||
.month_text {
|
||||
|
@ -422,7 +518,7 @@
|
|||
}
|
||||
|
||||
.version_text {
|
||||
padding-top: 0.25em;
|
||||
padding-top: 0.5em;
|
||||
font-size: 72%;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
@ -432,10 +528,6 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sign_up_button_area {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.thumbnail_left {
|
||||
float: left;
|
||||
margin: 0.5em;
|
||||
|
|
|
@ -107,10 +107,10 @@ h1 {
|
|||
background-image: url(/static/images/toolbar/strikethrough_button_hover.png);
|
||||
}
|
||||
|
||||
#title_button_hover_preload {
|
||||
#font_button_hover_preload {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
background-image: url(/static/images/toolbar/title_button_hover.png);
|
||||
background-image: url(/static/images/toolbar/font_button_hover.png);
|
||||
}
|
||||
|
||||
#bullet_list_button_hover_preload {
|
||||
|
@ -167,10 +167,10 @@ h1 {
|
|||
background-image: url(/static/images/toolbar/strikethrough_button_down_hover.png);
|
||||
}
|
||||
|
||||
#title_button_down_hover_preload {
|
||||
#font_button_down_hover_preload {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
background-image: url(/static/images/toolbar/title_button_down_hover.png);
|
||||
background-image: url(/static/images/toolbar/font_button_down_hover.png);
|
||||
}
|
||||
|
||||
#bullet_list_button_down_hover_preload {
|
||||
|
@ -227,10 +227,10 @@ h1 {
|
|||
background-image: url(/static/images/toolbar/strikethrough_button_down.png);
|
||||
}
|
||||
|
||||
#title_button_down_preload {
|
||||
#font_button_down_preload {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
background-image: url(/static/images/toolbar/title_button_down.png);
|
||||
background-image: url(/static/images/toolbar/font_button_down.png);
|
||||
}
|
||||
|
||||
#bullet_list_button_down_preload {
|
||||
|
@ -427,6 +427,12 @@ h1 {
|
|||
color: #ff6600;
|
||||
}
|
||||
|
||||
#notebook_header_links {
|
||||
position: absolute;
|
||||
top: 1.7em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
#rename_form {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -493,6 +499,9 @@ h1 {
|
|||
}
|
||||
|
||||
.note_button {
|
||||
-moz-border-radius-topleft: 4px;
|
||||
-moz-border-radius-topright: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
border-style: outset;
|
||||
border-width: 0px;
|
||||
background-color: #d0e0f0;
|
||||
|
@ -511,9 +520,12 @@ h1 {
|
|||
|
||||
#save_button {
|
||||
margin-left: 0.5em;
|
||||
-moz-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
}
|
||||
|
||||
.note_frame {
|
||||
-moz-border-radius: 5px;
|
||||
margin: 0em;
|
||||
padding: 0em;
|
||||
overflow: hidden;
|
||||
|
@ -560,6 +572,7 @@ h1 {
|
|||
}
|
||||
|
||||
.pulldown {
|
||||
-moz-border-radius: 4px;
|
||||
position: absolute;
|
||||
font-size: 72%;
|
||||
text-align: left;
|
||||
|
@ -581,6 +594,10 @@ h1 {
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.selected_mark {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
padding: 0.25em 0.5em 0.25em 0.5em;
|
||||
}
|
||||
|
@ -590,16 +607,31 @@ h1 {
|
|||
}
|
||||
|
||||
.pulldown_label {
|
||||
-moz-user-select: none;
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.font_label_button {
|
||||
font-size: 125%;
|
||||
border-style: none;
|
||||
border-width: 0px;
|
||||
text-align: left;
|
||||
background-color: #ffff99;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pulldown_label:hover {
|
||||
color: #ff6600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message {
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: bold;
|
||||
|
@ -607,12 +639,16 @@ h1 {
|
|||
}
|
||||
|
||||
.message_inner {
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
line-height: 140%;
|
||||
background-color: #ffaa44;
|
||||
}
|
||||
|
||||
.error {
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
border: 1px solid #550000;
|
||||
margin-bottom: 0.5em;
|
||||
|
@ -622,6 +658,8 @@ h1 {
|
|||
}
|
||||
|
||||
.error_inner {
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
line-height: 140%;
|
||||
color: #ffffff;
|
||||
|
@ -629,6 +667,8 @@ h1 {
|
|||
}
|
||||
|
||||
.message_button {
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
margin-left: 0.5em;
|
||||
border-style: outset;
|
||||
border-width: 0px;
|
||||
|
@ -676,7 +716,7 @@ h1 {
|
|||
|
||||
.link_area_item {
|
||||
font-size: 75%;
|
||||
padding: 0.25em 0.25em 0.25em 0.5em;
|
||||
padding: 0.15em 0.25em 0.15em 0.5em;
|
||||
}
|
||||
|
||||
.note_tree_item {
|
||||
|
@ -872,6 +912,8 @@ h1 {
|
|||
}
|
||||
|
||||
.hook_action_area {
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
background-color: #ffff99;
|
||||
font-weight: bold;
|
||||
padding: 1em;
|
||||
|
|
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 963 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 954 B |
After Width: | Height: | Size: 587 B |
After Width: | Height: | Size: 695 B |
After Width: | Height: | Size: 684 B |
After Width: | Height: | Size: 582 B |
|
@ -393,6 +393,10 @@ Editor.prototype.insert_html = function ( html ) {
|
|||
}
|
||||
}
|
||||
|
||||
Editor.prototype.query_command_value = function ( command ) {
|
||||
return this.document.queryCommandValue( command );
|
||||
}
|
||||
|
||||
// resize the editor's frame to fit the dimensions of its content
|
||||
Editor.prototype.resize = function () {
|
||||
if ( !this.document ) return;
|
||||
|
@ -437,10 +441,14 @@ Editor.prototype.key_released = function ( event ) {
|
|||
Editor.prototype.cleanup_html = function ( key_code ) {
|
||||
if ( WEBKIT ) {
|
||||
// if enter is pressed while in a title, end title mode, since WebKit doesn't do that for us
|
||||
var ENTER = 13;
|
||||
var ENTER = 13; BACKSPACE = 8;
|
||||
if ( key_code == ENTER && this.state_enabled( "h3" ) )
|
||||
this.exec_command( "h3" );
|
||||
|
||||
// if backspace is pressed, skip WebKit style scrubbing since it can cause problems
|
||||
if ( key_code == BACKSPACE )
|
||||
return null;
|
||||
|
||||
// as of this writing, WebKit doesn't support execCommand( "styleWithCSS" ). for more info, see
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=13490
|
||||
// so to make up for this shortcoming, manually scrub WebKit style spans and other nodes,
|
||||
|
@ -462,8 +470,6 @@ Editor.prototype.cleanup_html = function ( key_code ) {
|
|||
continue;
|
||||
|
||||
var replacement = withDocument( this.document, function () {
|
||||
if ( style == undefined )
|
||||
return createDOM( "span" );
|
||||
// font-size is set when ending title mode
|
||||
if ( style.indexOf( "font-size: " ) != -1 )
|
||||
return null;
|
||||
|
@ -712,7 +718,9 @@ Editor.prototype.end_link = function () {
|
|||
// end of the link if it's not already there
|
||||
if ( link && WEBKIT ) {
|
||||
var selection = this.iframe.contentWindow.getSelection();
|
||||
selection.collapse( link, 1 );
|
||||
var sentinel = this.document.createTextNode( Editor.title_placeholder_char );
|
||||
insertSiblingNodesAfter( link, sentinel );
|
||||
selection.collapse( sentinel, 1 );
|
||||
}
|
||||
} else if ( this.document.selection ) { // browsers such as IE
|
||||
// if some text is already selected, unlink it and bail
|
||||
|
|
|
@ -330,6 +330,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri
|
|||
connect( "italic", "onclick", function ( event ) { self.toggle_button( event, "italic" ); } );
|
||||
connect( "underline", "onclick", function ( event ) { self.toggle_button( event, "underline" ); } );
|
||||
connect( "strikethrough", "onclick", function ( event ) { self.toggle_button( event, "strikethrough" ); } );
|
||||
connect( "font", "onclick", this, "toggle_font_button" );
|
||||
connect( "title", "onclick", function ( event ) { self.toggle_button( event, "title" ); } );
|
||||
connect( "insertUnorderedList", "onclick", function ( event ) { self.toggle_button( event, "insertUnorderedList" ); } );
|
||||
connect( "insertOrderedList", "onclick", function ( event ) { self.toggle_button( event, "insertOrderedList" ); } );
|
||||
|
@ -342,6 +343,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri
|
|||
this.make_image_button( "italic" );
|
||||
this.make_image_button( "underline" );
|
||||
this.make_image_button( "strikethrough" );
|
||||
this.make_image_button( "font" );
|
||||
this.make_image_button( "title" );
|
||||
this.make_image_button( "insertUnorderedList", "bullet_list" );
|
||||
this.make_image_button( "insertOrderedList", "numbered_list" );
|
||||
|
@ -1069,8 +1071,8 @@ Wiki.prototype.key_pressed = function ( event ) {
|
|||
|
||||
var code = event.key().code;
|
||||
if ( event.modifier().ctrl ) {
|
||||
// ctrl-n: new note
|
||||
if ( code == 78 )
|
||||
// ctrl-m: make a new note
|
||||
if ( code == 77 )
|
||||
this.create_blank_editor( event );
|
||||
}
|
||||
}
|
||||
|
@ -1112,8 +1114,8 @@ Wiki.prototype.editor_key_pressed = function ( editor, event ) {
|
|||
// ctrl-l: link
|
||||
} else if ( code == 76 ) {
|
||||
this.toggle_link_button( event );
|
||||
// ctrl-n: new note
|
||||
} else if ( code == 78 ) {
|
||||
// ctrl-m: make a new note
|
||||
} else if ( code == 77 ) {
|
||||
this.create_blank_editor( event );
|
||||
// ctrl-h: hide note
|
||||
} else if ( code == 72 ) {
|
||||
|
@ -1150,8 +1152,10 @@ Wiki.prototype.editor_key_pressed = function ( editor, event ) {
|
|||
} else if ( code == 8 && editor.document.selection ) {
|
||||
var range = editor.document.selection.createRange();
|
||||
range.moveStart( "character", -1 );
|
||||
range.text = "";
|
||||
event.stop();
|
||||
if ( range.text != "" ) {
|
||||
range.text = "";
|
||||
event.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1330,6 +1334,7 @@ Wiki.prototype.update_toolbar = function() {
|
|||
this.update_button( "italic", "i", node_names );
|
||||
this.update_button( "underline", "u", node_names );
|
||||
this.update_button( "strikethrough", "strike", node_names );
|
||||
this.update_button( "font", "font", node_names );
|
||||
this.update_button( "title", "h3", node_names );
|
||||
this.update_button( "insertUnorderedList", "ul", node_names );
|
||||
this.update_button( "insertOrderedList", "ol", node_names );
|
||||
|
@ -1405,6 +1410,30 @@ Wiki.prototype.toggle_attach_button = function ( event ) {
|
|||
event.stop();
|
||||
}
|
||||
|
||||
Wiki.prototype.toggle_font_button = function ( event ) {
|
||||
if ( this.focused_editor && this.focused_editor.read_write ) {
|
||||
this.focused_editor.focus();
|
||||
|
||||
// if a pulldown is already open, then just close it
|
||||
var existing_div = getElement( "font_pulldown" );
|
||||
|
||||
if ( existing_div ) {
|
||||
this.up_image_button( "font" );
|
||||
existing_div.pulldown.shutdown();
|
||||
existing_div.pulldown = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.down_image_button( "font" );
|
||||
this.clear_messages();
|
||||
this.clear_pulldowns();
|
||||
|
||||
new Font_pulldown( this, this.notebook.object_id, this.invoker, event.target(), this.focused_editor );
|
||||
}
|
||||
|
||||
event.stop();
|
||||
}
|
||||
|
||||
Wiki.prototype.hide_editor = function ( event, editor ) {
|
||||
this.clear_messages();
|
||||
this.clear_pulldowns();
|
||||
|
@ -2790,7 +2819,7 @@ Wiki.prototype.start_notebook_rename = function () {
|
|||
"form", { "id": "rename_form" }, notebook_name_field, ok_button
|
||||
);
|
||||
|
||||
replaceChildNodes( "notebook_header_area", rename_form );
|
||||
replaceChildNodes( "notebook_header_name", rename_form );
|
||||
|
||||
var self = this;
|
||||
connect( rename_form, "onsubmit", function ( event ) {
|
||||
|
@ -2801,6 +2830,9 @@ Wiki.prototype.start_notebook_rename = function () {
|
|||
self.end_notebook_rename();
|
||||
event.stop();
|
||||
} );
|
||||
connect( notebook_name_field, "onclick", function ( event ) {
|
||||
event.stop();
|
||||
} );
|
||||
|
||||
notebook_name_field.focus();
|
||||
notebook_name_field.select();
|
||||
|
@ -2971,11 +3003,15 @@ function Pulldown( wiki, notebook_id, pulldown_id, anchor, relative_to, ephemera
|
|||
|
||||
if ( this.ephemeral ) {
|
||||
// when the mouse cursor is moved into the pulldown, it becomes non-ephemeral (in other words,
|
||||
// it will no longer disappear in a few seconds)
|
||||
// it will no longer disappear in a few seconds). but as soon as the mouse leaves, it becomes
|
||||
// ephemeral again
|
||||
var self = this;
|
||||
connect( this.div, "onmouseover", function ( event ) {
|
||||
self.ephemeral = false;
|
||||
} );
|
||||
connect( this.div, "onmouseout", function ( event ) {
|
||||
self.ephemeral = true;
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3051,6 +3087,13 @@ function calculate_position( node, anchor, relative_to, always_left_align ) {
|
|||
Pulldown.prototype.update_position = function ( always_left_align ) {
|
||||
var position = calculate_position( this.div, this.anchor, this.relative_to, always_left_align );
|
||||
setElementPosition( this.div, position );
|
||||
|
||||
var div_height = getElementDimensions( this.div ).h;
|
||||
var viewport_bottom = getViewportPosition().y + getViewportDimensions().h;
|
||||
|
||||
// if the pulldown is now partially off the bottom of the window, move it up until it isn't
|
||||
if ( position.y + div_height > viewport_bottom )
|
||||
new Move( this.div, { "x": position.x, "y": viewport_bottom - div_height, "mode": "absolute", "duration": 0.25 } );
|
||||
}
|
||||
|
||||
Pulldown.prototype.shutdown = function () {
|
||||
|
@ -3302,10 +3345,16 @@ Link_pulldown.prototype.display_summary = function ( title, summary ) {
|
|||
}
|
||||
|
||||
Link_pulldown.prototype.title_field_clicked = function ( event ) {
|
||||
disconnectAll( this.div );
|
||||
this.ephemeral = false;
|
||||
|
||||
event.stop();
|
||||
}
|
||||
|
||||
Link_pulldown.prototype.title_field_focused = function ( event ) {
|
||||
disconnectAll( this.div );
|
||||
this.ephemeral = false;
|
||||
|
||||
this.title_field.select();
|
||||
}
|
||||
|
||||
|
@ -4184,6 +4233,89 @@ Suggest_pulldown.prototype.shutdown = function () {
|
|||
}
|
||||
|
||||
|
||||
function Font_pulldown( wiki, notebook_id, invoker, anchor, editor ) {
|
||||
anchor.pulldown = this;
|
||||
this.anchor = anchor;
|
||||
this.editor = editor;
|
||||
this.initial_selected_mark = null;
|
||||
|
||||
Pulldown.call( this, wiki, notebook_id, "font_pulldown", anchor );
|
||||
|
||||
this.invoker = invoker;
|
||||
|
||||
var fonts = [
|
||||
[ "Arial", "arial,sans-serif" ],
|
||||
[ "Times New Roman", "times new roman,serif" ],
|
||||
[ "Courier", "courier new,monospace" ],
|
||||
[ "Comic Sans", "comic sans ms,sans-serif" ],
|
||||
[ "Garamond", "garamond,serif" ],
|
||||
[ "Georgia", "georgia,serif" ],
|
||||
[ "Tahoma", "tahoma,sans-serif" ],
|
||||
[ "Trebuchet", "trebuchet ms,sans-serif" ],
|
||||
[ "Verdana", "verdana,sans-serif" ]
|
||||
];
|
||||
|
||||
var self = this;
|
||||
var current_font_family = editor.query_command_value( "fontname" );
|
||||
if ( current_font_family ) {
|
||||
current_font_family = current_font_family.toLowerCase();
|
||||
current_font_family = current_font_family.replace( /'/g, "" ).replace( /-webkit-/, "" );
|
||||
current_font_family = current_font_family.split( ',' )[ 0 ];
|
||||
}
|
||||
|
||||
for ( var i in fonts ) {
|
||||
var font = fonts[ i ];
|
||||
var font_name = font[ 0 ];
|
||||
var font_family = font[ 1 ];
|
||||
|
||||
// using a button here instead of a <label> to make IE happy: when a <label> is used, clicking
|
||||
// on the label steals focus from the editor iframe and prevents the font from being changed
|
||||
var label = createDOM( "input", { "type": "button", "value": font_name, "class": "pulldown_label font_label_button", "style": "font-family: " + font_family + ";" } );
|
||||
|
||||
var selected_mark_char = document.createTextNode( "\u25cf" );
|
||||
if ( current_font_family && font_family.search( current_font_family ) == 0 ) {
|
||||
var selected_mark = createDOM( "span", { "class": "selected_mark" }, selected_mark_char );
|
||||
this.initial_selected_mark = selected_mark;
|
||||
} else {
|
||||
var selected_mark = createDOM( "span", { "class": "selected_mark invisible" }, selected_mark_char );
|
||||
}
|
||||
|
||||
var div = createDOM( "div", {}, selected_mark, " ", label );
|
||||
|
||||
label.font_family = font_family;
|
||||
label.selected_mark = selected_mark;
|
||||
appendChildNodes( this.div, div );
|
||||
connect( label, "onclick", function ( event ) { self.font_name_clicked( event ); } );
|
||||
}
|
||||
|
||||
Pulldown.prototype.finish_init.call( this );
|
||||
}
|
||||
|
||||
Font_pulldown.prototype = new function () { this.prototype = Pulldown.prototype; };
|
||||
Font_pulldown.prototype.constructor = Font_pulldown;
|
||||
|
||||
Font_pulldown.prototype.font_name_clicked = function ( event ) {
|
||||
var label = event.src();
|
||||
if ( this.initial_selected_mark )
|
||||
addElementClass( this.initial_selected_mark, "invisible" );
|
||||
removeElementClass( label.selected_mark, "invisible" );
|
||||
|
||||
var self = this;
|
||||
setTimeout( function () {
|
||||
self.editor.focus();
|
||||
self.editor.exec_command( "fontname", label.font_family );
|
||||
self.shutdown();
|
||||
}, 100 );
|
||||
}
|
||||
|
||||
Font_pulldown.prototype.shutdown = function () {
|
||||
Pulldown.prototype.shutdown.call( this );
|
||||
|
||||
this.anchor.pulldown = null;
|
||||
disconnectAll( this );
|
||||
}
|
||||
|
||||
|
||||
function Note_tree( wiki, notebook_id, invoker ) {
|
||||
this.wiki = wiki;
|
||||
this.notebook_id = notebook_id;
|
||||
|
|
|
@ -31,9 +31,8 @@ class Download_page( Product_page ):
|
|||
),
|
||||
),
|
||||
P(
|
||||
"""
|
||||
Install Luminotes on your computer. 60-day money-back guarantee.
|
||||
""",
|
||||
u"Install Luminotes on your computer.",
|
||||
Span( u"60-day money-back guarantee.", class_ = u"lighter_text" ),
|
||||
class_ = u"upgrade_subtitle",
|
||||
),
|
||||
Div(
|
||||
|
@ -60,167 +59,66 @@ class Download_page( Product_page ):
|
|||
P(
|
||||
Table(
|
||||
Tr(
|
||||
Th( u" " ),
|
||||
Th(
|
||||
u"Luminotes Desktop",
|
||||
Span( u"Luminotes Desktop", class_ = u"plan_name" ),
|
||||
Div(
|
||||
A( "version", VERSION, href = news_url ),
|
||||
class_ = u"version_text",
|
||||
),
|
||||
class_ = u"plan_name_area download_plan_width",
|
||||
)
|
||||
),
|
||||
Tr( Td(), class_ = u"spacer_row" ),
|
||||
Tr(
|
||||
Td(
|
||||
Span( u"Solo", class_ = u"highlight" ), u"note taking",
|
||||
title = u"Luminotes Desktop is designed for individuals.",
|
||||
class_ = u"feature_value",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
u"Runs on your", Span( u"own computer", class_ = u"highlight" ),
|
||||
title = u"All of your notes are stored privately on your own computer or on a USB drive.",
|
||||
class_ = u"feature_value",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Span( u"Unlimited", class_ = u"highlight" ), u"storage",
|
||||
title = u"Add as many notes, documents, and files as you want.",
|
||||
class_ = u"feature_value",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
u"Works", Span( "offline", class_ = u"highlight" ),
|
||||
title = u"Take notes in meetings, in class, or while on the go. Runs in a web browser, but doesn't need an internet connection.",
|
||||
class_ = u"feature_value",
|
||||
),
|
||||
),
|
||||
Tr( Td(), class_ = u"spacer_row" ),
|
||||
Tr(
|
||||
Td(
|
||||
u"Windows XP/Vista,", A( u"Linux source", href = u"/source_code" ),
|
||||
class_ = u"small_text",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
u"Firefox, Internet Explorer, Chrome, Safari",
|
||||
class_ = u"small_text",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Div(
|
||||
download_button,
|
||||
class_ = u"download_button_area",
|
||||
),
|
||||
class_ = u"plan_name",
|
||||
)
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Unlimited storage space", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'storage_description' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
Td(
|
||||
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"More space for your wiki notes." ),
|
||||
Li( u"More space for your documents and files." ),
|
||||
),
|
||||
colspan = u"2",
|
||||
id = u"storage_description",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Unlimited wiki notebooks", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'notebooks_description' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
Td(
|
||||
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Create a unique notebook for each subject." ),
|
||||
Li( u"Keep work and personal notebooks separate." ),
|
||||
),
|
||||
colspan = u"2",
|
||||
id = u"notebooks_description",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Friendly email support", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'support_description' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
Td(
|
||||
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Fast email responses to your support questions. From a real live human." ),
|
||||
Li( u"No waiting on hold with a call center." ),
|
||||
),
|
||||
colspan = u"2",
|
||||
id = u"support_description",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Notes stored on your own computer", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'local_storage' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
Td(
|
||||
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"All of your notes are stored privately on your own computer." ),
|
||||
Li( u"You can also run Luminotes Desktop from a USB drive." ),
|
||||
Li( u"A future release will support optional online syncing." ),
|
||||
),
|
||||
colspan = u"2",
|
||||
id = u"local_storage",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Works without an internet connection", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'works_offline' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
Td(
|
||||
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Take notes in meetings, in class, or while on the go." ),
|
||||
Li( u"Runs in a web browser, but no internet connection is needed." ),
|
||||
Li( u'Absolutely no DRM. Does not "phone home".' ),
|
||||
),
|
||||
colspan = u"2",
|
||||
id = u"works_offline",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Supported operating systems", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'supported_oses' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
Td(
|
||||
u"Windows XP/Vista, Linux",
|
||||
class_ = u"small_text",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Fully supports Windows XP and Windows Vista." ),
|
||||
Li( u"Linux", A( u"source code", href = u"/source_code" ), "is available." ),
|
||||
Li( u"Future releases will include Mac OS X support." ),
|
||||
),
|
||||
colspan = u"2",
|
||||
id = u"supported_oses",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Supported web browsers", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'supported_browsers' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
Td(
|
||||
u"Firefox, Internet Explorer,", Br(),
|
||||
U"Chrome, Safari",
|
||||
class_ = u"small_text",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Fully supports Firefox and Internet Explorer." ),
|
||||
Li( u"Beta support for Chrome and Safari." ),
|
||||
Li( u"Future upgrades will support Opera." ),
|
||||
),
|
||||
colspan = u"2",
|
||||
id = u"supported_browsers",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr( Td(), class_ = u"spacer_row" ),
|
||||
border = u"1",
|
||||
id = u"upgrade_table",
|
||||
),
|
||||
|
@ -232,7 +130,7 @@ class Download_page( Product_page ):
|
|||
u"Don't want to install anything? Need collaboration features? ",
|
||||
A( u"Use Luminotes online", href = u"/pricing" ),
|
||||
u".",
|
||||
class_ = u"small_text",
|
||||
class_ = u"small_text luminotes_online_link_area",
|
||||
separator = u"",
|
||||
),
|
||||
|
||||
|
@ -321,13 +219,17 @@ class Download_page( Product_page ):
|
|||
Table(
|
||||
Tr(
|
||||
Th(
|
||||
u"Luminotes Desktop",
|
||||
Span( u"Luminotes Desktop", class_ = u"plan_name" ),
|
||||
class_ = u"plan_name_area",
|
||||
)
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Div(
|
||||
download_button,
|
||||
class_ = u"download_button_area",
|
||||
),
|
||||
class_ = u"plan_name",
|
||||
)
|
||||
),
|
||||
),
|
||||
id = u"upgrade_table_small",
|
||||
),
|
||||
|
|
|
@ -367,6 +367,7 @@ class Front_page( Product_page ):
|
|||
Span( id = u"italic_button_preload" ),
|
||||
Span( id = u"underline_button_preload" ),
|
||||
Span( id = u"strikethrough_button_preload" ),
|
||||
Span( id = u"font_button_preload" ),
|
||||
Span( id = u"title_button_preload" ),
|
||||
Span( id = u"bullet_list_button_preload" ),
|
||||
Span( id = u"numbered_list_button_preload" ),
|
||||
|
|
|
@ -18,8 +18,8 @@ class Header( Div ):
|
|||
Div(
|
||||
u"version", VERSION, u" | ",
|
||||
A( u"upgrade", href = u"http://luminotes.com/download?upgrade=True", target = "_new" ), u" | ",
|
||||
A( u"support", href = u"http://luminotes.com/support", target = "_new" ), u" | ",
|
||||
A( u"blog", href = u"http://luminotes.com/blog", target = "_new" ),
|
||||
A( u"community", href = u"http://luminotes.com/community", target = "_new" ), u" | ",
|
||||
A( u"blog", href = u"http://luminotes.com/blog/", target = "_new" ),
|
||||
class_ = u"header_links",
|
||||
),
|
||||
class_ = u"wide_center_area",
|
||||
|
|
|
@ -39,7 +39,7 @@ class Link_area( Div ):
|
|||
|
||||
( rate_plan.get( u"notebook_sharing" ) and notebook.name == u"Luminotes blog" ) and Div(
|
||||
A(
|
||||
u"subscribe to rss",
|
||||
u"follow",
|
||||
href = u"%s?rss" % notebook_path,
|
||||
id = u"blog_rss_link",
|
||||
title = u"Subscribe to the RSS feed for the Luminotes blog.",
|
||||
|
@ -52,7 +52,7 @@ class Link_area( Div ):
|
|||
class_ = u"link_area_item",
|
||||
) or ( updates_path and rate_plan.get( u"notebook_sharing" ) and ( not forum_tag ) and Div(
|
||||
A(
|
||||
u"subscribe to rss",
|
||||
u"follow",
|
||||
href = updates_path,
|
||||
id = u"notebook_rss_link",
|
||||
title = u"Subscribe to the RSS feed for this %s." % notebook_word,
|
||||
|
@ -87,13 +87,13 @@ class Link_area( Div ):
|
|||
|
||||
( notebook.read_write == Notebook.READ_WRITE ) and Span(
|
||||
Div(
|
||||
A(
|
||||
( notebook.name != u"trash" ) and A(
|
||||
u"import",
|
||||
href = u"#",
|
||||
id = u"import_link",
|
||||
title = u"Import %ss from other software into Luminotes." % note_word,
|
||||
),
|
||||
u"|",
|
||||
) or None,
|
||||
( notebook.name != u"trash" ) and u"|" or None,
|
||||
A(
|
||||
u"export",
|
||||
href = u"#",
|
||||
|
@ -103,27 +103,35 @@ class Link_area( Div ):
|
|||
class_ = u"link_area_item",
|
||||
) or None,
|
||||
|
||||
( notebook.name != u"trash" ) and Div(
|
||||
notebook.trash_id and A(
|
||||
u"trash",
|
||||
href = u"/notebooks/%s?parent_id=%s" % ( notebook.trash_id, notebook.object_id ),
|
||||
id = u"trash_link",
|
||||
title = u"Look here for %ss you've deleted." % note_word,
|
||||
) or None,
|
||||
( notebook.owner and notebook.name != u"trash" and notebook.trash_id ) and u"|" or None,
|
||||
( notebook.owner and notebook.name != u"trash" ) and A(
|
||||
u"delete",
|
||||
href = u"#",
|
||||
id = u"delete_notebook_link",
|
||||
title = u"Move this %s to the trash." % notebook_word,
|
||||
) or None,
|
||||
class_ = u"link_area_item",
|
||||
) or None,
|
||||
|
||||
( notebook.owner and notebook.name != u"trash" ) and Div(
|
||||
A(
|
||||
u"rename",
|
||||
href = u"#",
|
||||
id = u"rename_notebook_link",
|
||||
title = u"Change the name of this %s." % notebook_word,
|
||||
),
|
||||
class_ = u"link_area_item",
|
||||
),
|
||||
class_ = u"link_area_item",
|
||||
) or None,
|
||||
|
||||
( notebook.owner and notebook.name != u"trash" ) and Div(
|
||||
A(
|
||||
u"delete",
|
||||
href = u"#",
|
||||
id = u"delete_notebook_link",
|
||||
title = u"Move this %s to the trash." % notebook_word,
|
||||
),
|
||||
class_ = u"link_area_item",
|
||||
) or None,
|
||||
|
||||
( notebook.owner and user.username and rate_plan.get( u"notebook_sharing" ) ) and Div(
|
||||
( notebook.owner and notebook.name != u"trash" and
|
||||
user.username and rate_plan.get( u"notebook_sharing" ) ) and Div(
|
||||
A(
|
||||
u"share",
|
||||
href = u"#",
|
||||
|
@ -133,16 +141,6 @@ class Link_area( Div ):
|
|||
class_ = u"link_area_item",
|
||||
) or None,
|
||||
|
||||
notebook.trash_id and Div(
|
||||
A(
|
||||
u"trash",
|
||||
href = u"/notebooks/%s?parent_id=%s" % ( notebook.trash_id, notebook.object_id ),
|
||||
id = u"trash_link",
|
||||
title = u"Look here for %ss you've deleted." % note_word,
|
||||
),
|
||||
class_ = u"link_area_item",
|
||||
) or None,
|
||||
|
||||
( notebook.name == u"trash" ) and Rounded_div(
|
||||
u"trash_notebook",
|
||||
A(
|
||||
|
|
|
@ -231,6 +231,12 @@ class Main_page( Page ):
|
|||
) or None,
|
||||
Rounded_div(
|
||||
( notebook.name == u"trash" ) and u"trash_notebook" or u"current_notebook",
|
||||
parent_id and Span(
|
||||
A( u"empty", href = u"/notebooks/%s" % notebook.object_id, id = u"empty_trash_link" ),
|
||||
u" | ",
|
||||
A( u"go back", href = u"/notebooks/%s" % parent_id ),
|
||||
id = u"notebook_header_links",
|
||||
) or None,
|
||||
( notebook.name == u"Luminotes" and title == u"source code" ) and \
|
||||
Strong( "%s %s" % ( notebook.name, VERSION ) ) or \
|
||||
Span(
|
||||
|
@ -238,12 +244,6 @@ class Main_page( Page ):
|
|||
and Strong( notebook.name ) \
|
||||
or Span( Strong( notebook.name ), id = u"notebook_header_name", title = "Rename this notebook." ),
|
||||
),
|
||||
parent_id and Span(
|
||||
u" | ",
|
||||
A( u"empty trash", href = u"/notebooks/%s" % notebook.object_id, id = u"empty_trash_link" ),
|
||||
u" | ",
|
||||
A( u"return to notebook", href = u"/notebooks/%s" % parent_id ),
|
||||
) or None,
|
||||
id = u"notebook_header_area",
|
||||
corners = ( u"tl", u"tr", u"br" ),
|
||||
),
|
||||
|
@ -267,6 +267,8 @@ class Main_page( Page ):
|
|||
) or None,
|
||||
( forum_tag and user.username and user.username != u"anonymous" ) and \
|
||||
P( u"To write a comment, click that large \"+\" button to the left. To publish your comment, click the save button.", class_ = u"small_text" ) or None,
|
||||
( forum_tag and ( not user.username or user.username == u"anonymous" ) ) and \
|
||||
P( u"To write a comment, please login first. No account?", A( u"Sign up", href = u"/pricing" ), u"to get a free account.", class_ = "small_text" ) or None,
|
||||
Page_navigation(
|
||||
notebook_path, len( notes ), total_notes_count, start, count,
|
||||
return_text = u"return to the discussion",
|
||||
|
|
|
@ -21,13 +21,12 @@ class Product_page( Page ):
|
|||
|
||||
Div(
|
||||
Div(
|
||||
# TODO make into a table kinda like on the footer of change.gov?
|
||||
Div(
|
||||
Div(
|
||||
Ul(
|
||||
Li( u"About", class_ = u"footer_category" ),
|
||||
Li( A( u"tour", href = u"/tour" ) ),
|
||||
Li( A( u"demo", href = u"/demo" ) ),
|
||||
Li( A( u"demo", href = u"/users/demo" ) ),
|
||||
Li( A( u"faq", href = u"/faq" ) ),
|
||||
Li( A( u"team", href = u"/meet_the_team" ) ),
|
||||
Li( A( u"user guide", href = u"/guide" ) ),
|
||||
|
|
|
@ -2,7 +2,7 @@ from Tags import Span, H3, P, A
|
|||
|
||||
|
||||
class Thanks_note( Span ):
|
||||
def __init__( self, rate_plan_name ):
|
||||
def __init__( self, rate_plan_name = None ):
|
||||
Span.__init__(
|
||||
self,
|
||||
H3( u"thank you" ),
|
||||
|
@ -13,9 +13,9 @@ class Thanks_note( Span ):
|
|||
),
|
||||
P(
|
||||
u"""
|
||||
You are now subscribed to Luminotes %s. Please click on one of your
|
||||
notebooks to the right to get started with your newly upgraded wiki.
|
||||
""" % rate_plan_name,
|
||||
You are now subscribed to Luminotes%s. Please click on one of your
|
||||
notebooks to the left to get started with your newly upgraded wiki.
|
||||
""" % ( rate_plan_name and u" %s" % rate_plan_name or u"" ),
|
||||
),
|
||||
P(
|
||||
u"""
|
||||
|
|
|
@ -10,7 +10,7 @@ class Toolbar( Div ):
|
|||
P(
|
||||
Div( Input(
|
||||
type = u"image",
|
||||
id = u"newNote", title = u"new %s [ctrl-N]" % ( note_word or u"note" ),
|
||||
id = u"newNote", title = u"make a new %s [ctrl-M]" % ( note_word or u"note" ),
|
||||
src = u"/static/images/toolbar/new_note_button.png",
|
||||
width = u"40", height = u"40",
|
||||
class_ = "image_button",
|
||||
|
@ -60,6 +60,13 @@ class Toolbar( Div ):
|
|||
width = u"40", height = u"40",
|
||||
class_ = "image_button",
|
||||
) ),
|
||||
Div( Input(
|
||||
type = u"image",
|
||||
id = u"font", title = u"font",
|
||||
src = u"/static/images/toolbar/font_button.png",
|
||||
width = u"40", height = u"40",
|
||||
class_ = "image_button",
|
||||
) ),
|
||||
Div( Input(
|
||||
type = u"image",
|
||||
id = u"title", title = u"title",
|
||||
|
@ -94,7 +101,7 @@ class Toolbar( Div ):
|
|||
Span( id = "italic_button_hover_preload" ),
|
||||
Span( id = "underline_button_hover_preload" ),
|
||||
Span( id = "strikethrough_button_hover_preload" ),
|
||||
Span( id = "title_button_hover_preload" ),
|
||||
Span( id = "font_button_hover_preload" ),
|
||||
Span( id = "bullet_list_button_hover_preload" ),
|
||||
Span( id = "numbered_list_button_hover_preload" ),
|
||||
|
||||
|
@ -105,7 +112,7 @@ class Toolbar( Div ):
|
|||
Span( id = "italic_button_down_hover_preload" ),
|
||||
Span( id = "underline_button_down_hover_preload" ),
|
||||
Span( id = "strikethrough_button_down_hover_preload" ),
|
||||
Span( id = "title_button_down_hover_preload" ),
|
||||
Span( id = "font_button_down_hover_preload" ),
|
||||
Span( id = "bullet_list_button_down_hover_preload" ),
|
||||
Span( id = "numbered_list_button_down_hover_preload" ),
|
||||
|
||||
|
@ -116,7 +123,7 @@ class Toolbar( Div ):
|
|||
Span( id = "italic_button_down_preload" ),
|
||||
Span( id = "underline_button_down_preload" ),
|
||||
Span( id = "strikethrough_button_down_preload" ),
|
||||
Span( id = "title_button_down_preload" ),
|
||||
Span( id = "font_button_down_preload" ),
|
||||
Span( id = "bullet_list_button_down_preload" ),
|
||||
Span( id = "numbered_list_button_down_preload" ),
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from Tags import Div, H1, Img, A, P, Table, Th, Tr, Td, Li, Span, I, Br, Ul, Li,
|
|||
|
||||
|
||||
class Upgrade_page( Product_page ):
|
||||
HIDDEN_PLAN_THRESHOLD = 3
|
||||
FOCUSED_PLAN = 2
|
||||
|
||||
def __init__( self, user, notebooks, first_notebook, login_url, logout_url, rate_plan, groups, rate_plans, unsubscribe_button ):
|
||||
MEGABYTE = 1024 * 1024
|
||||
|
@ -36,205 +36,48 @@ class Upgrade_page( Product_page ):
|
|||
),
|
||||
),
|
||||
P(
|
||||
"""
|
||||
Upgrade, downgrade, or cancel anytime. 60-day money-back guarantee.
|
||||
""",
|
||||
( user.rate_plan == 0 ) and u"30-day free trial on all plans." or None,
|
||||
Span( u"Upgrade, downgrade, or cancel anytime.", class_ = u"lighter_text" ),
|
||||
class_ = u"upgrade_subtitle",
|
||||
),
|
||||
P(
|
||||
Table(
|
||||
self.fee_row( rate_plans, user ),
|
||||
self.spacer_row( rate_plans ),
|
||||
Tr(
|
||||
Td(
|
||||
u"Designed for",
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
[ Td(
|
||||
plan[ u"designed_for" ],
|
||||
class_ = u"feature_value" + self.displayed( index ),
|
||||
( plan[ u"included_users" ] == 1 ) and
|
||||
Span( Span( u"Single", class_ = u"highlight" ), u"user", title = u"This plan includes one user account, so it's ideal for individuals." ) or
|
||||
Span( u"Up to", Span( "%s" % plan[ u"included_users" ], class_ = u"highlight" ), u"users", title = "This plan includes multiple accounts, including an admin area where you can create and manage users for your organization." ),
|
||||
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Included accounts", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'users_description' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
[ Td(
|
||||
( plan[ u"included_users" ] == 1 ) and u"1 user" or "up to<br>%s users" % plan[ u"included_users" ],
|
||||
class_ = u"feature_value" + self.displayed( index ),
|
||||
plan[ u"storage_quota_bytes" ] and
|
||||
Span( "%s MB" % ( plan[ u"storage_quota_bytes" ] // MEGABYTE ), class_ = u"highlight" ) or
|
||||
Span( u"unlimited", class_ = u"highlight" ),
|
||||
u"storage",
|
||||
title = u"Storage space for your notes, documents, and files.",
|
||||
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Collaborate on a wiki with multiple people in your organization." ),
|
||||
Li( u"Even collaborate with other Luminotes users beyond your included accounts." ),
|
||||
Li( u"Only one subscription is necessary." ),
|
||||
),
|
||||
colspan = len( rate_plans ) + 1,
|
||||
id = u"users_description",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Included storage space", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'storage_description' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
[ Td(
|
||||
plan[ u"storage_quota_bytes" ] and "%s MB" % ( plan[ u"storage_quota_bytes" ] // MEGABYTE ) or u"unlimited",
|
||||
class_ = u"feature_value" + self.displayed( index ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"More space for your wiki notes." ),
|
||||
Li( u"More space for your documents and files." ),
|
||||
Li( u"All of your users share a common pool of storage space." ),
|
||||
),
|
||||
colspan = len( rate_plans ) + 1,
|
||||
id = u"storage_description",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Unlimited wiki notebooks", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'notebooks_description' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
[ Td(
|
||||
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
|
||||
class_ = u"feature_value" + self.displayed( index ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Create a unique notebook for each subject." ),
|
||||
Li( u"Keep work and personal notebooks separate." ),
|
||||
),
|
||||
colspan = len( rate_plans ) + 1,
|
||||
id = u"notebooks_description",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Friendly email support", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'support_description' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
[ Td(
|
||||
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
|
||||
class_ = u"feature_value" + self.displayed( index ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Fast email responses to your support questions. From a real live human." ),
|
||||
Li( u"No waiting on hold with a call center." ),
|
||||
),
|
||||
colspan = len( rate_plans ) + 1,
|
||||
id = u"support_description",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Invite people to view your wiki", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'view_description' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
[ Td(
|
||||
plan[ u"notebook_sharing" ] and
|
||||
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ) or u" ",
|
||||
class_ = u"feature_value" + self.displayed( index ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Invite specific people to read your wiki." ),
|
||||
Li( u"Invite as many people as you want." ),
|
||||
Li( u"Share only the notebooks you want to share. Keep the others private." ),
|
||||
),
|
||||
colspan = len( rate_plans ) + 1,
|
||||
id = u"view_description",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"Invite people to edit your wiki", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'edit_description' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
plan[ u"notebook_sharing"] and Tr(
|
||||
[ Td(
|
||||
plan[ u"notebook_collaboration" ] and
|
||||
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ) or u" ",
|
||||
class_ = u"feature_value" + self.displayed( index ),
|
||||
Span( u"Invite", Span( u"editors", class_ = u"highlight" ), title = u"Invite people to collaborate on your wiki. Share only the notebooks you want. Keep the others private." ) or
|
||||
Span( u"Invite", Span( u"viewers", class_ = u"highlight" ), title = u"Invite people to view your wiki. Share only the notebooks you want. Keep the others private." ),
|
||||
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Invite specific people to collaborate on your wiki." ),
|
||||
Li( u"Decide who can edit and who can only view." ),
|
||||
Li( u"Invite as many people as you want. They only need free Luminotes accounts." ),
|
||||
Li( u"Revoke collaboration access with a single click." ),
|
||||
Li( u"Share only the notebooks you want to share. Keep the others private." ),
|
||||
),
|
||||
colspan = len( rate_plans ) + 1,
|
||||
id = u"edit_description",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
A( u"User administration", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'admin_description' ); return false;" ),
|
||||
class_ = u"feature_name",
|
||||
),
|
||||
[ Td(
|
||||
plan[ u"user_admin" ] and
|
||||
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ) or u" ",
|
||||
class_ = u"feature_value" + self.displayed( index ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
),
|
||||
Tr(
|
||||
Td(
|
||||
Ul(
|
||||
Li( u"Manage all Luminotes accounts for your organization from one web page." ),
|
||||
Li( u"Create and delete users as needed." ),
|
||||
),
|
||||
colspan = len( rate_plans ) + 1,
|
||||
id = u"admin_description",
|
||||
class_ = u"feature_description undisplayed",
|
||||
),
|
||||
),
|
||||
) or None,
|
||||
self.button_row( rate_plans, user ),
|
||||
self.spacer_row( rate_plans, bottom = True ),
|
||||
border = u"1",
|
||||
id = u"upgrade_table",
|
||||
),
|
||||
class_ = u"upgrade_table_area",
|
||||
),
|
||||
|
||||
Script(
|
||||
"""
|
||||
function toggle_plans() {
|
||||
var nodes = getElementsByTagAndClassName( null, "hidden_plans" );
|
||||
for ( var i in nodes ) { var node = nodes[ i ]; toggleElementClass( "undisplayed", node ); }
|
||||
|
||||
toggleElementClass( "undisplayed", "more_plans_link" );
|
||||
toggleElementClass( "undisplayed", "fewer_plans_link" );
|
||||
return false;
|
||||
}
|
||||
""",
|
||||
type = u"text/javascript",
|
||||
),
|
||||
Div(
|
||||
A( "Show me plans for teams.", id = "more_plans_link", href = u"#", onclick = u"return toggle_plans();" ),
|
||||
A( "Show me fewer plans.", id = "fewer_plans_link", class_ = u"undisplayed", href = u"#", onclick = u"return toggle_plans();" ),
|
||||
class_ = u"small_text",
|
||||
),
|
||||
|
||||
user and user.username not in ( u"anonymous", None ) and P(
|
||||
u"You're currently subscribed to Luminotes %s." %
|
||||
user_plan[ u"name" ].capitalize(),
|
||||
|
@ -329,19 +172,37 @@ class Upgrade_page( Product_page ):
|
|||
""",
|
||||
class_ = u"upgrade_text",
|
||||
),
|
||||
H4( u"How does the 30-day free trial work?", class_ = u"upgrade_question" ),
|
||||
P(
|
||||
"""
|
||||
When you subscribe to Luminotes, your first 30 days are completely free. And if you
|
||||
cancel during that period, you aren't charged a thing. During those 30 days, you have
|
||||
full access to all features of your selected subscription plan. That way, you can see
|
||||
whether Luminotes works for you without any sort of commitment.
|
||||
""",
|
||||
class_ = u"upgrade_text",
|
||||
),
|
||||
H4( u"Once I subscribe, can I cancel anytime?", class_ = u"upgrade_question" ),
|
||||
P(
|
||||
"""
|
||||
Of course. This isn't a cell phone plan. There are no contracts or cancellation fees.
|
||||
You can upgrade, downgrade, or cancel your account anytime. Simply login to your
|
||||
account and return to this pricing page.
|
||||
Of course. There are no contracts or cancellation fees. There are no hidden fees. You
|
||||
can upgrade, downgrade, or cancel your account anytime. Simply login to your account
|
||||
and return to this pricing page.
|
||||
""",
|
||||
class_ = u"upgrade_text",
|
||||
),
|
||||
P(
|
||||
"""
|
||||
And if you cancel during your trial period, then you are not charged anything at all.
|
||||
""",
|
||||
class_ = u"upgrade_text",
|
||||
),
|
||||
H4( u"What is your refund policy?", class_ = u"upgrade_question" ),
|
||||
P(
|
||||
"""
|
||||
It's this simple: Luminotes comes with a 60-day money-back guarantee. No questions asked.
|
||||
It's this simple: Luminotes comes with a 30-day money-back guarantee, starting from
|
||||
the end of the 30-day free trial. No questions asked. So that gives you a full 60
|
||||
days to see whether Luminotes meets your needs.
|
||||
""",
|
||||
class_ = u"upgrade_text",
|
||||
),
|
||||
|
@ -359,24 +220,41 @@ class Upgrade_page( Product_page ):
|
|||
|
||||
P(
|
||||
A( name = "yearly" ),
|
||||
Div(
|
||||
u"Get two months free with a yearly subscription!",
|
||||
class_ = u"upgrade_subtitle",
|
||||
),
|
||||
Table(
|
||||
Tr( Td(
|
||||
u"Get two months free with a yearly subscription!",
|
||||
class_ = u"upgrade_subtitle",
|
||||
colspan = u"%d" % len( rate_plans ),
|
||||
), colspan = u"%d" % len( rate_plans ) ),
|
||||
self.fee_row( rate_plans, user, include_blank = False, yearly = True ),
|
||||
self.fee_row( rate_plans, user, yearly = True ),
|
||||
self.spacer_row( rate_plans ),
|
||||
Tr(
|
||||
[ Td(
|
||||
( plan[ u"included_users" ] == 1 ) and u"1 user" or "up to<br />%s users" % plan[ u"included_users" ],
|
||||
class_ = u"feature_value",
|
||||
) for plan in rate_plans ],
|
||||
( plan[ u"included_users" ] == 1 ) and
|
||||
Span( Span( u"Single", class_ = u"highlight" ), u"user", title = u"This plan includes one user account, so it's ideal for individuals." ) or
|
||||
Span( u"Up to", Span( "%s" % plan[ u"included_users" ], class_ = u"highlight" ), u"users", title = "This plan includes multiple accounts, including an admin area where you can create and manage users for your organization." ),
|
||||
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
),
|
||||
Tr(
|
||||
[ Td(
|
||||
plan[ u"storage_quota_bytes" ] and "%s MB" % ( plan[ u"storage_quota_bytes" ] // MEGABYTE ) or u"unlimited",
|
||||
) for plan in rate_plans ],
|
||||
plan[ u"storage_quota_bytes" ] and
|
||||
Span( "%s MB" % ( plan[ u"storage_quota_bytes" ] // MEGABYTE ), class_ = u"highlight" ) or
|
||||
Span( u"unlimited", class_ = u"highlight" ),
|
||||
u"storage",
|
||||
title = u"Storage space for your notes, documents, and files.",
|
||||
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
),
|
||||
plan[ u"notebook_sharing"] and Tr(
|
||||
[ Td(
|
||||
plan[ u"notebook_collaboration" ] and
|
||||
Span( u"Invite", Span( u"editors", class_ = u"highlight" ), title = u"Invite people to collaborate on your wiki. Share only the notebooks you want. Keep the others private." ) or
|
||||
Span( u"Invite", Span( u"viewers", class_ = u"highlight" ), title = u"Invite people to view your wiki. Share only the notebooks you want. Keep the others private." ),
|
||||
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
) or None,
|
||||
self.button_row( rate_plans, user, yearly = True ),
|
||||
self.spacer_row( rate_plans, bottom = True ),
|
||||
border = u"1",
|
||||
id = u"upgrade_table_small",
|
||||
),
|
||||
|
@ -398,38 +276,67 @@ class Upgrade_page( Product_page ):
|
|||
),
|
||||
)
|
||||
|
||||
def fee_row( self, rate_plans, user, include_blank = True, yearly = False ):
|
||||
return Tr(
|
||||
include_blank and Th( u" " ) or None,
|
||||
[ Th(
|
||||
plan[ u"name" ].capitalize(),
|
||||
def fee_row( self, rate_plans, user, yearly = False ):
|
||||
def make_fee_area( plan, index ):
|
||||
fee_area = (
|
||||
Span( plan[ u"name" ].capitalize(), class_ = u"plan_name" ),
|
||||
plan[ u"fee" ] and Div(
|
||||
yearly and Span(
|
||||
u"$%s" % plan[ u"yearly_fee" ],
|
||||
Span( u"/year", class_ = u"month_text" ),
|
||||
class_ = u"price_text",
|
||||
separator = u"",
|
||||
) or Span(
|
||||
) or Div(
|
||||
u"$%s" % plan[ u"fee" ],
|
||||
Span( u"/month", class_ = u"month_text" ),
|
||||
class_ = u"price_text",
|
||||
separator = u"",
|
||||
),
|
||||
user and user.username not in ( u"anonymous", None ) and user.rate_plan != index \
|
||||
and ( yearly and ( plan.get( u"yearly_button" ).strip() and plan.get( u"yearly_button" ) % user.object_id or None ) or \
|
||||
( plan.get( u"button" ).strip() and plan.get( u"button" ) % user.object_id or None ) ) or None,
|
||||
) or Div( Span( u"No fee", class_ = u"price_text" ) ),
|
||||
( not user or user.username in ( u"anonymous", None ) ) and Div(
|
||||
A(
|
||||
Img( src = u"/static/images/sign_up_button.png", width = "76", height = "23" ),
|
||||
href = u"/sign_up?plan=%s&yearly=%s" % ( index, yearly ),
|
||||
),
|
||||
class_ = u"sign_up_button_area",
|
||||
) or None,
|
||||
class_ = u"plan_name" + self.displayed( index, yearly ),
|
||||
) or Div( Div( u"No fee", class_ = u"price_text" ) ),
|
||||
Div(
|
||||
u"For", plan[ u"designed_for" ],
|
||||
class_ = u"small_text",
|
||||
),
|
||||
( index == self.FOCUSED_PLAN ) and Div( u"Best value", class_ = u"focused_text highlight" ) or None,
|
||||
)
|
||||
|
||||
# if this is a demo/guest user, then make the fee area a big link to the sign up page
|
||||
if not user or user.username in ( u"anonymous", None ):
|
||||
fee_area = A( href = u"/sign_up?plan=%s&yearly=%s" % ( index, yearly ), *fee_area )
|
||||
else:
|
||||
fee_area = Span( *fee_area )
|
||||
|
||||
return fee_area
|
||||
|
||||
return Tr(
|
||||
[ Th(
|
||||
make_fee_area( plan, index ),
|
||||
class_ = u"plan_name_area plan_width" + ( index == self.FOCUSED_PLAN and u" focused_plan_name_area" or u"" ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def displayed( index, yearly = False ):
|
||||
return ( index >= Upgrade_page.HIDDEN_PLAN_THRESHOLD and not yearly ) and " undisplayed hidden_plans" or ""
|
||||
def button_row( self, rate_plans, user, yearly = False ):
|
||||
return Tr(
|
||||
[ Td(
|
||||
Div(
|
||||
# 1 = modifying an existing subscription, 0 = new subscription
|
||||
user and user.username not in ( u"anonymous", None ) and user.rate_plan != index \
|
||||
and ( yearly and ( plan.get( u"yearly_button" ) and plan.get( u"yearly_button" ).strip() and
|
||||
plan.get( u"yearly_button" ) % ( user.object_id, user.rate_plan and 1 or 0 ) or None ) or \
|
||||
( plan.get( u"button" ) and plan.get( u"button" ).strip() and
|
||||
plan.get( u"button" ) % ( user.object_id, user.rate_plan and 1 or 0 ) or None ) ) or None,
|
||||
( not user or user.username in ( u"anonymous", None ) ) and A(
|
||||
Img( src = u"/static/images/sign_up_button.png", width = "76", height = "23" ),
|
||||
href = u"/sign_up?plan=%s&yearly=%s" % ( index, yearly ),
|
||||
) or None,
|
||||
class_ = u"subscribe_button_area",
|
||||
),
|
||||
( user.rate_plan == 0 ) and Div( "30-day free trial", class_ = u"small_text" ) or None,
|
||||
class_ = ( index == self.FOCUSED_PLAN and u"focused_feature_value" or u"" ),
|
||||
) for ( index, plan ) in enumerate( rate_plans ) ],
|
||||
)
|
||||
|
||||
def spacer_row( self, rate_plans, bottom = False ):
|
||||
border_bottom = bottom and " focused_border_bottom" or ""
|
||||
|
||||
return Tr( [ Td( class_ = ( i == self.FOCUSED_PLAN and u"focused_feature_value" + border_bottom or u"spacer_row" ) ) for i in range( len( rate_plans ) ) ], class_ = u"spacer_row" )
|
||||
|
|