diff --git a/NEWS b/NEWS index c8a4dc6..0466880 100644 --- a/NEWS +++ b/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. diff --git a/README b/README index 26f4c4d..f5823bc 100644 --- a/README +++ b/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 diff --git a/config/Desktop.py b/config/Desktop.py index d529edf..3e4d482 100644 --- a/config/Desktop.py +++ b/config/Desktop.py @@ -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, diff --git a/config/Version.py b/config/Version.py index 61ebe8c..4f777e1 100644 --- a/config/Version.py +++ b/config/Version.py @@ -1 +1 @@ -VERSION = u"1.5.9" +VERSION = u"1.5.13" diff --git a/controller/Database.py b/controller/Database.py index b197ec8..1519903 100644 --- a/controller/Database.py +++ b/controller/Database.py @@ -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( diff --git a/controller/Files.py b/controller/Files.py index 0c3072f..2b51c6d 100644 --- a/controller/Files.py +++ b/controller/Files.py @@ -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( diff --git a/controller/Forums.py b/controller/Forums.py index 44652df..c9f47c5 100644 --- a/controller/Forums.py +++ b/controller/Forums.py @@ -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 diff --git a/controller/Users.py b/controller/Users.py index d5773f4..88a287d 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -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() ) diff --git a/controller/test/Test_controller.py b/controller/test/Test_controller.py index 0158a18..02e7897 100644 --- a/controller/test/Test_controller.py +++ b/controller/test/Test_controller.py @@ -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": [ diff --git a/controller/test/Test_database.py b/controller/test/Test_database.py index dac977f..ac51bee 100644 --- a/controller/test/Test_database.py +++ b/controller/test/Test_database.py @@ -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 diff --git a/controller/test/Test_forums.py b/controller/test/Test_forums.py index 2ab4e61..041a39a 100644 --- a/controller/test/Test_forums.py +++ b/controller/test/Test_forums.py @@ -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() diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py index 45a8485..9496390 100644 --- a/controller/test/Test_users.py +++ b/controller/test/Test_users.py @@ -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" diff --git a/model/delta/1.5.10.sql b/model/delta/1.5.10.sql new file mode 100644 index 0000000..f0d2610 --- /dev/null +++ b/model/delta/1.5.10.sql @@ -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; diff --git a/model/schema.sqlite b/model/schema.sqlite index 110d5e2..8caae64 100644 --- a/model/schema.sqlite +++ b/model/schema.sqlite @@ -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, diff --git a/static/css/download.css b/static/css/download.css index 617e8cc..9b59eaf 100644 --- a/static/css/download.css +++ b/static/css/download.css @@ -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; diff --git a/static/css/ie6.css b/static/css/ie6.css index 06d363f..27e8d76 100644 --- a/static/css/ie6.css +++ b/static/css/ie6.css @@ -71,3 +71,7 @@ margin-bottom: 0.25em; padding: 0.25em 0.25em 0 0.5em; } + +#current_notebook_wrapper { + margin-right: 1em; +} diff --git a/static/css/note.css b/static/css/note.css index e0c6ef1..0933ce1 100644 --- a/static/css/note.css +++ b/static/css/note.css @@ -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 { diff --git a/static/css/product.css b/static/css/product.css index 1b7a93c..aa1322d 100644 --- a/static/css/product.css +++ b/static/css/product.css @@ -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; diff --git a/static/css/style.css b/static/css/style.css index 0226abf..c1a9b71 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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; diff --git a/static/images/screenshot_small.jpg b/static/images/screenshot_small.jpg deleted file mode 100644 index 3cb81cb..0000000 Binary files a/static/images/screenshot_small.jpg and /dev/null differ diff --git a/static/images/screenshot_small.png b/static/images/screenshot_small.png index 196b332..b603150 100644 Binary files a/static/images/screenshot_small.png and b/static/images/screenshot_small.png differ diff --git a/static/images/toolbar/font_button.png b/static/images/toolbar/font_button.png new file mode 100644 index 0000000..9372ee9 Binary files /dev/null and b/static/images/toolbar/font_button.png differ diff --git a/static/images/toolbar/font_button.xcf b/static/images/toolbar/font_button.xcf new file mode 100644 index 0000000..4331ad9 Binary files /dev/null and b/static/images/toolbar/font_button.xcf differ diff --git a/static/images/toolbar/font_button_down.png b/static/images/toolbar/font_button_down.png new file mode 100644 index 0000000..7c0de1e Binary files /dev/null and b/static/images/toolbar/font_button_down.png differ diff --git a/static/images/toolbar/font_button_down.xcf b/static/images/toolbar/font_button_down.xcf new file mode 100644 index 0000000..d9b9dea Binary files /dev/null and b/static/images/toolbar/font_button_down.xcf differ diff --git a/static/images/toolbar/font_button_down_hover.png b/static/images/toolbar/font_button_down_hover.png new file mode 100644 index 0000000..1294bcd Binary files /dev/null and b/static/images/toolbar/font_button_down_hover.png differ diff --git a/static/images/toolbar/font_button_down_hover.xcf b/static/images/toolbar/font_button_down_hover.xcf new file mode 100644 index 0000000..e616221 Binary files /dev/null and b/static/images/toolbar/font_button_down_hover.xcf differ diff --git a/static/images/toolbar/font_button_hover.png b/static/images/toolbar/font_button_hover.png new file mode 100644 index 0000000..927c7bf Binary files /dev/null and b/static/images/toolbar/font_button_hover.png differ diff --git a/static/images/toolbar/font_button_hover.xcf b/static/images/toolbar/font_button_hover.xcf new file mode 100644 index 0000000..ce96066 Binary files /dev/null and b/static/images/toolbar/font_button_hover.xcf differ diff --git a/static/images/toolbar/small/font_button.png b/static/images/toolbar/small/font_button.png new file mode 100644 index 0000000..e0d67b8 Binary files /dev/null and b/static/images/toolbar/small/font_button.png differ diff --git a/static/images/toolbar/small/font_button.xcf b/static/images/toolbar/small/font_button.xcf new file mode 100644 index 0000000..fc89f25 Binary files /dev/null and b/static/images/toolbar/small/font_button.xcf differ diff --git a/static/images/toolbar/small/font_button_down.png b/static/images/toolbar/small/font_button_down.png new file mode 100644 index 0000000..eedcfbf Binary files /dev/null and b/static/images/toolbar/small/font_button_down.png differ diff --git a/static/images/toolbar/small/font_button_down.xcf b/static/images/toolbar/small/font_button_down.xcf new file mode 100644 index 0000000..94b8e52 Binary files /dev/null and b/static/images/toolbar/small/font_button_down.xcf differ diff --git a/static/images/toolbar/small/font_button_down_hover.png b/static/images/toolbar/small/font_button_down_hover.png new file mode 100644 index 0000000..549562c Binary files /dev/null and b/static/images/toolbar/small/font_button_down_hover.png differ diff --git a/static/images/toolbar/small/font_button_down_hover.xcf b/static/images/toolbar/small/font_button_down_hover.xcf new file mode 100644 index 0000000..b306a10 Binary files /dev/null and b/static/images/toolbar/small/font_button_down_hover.xcf differ diff --git a/static/images/toolbar/small/font_button_hover.png b/static/images/toolbar/small/font_button_hover.png new file mode 100644 index 0000000..6b9925e Binary files /dev/null and b/static/images/toolbar/small/font_button_hover.png differ diff --git a/static/images/toolbar/small/font_button_hover.xcf b/static/images/toolbar/small/font_button_hover.xcf new file mode 100644 index 0000000..9adf123 Binary files /dev/null and b/static/images/toolbar/small/font_button_hover.xcf differ diff --git a/static/js/Editor.js b/static/js/Editor.js index 787fc11..9d65dbf 100644 --- a/static/js/Editor.js +++ b/static/js/Editor.js @@ -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 diff --git a/static/js/Wiki.js b/static/js/Wiki.js index fb22a43..afc27c1 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -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