diff options
Diffstat (limited to 'plugins')
56 files changed, 1454 insertions, 8645 deletions
diff --git a/plugins/authenticator/shotwell/FacebookPublishingAuthenticator.vala b/plugins/authenticator/shotwell/FacebookPublishingAuthenticator.vala deleted file mode 100644 index 26a2363..0000000 --- a/plugins/authenticator/shotwell/FacebookPublishingAuthenticator.vala +++ /dev/null @@ -1,318 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -using Shotwell; -using Shotwell.Plugins; - -namespace Publishing.Authenticator.Shotwell.Facebook { - private const string APPLICATION_ID = "1612018629063184"; - - private class WebAuthenticationPane : Common.WebAuthenticationPane { - private static bool cache_dirty = false; - - public signal void login_succeeded(string success_url); - public signal void login_failed(); - - public WebAuthenticationPane() { - Object (login_uri : get_login_url ()); - } - - private class LocaleLookup { - public string prefix; - public string translation; - public string? exception_code; - public string? exception_translation; - public string? exception_code_2; - public string? exception_translation_2; - - public LocaleLookup(string prefix, string translation, string? exception_code = null, - string? exception_translation = null, string? exception_code_2 = null, - string? exception_translation_2 = null) { - this.prefix = prefix; - this.translation = translation; - this.exception_code = exception_code; - this.exception_translation = exception_translation; - this.exception_code_2 = exception_code_2; - this.exception_translation_2 = exception_translation_2; - } - - } - - private static LocaleLookup[] locale_lookup_table = { - new LocaleLookup( "es", "es-la", "ES", "es-es" ), - new LocaleLookup( "en", "en-gb", "US", "en-us" ), - new LocaleLookup( "fr", "fr-fr", "CA", "fr-ca" ), - new LocaleLookup( "pt", "pt-br", "PT", "pt-pt" ), - new LocaleLookup( "zh", "zh-cn", "HK", "zh-hk", "TW", "zh-tw" ), - new LocaleLookup( "af", "af-za" ), - new LocaleLookup( "ar", "ar-ar" ), - new LocaleLookup( "nb", "nb-no" ), - new LocaleLookup( "no", "nb-no" ), - new LocaleLookup( "id", "id-id" ), - new LocaleLookup( "ms", "ms-my" ), - new LocaleLookup( "ca", "ca-es" ), - new LocaleLookup( "cs", "cs-cz" ), - new LocaleLookup( "cy", "cy-gb" ), - new LocaleLookup( "da", "da-dk" ), - new LocaleLookup( "de", "de-de" ), - new LocaleLookup( "tl", "tl-ph" ), - new LocaleLookup( "ko", "ko-kr" ), - new LocaleLookup( "hr", "hr-hr" ), - new LocaleLookup( "it", "it-it" ), - new LocaleLookup( "lt", "lt-lt" ), - new LocaleLookup( "hu", "hu-hu" ), - new LocaleLookup( "nl", "nl-nl" ), - new LocaleLookup( "ja", "ja-jp" ), - new LocaleLookup( "nb", "nb-no" ), - new LocaleLookup( "no", "nb-no" ), - new LocaleLookup( "pl", "pl-pl" ), - new LocaleLookup( "ro", "ro-ro" ), - new LocaleLookup( "ru", "ru-ru" ), - new LocaleLookup( "sk", "sk-sk" ), - new LocaleLookup( "sl", "sl-si" ), - new LocaleLookup( "sv", "sv-se" ), - new LocaleLookup( "th", "th-th" ), - new LocaleLookup( "vi", "vi-vn" ), - new LocaleLookup( "tr", "tr-tr" ), - new LocaleLookup( "el", "el-gr" ), - new LocaleLookup( "bg", "bg-bg" ), - new LocaleLookup( "sr", "sr-rs" ), - new LocaleLookup( "he", "he-il" ), - new LocaleLookup( "hi", "hi-in" ), - new LocaleLookup( "bn", "bn-in" ), - new LocaleLookup( "pa", "pa-in" ), - new LocaleLookup( "ta", "ta-in" ), - new LocaleLookup( "te", "te-in" ), - new LocaleLookup( "ml", "ml-in" ) - }; - - private static string get_system_locale_as_facebook_locale() { - unowned string? raw_system_locale = Intl.setlocale(LocaleCategory.ALL, ""); - if (raw_system_locale == null || raw_system_locale == "") - return "www"; - - string system_locale = raw_system_locale.split(".")[0]; - - foreach (LocaleLookup locale_lookup in locale_lookup_table) { - if (!system_locale.has_prefix(locale_lookup.prefix)) - continue; - - if (locale_lookup.exception_code != null) { - assert(locale_lookup.exception_translation != null); - - if (system_locale.contains(locale_lookup.exception_code)) - return locale_lookup.exception_translation; - } - - if (locale_lookup.exception_code_2 != null) { - assert(locale_lookup.exception_translation_2 != null); - - if (system_locale.contains(locale_lookup.exception_code_2)) - return locale_lookup.exception_translation_2; - } - - return locale_lookup.translation; - } - - // default - return "www"; - } - - private static string get_login_url() { - var facebook_locale = get_system_locale_as_facebook_locale(); - - return "https://%s.facebook.com/dialog/oauth?client_id=%s&redirect_uri=https://www.facebook.com/connect/login_success.html&display=popup&scope=publish_actions,user_photos,user_videos&response_type=token".printf(facebook_locale, APPLICATION_ID); - } - - public override void on_page_load() { - string loaded_url = get_view ().uri.dup(); - debug("loaded url: " + loaded_url); - - // strip parameters from the loaded url - if (loaded_url.contains("?")) { - int index = loaded_url.index_of_char('?'); - string params = loaded_url[index:loaded_url.length]; - loaded_url = loaded_url.replace(params, ""); - } - - // were we redirected to the facebook login success page? - if (loaded_url.contains("login_success")) { - cache_dirty = true; - login_succeeded(get_view ().uri); - return; - } - - // were we redirected to the login total failure page? - if (loaded_url.contains("login_failure")) { - login_failed(); - return; - } - } - - public static bool is_cache_dirty() { - return cache_dirty; - } - } - - internal class Facebook : Spit.Publishing.Authenticator, GLib.Object { - private Spit.Publishing.PluginHost host; - private Publishing.Authenticator.Shotwell.Facebook.WebAuthenticationPane web_auth_pane = null; - private GLib.HashTable<string, Variant> params; - - private const string SERVICE_WELCOME_MESSAGE = - _("You are not currently logged into Facebook.\n\nIf you don’t yet have a Facebook account, you can create one during the login process. During login, Shotwell Connect may ask you for permission to upload photos and publish to your feed. These permissions are required for Shotwell Connect to function."); - private const string RESTART_ERROR_MESSAGE = - _("You have already logged in and out of Facebook during this Shotwell session.\nTo continue publishing to Facebook, quit and restart Shotwell, then try publishing again."); - - /* Interface functions */ - public Facebook(Spit.Publishing.PluginHost host) { - this.host = host; - this.params = new GLib.HashTable<string, Variant>(str_hash, str_equal); - } - - public void authenticate() { - // Do we have saved user credentials? If so, go ahead and authenticate the session - // with the saved credentials and proceed with the publishing interaction. Otherwise, show - // the Welcome pane - if (is_persistent_session_valid()) { - var access_token = get_persistent_access_token(); - this.params.insert("AccessToken", new Variant.string(access_token)); - this.authenticated(); - return; - } - - // FIXME: Find a way for a proper logout - if (WebAuthenticationPane.is_cache_dirty()) { - host.set_service_locked(false); - host.install_static_message_pane(RESTART_ERROR_MESSAGE, - Spit.Publishing.PluginHost.ButtonMode.CANCEL); - } else { - this.do_show_service_welcome_pane(); - } - } - - public bool can_logout() { - return true; - } - - public GLib.HashTable<string, Variant> get_authentication_parameter() { - return this.params; - } - - public void invalidate_persistent_session() { - debug("invalidating saved Facebook session."); - set_persistent_access_token(""); - } - - public void logout() { - invalidate_persistent_session(); - } - - public void refresh() { - // No-Op with Flickr - } - - /* Private functions */ - private bool is_persistent_session_valid() { - string? token = get_persistent_access_token(); - - if (token != null) - debug("existing Facebook session found in configuration database (access_token = %s).", - token); - else - debug("no existing Facebook session available."); - - return token != null; - } - - private string? get_persistent_access_token() { - return host.get_config_string("access_token", null); - } - - private void set_persistent_access_token(string access_token) { - host.set_config_string("access_token", access_token); - } - - private void do_show_service_welcome_pane() { - debug("ACTION: showing service welcome pane."); - - host.install_welcome_pane(SERVICE_WELCOME_MESSAGE, on_login_clicked); - host.set_service_locked(false); - } - - private void on_login_clicked() { - debug("EVENT: user clicked 'Login' on welcome pane."); - - do_hosted_web_authentication(); - } - - private void do_hosted_web_authentication() { - debug("ACTION: doing hosted web authentication."); - - this.host.set_service_locked(false); - - this.web_auth_pane = new WebAuthenticationPane(); - this.web_auth_pane.login_succeeded.connect(on_web_auth_pane_login_succeeded); - this.web_auth_pane.login_failed.connect(on_web_auth_pane_login_failed); - - this.host.install_dialog_pane(this.web_auth_pane, - Spit.Publishing.PluginHost.ButtonMode.CANCEL); - - } - - private void on_web_auth_pane_login_succeeded(string success_url) { - debug("EVENT: hosted web login succeeded."); - - do_authenticate_session(success_url); - } - - private void on_web_auth_pane_login_failed() { - debug("EVENT: hosted web login failed."); - - // In this case, "failed" doesn't mean that the user didn't enter the right username and - // password -- Facebook handles that case inside the Facebook Connect web control. Instead, - // it means that no session was initiated in response to our login request. The only - // way this happens is if the user clicks the "Cancel" button that appears inside - // the web control. In this case, the correct behavior is to return the user to the - // service welcome pane so that they can start the web interaction again. - do_show_service_welcome_pane(); - } - - private void do_authenticate_session(string good_login_uri) { - debug("ACTION: preparing to extract session information encoded in uri = '%s'", - good_login_uri); - - // the raw uri is percent-encoded, so decode it - string decoded_uri = Soup.URI.decode(good_login_uri); - - // locate the access token within the URI - string? access_token = null; - int index = decoded_uri.index_of("#access_token="); - if (index >= 0) - access_token = decoded_uri[index:decoded_uri.length]; - if (access_token == null) { - host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( - "Server redirect URL contained no access token")); - return; - } - - // remove any trailing parameters from the session description string - string? trailing_params = null; - index = access_token.index_of_char('&'); - if (index >= 0) - trailing_params = access_token[index:access_token.length]; - if (trailing_params != null) - access_token = access_token.replace(trailing_params, ""); - - // remove the key from the session description string - access_token = access_token.replace("#access_token=", ""); - this.params.insert("AccessToken", new Variant.string(access_token)); - set_persistent_access_token(access_token); - - this.authenticated(); - } - } -} // namespace Publishing.Facebook; diff --git a/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala b/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala index 82448e2..26af6d1 100644 --- a/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala +++ b/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala @@ -21,7 +21,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr { public AuthenticationRequestTransaction(Publishing.RESTSupport.OAuth1.Session session) { base.with_uri(session, "https://www.flickr.com/services/oauth/request_token", Publishing.RESTSupport.HttpMethod.GET); - add_argument("oauth_callback", "shotwell-auth%3A%2F%2Flocal-callback"); + add_argument("oauth_callback", "shotwell-auth://local-callback"); } } @@ -31,7 +31,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr { Publishing.RESTSupport.HttpMethod.GET); add_argument("oauth_verifier", user_verifier); add_argument("oauth_token", session.get_request_phase_token()); - add_argument("oauth_callback", "shotwell-auth%3A%2F%2Flocal-callback"); + add_argument("oauth_callback", "shotwell-auth://local-callback"); } } @@ -64,10 +64,16 @@ namespace Publishing.Authenticator.Shotwell.Flickr { return; } - var uri = new Soup.URI(get_view().get_uri()); - if (uri.scheme == "shotwell-auth" && this.auth_code == null) { - var form_data = Soup.Form.decode (uri.query); - this.auth_code = form_data.lookup("oauth_verifier"); + try { + var uri = GLib.Uri.parse(get_view().get_uri(), GLib.UriFlags.NONE); + if (uri.get_scheme() == "shotwell-auth" && this.auth_code == null) { + var form_data = Soup.Form.decode (uri.get_query()); + this.auth_code = form_data.lookup("oauth_verifier"); + } + } catch (Error err) { + this.error(); + + return; } if (this.auth_code != null) { @@ -76,9 +82,13 @@ namespace Publishing.Authenticator.Shotwell.Flickr { } private void on_shotwell_auth_request_cb(WebKit.URISchemeRequest request) { - var uri = new Soup.URI(request.get_uri()); - var form_data = Soup.Form.decode (uri.query); - this.auth_code = form_data.lookup("oauth_verifier"); + try { + var uri = GLib.Uri.parse(request.get_uri(), GLib.UriFlags.NONE); + var form_data = Soup.Form.decode (uri.get_query()); + this.auth_code = form_data.lookup("oauth_verifier"); + } catch (Error err) { + debug ("Failed to parse URI %s: %s", request.get_uri(), err.message); + } var response = ""; var mins = new MemoryInputStream.from_data(response.data, null); @@ -90,7 +100,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr { private WebAuthenticationPane pane; public Flickr(Spit.Publishing.PluginHost host) { - base(API_KEY, API_SECRET, host); + base("Flickr", API_KEY, API_SECRET, host); } public override void authenticate() { @@ -128,45 +138,27 @@ namespace Publishing.Authenticator.Shotwell.Flickr { private void on_welcome_pane_login_clicked() { debug("EVENT: user clicked 'Login' button in the welcome pane"); - do_run_authentication_request_transaction(); + do_run_authentication_request_transaction.begin(); } - private void do_run_authentication_request_transaction() { + private async void do_run_authentication_request_transaction() { debug("ACTION: running authentication request transaction"); host.set_service_locked(true); host.install_static_message_pane(_("Preparing for login…")); AuthenticationRequestTransaction txn = new AuthenticationRequestTransaction(session); - txn.completed.connect(on_auth_request_txn_completed); - txn.network_error.connect(on_auth_request_txn_error); - try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield txn.execute_async(); + debug("EVENT: OAuth authentication request transaction completed; response = '%s'", + txn.get_response()); + do_parse_token_info_from_auth_request(txn.get_response()); + } catch (Error err) { + debug("EVENT: OAuth authentication request transaction caused a network error"); host.post_error(err); - } - } - - private void on_auth_request_txn_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_auth_request_txn_completed); - txn.network_error.disconnect(on_auth_request_txn_error); - - debug("EVENT: OAuth authentication request transaction completed; response = '%s'", - txn.get_response()); - - do_parse_token_info_from_auth_request(txn.get_response()); - } - private void on_auth_request_txn_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_auth_request_txn_completed); - txn.network_error.disconnect(on_auth_request_txn_error); - - debug("EVENT: OAuth authentication request transaction caused a network error"); - host.post_error(err); - - this.authentication_failed(); + this.authentication_failed(); + } } private void do_parse_token_info_from_auth_request(string response) { @@ -199,7 +191,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr { private void do_web_authentication(string token) { pane = new WebAuthenticationPane(token); host.install_dialog_pane(pane); - pane.authorized.connect(this.do_verify_pin); + pane.authorized.connect((pin) => { this.do_verify_pin.begin(pin); }); pane.error.connect(this.on_web_login_error); } @@ -211,44 +203,28 @@ namespace Publishing.Authenticator.Shotwell.Flickr { host.post_error(new Spit.Publishing.PublishingError.PROTOCOL_ERROR(_("Flickr authorization failed"))); } - private void do_verify_pin(string pin) { + private async void do_verify_pin(string pin) { debug("ACTION: validating authorization PIN %s", pin); host.set_service_locked(true); host.install_static_message_pane(_("Verifying authorization…")); AccessTokenFetchTransaction txn = new AccessTokenFetchTransaction(session, pin); - txn.completed.connect(on_access_token_fetch_txn_completed); - txn.network_error.connect(on_access_token_fetch_error); try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - } - } - - private void on_access_token_fetch_txn_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_access_token_fetch_txn_completed); - txn.network_error.disconnect(on_access_token_fetch_error); + yield txn.execute_async(); + debug("EVENT: fetching OAuth access token over the network succeeded"); - debug("EVENT: fetching OAuth access token over the network succeeded"); + do_extract_access_phase_credentials_from_response(txn.get_response()); + } catch (Error err) { + debug("EVENT: fetching OAuth access token over the network caused an error."); - do_extract_access_phase_credentials_from_reponse(txn.get_response()); - } - - private void on_access_token_fetch_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_access_token_fetch_txn_completed); - txn.network_error.disconnect(on_access_token_fetch_error); - - debug("EVENT: fetching OAuth access token over the network caused an error."); - - host.post_error(err); - this.authentication_failed(); + host.post_error(err); + this.authentication_failed(); + } } - private void do_extract_access_phase_credentials_from_reponse(string response) { + private void do_extract_access_phase_credentials_from_response(string response) { debug("ACTION: extracting access phase credentials from '%s'", response); string? token = null; diff --git a/plugins/authenticator/shotwell/GoogleAuthenticator.vala b/plugins/authenticator/shotwell/GoogleAuthenticator.vala index a607cd0..3276e67 100644 --- a/plugins/authenticator/shotwell/GoogleAuthenticator.vala +++ b/plugins/authenticator/shotwell/GoogleAuthenticator.vala @@ -7,6 +7,9 @@ namespace Publishing.Authenticator.Shotwell.Google { private const string OAUTH_CLIENT_SECRET = "pwpzZ7W1TCcD5uIfYCu8sM7x"; private const string OAUTH_CALLBACK_URI = REVERSE_CLIENT_ID + ":/auth-callback"; + private const string SCHEMA_KEY_PROFILE_ID = "shotwell-profile-id"; + private const string SCHEMA_KEY_ACCOUNTNAME = "accountname"; + private class WebAuthenticationPane : Common.WebAuthenticationPane { public static bool cache_dirty = false; private string? auth_code = null; @@ -27,10 +30,15 @@ namespace Publishing.Authenticator.Shotwell.Google { return; } - var uri = new Soup.URI(get_view().get_uri()); - if (uri.scheme == REVERSE_CLIENT_ID && this.auth_code == null) { - var form_data = Soup.Form.decode (uri.query); - this.auth_code = form_data.lookup("code"); + try { + var uri = GLib.Uri.parse(get_view().get_uri(), UriFlags.NONE); + if (uri.get_scheme() == REVERSE_CLIENT_ID && this.auth_code == null) { + var form_data = Soup.Form.decode (uri.get_query()); + this.auth_code = form_data.lookup("code"); + } + } catch (Error err) { + debug ("Failed to parse auth code from URI %s: %s", get_view().get_uri(), + err.message); } if (this.auth_code != null) { @@ -39,10 +47,14 @@ namespace Publishing.Authenticator.Shotwell.Google { } private void on_shotwell_auth_request_cb(WebKit.URISchemeRequest request) { - var uri = new Soup.URI(request.get_uri()); - debug("URI: %s", request.get_uri()); - var form_data = Soup.Form.decode (uri.query); - this.auth_code = form_data.lookup("code"); + try { + var uri = GLib.Uri.parse(request.get_uri(), GLib.UriFlags.NONE); + debug("URI: %s", request.get_uri()); + var form_data = Soup.Form.decode (uri.get_query()); + this.auth_code = form_data.lookup("code"); + } catch (Error err) { + debug("Failed to parse request URI: %s", err.message); + } var response = ""; var mins = new MemoryInputStream.from_data(response.data, null); @@ -77,7 +89,7 @@ namespace Publishing.Authenticator.Shotwell.Google { } private class GetAccessTokensTransaction : Publishing.RESTSupport.Transaction { - private const string ENDPOINT_URL = "https://accounts.google.com/o/oauth2/token"; + private const string ENDPOINT_URL = "https://oauth2.googleapis.com/token"; public GetAccessTokensTransaction(Session session, string auth_code) { base.with_endpoint_url(session, ENDPOINT_URL); @@ -91,7 +103,7 @@ namespace Publishing.Authenticator.Shotwell.Google { } private class RefreshAccessTokenTransaction : Publishing.RESTSupport.Transaction { - private const string ENDPOINT_URL = "https://accounts.google.com/o/oauth2/token"; + private const string ENDPOINT_URL = "https://oauth2.googleapis.com/token"; public RefreshAccessTokenTransaction(Session session) { base.with_endpoint_url(session, ENDPOINT_URL); @@ -112,12 +124,18 @@ namespace Publishing.Authenticator.Shotwell.Google { } internal class Google : Spit.Publishing.Authenticator, Object { + private const string PASSWORD_SCHEME = "org.gnome.Shotwell.Google"; + private string scope = null; + + // Prepare for multiple user accounts + private string accountname = "default"; private Spit.Publishing.PluginHost host = null; private GLib.HashTable<string, Variant> params = null; private WebAuthenticationPane web_auth_pane = null; private Session session = null; private string welcome_message = null; + private Secret.Schema? schema = null; public Google(string scope, string welcome_message, @@ -127,13 +145,24 @@ namespace Publishing.Authenticator.Shotwell.Google { this.scope = scope; this.session = new Session(); this.welcome_message = welcome_message; + this.schema = new Secret.Schema(PASSWORD_SCHEME, Secret.SchemaFlags.NONE, + SCHEMA_KEY_PROFILE_ID, Secret.SchemaAttributeType.STRING, + SCHEMA_KEY_ACCOUNTNAME, Secret.SchemaAttributeType.STRING, + "scope", Secret.SchemaAttributeType.STRING); } public void authenticate() { - var refresh_token = host.get_config_string("refresh_token", null); + string? refresh_token = null; + try { + refresh_token = Secret.password_lookup_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope); + } catch (Error err) { + critical("Failed to lookup refresh_token from password store: %s", err.message); + } if (refresh_token != null && refresh_token != "") { on_refresh_token_available(refresh_token); - do_exchange_refresh_token_for_access_token(); + do_exchange_refresh_token_for_access_token.begin(); return; } @@ -157,22 +186,32 @@ namespace Publishing.Authenticator.Shotwell.Google { public void logout() { session.deauthenticate(); - host.set_config_string("refresh_token", ""); + try { + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope); + } catch (Error err) { + critical("Failed to remove password for scope %s: %s", this.scope, err.message); + } } public void refresh() { // TODO: Needs to re-auth } + public void set_accountname(string accountname) { + this.accountname = accountname; + } + private void do_hosted_web_authentication() { debug("ACTION: running OAuth authentication flow in hosted web pane."); string user_authorization_url = "https://accounts.google.com/o/oauth2/auth?" + "response_type=code&" + "client_id=" + OAUTH_CLIENT_ID + "&" + - "redirect_uri=" + Soup.URI.encode(OAUTH_CALLBACK_URI, null) + "&" + - "scope=" + Soup.URI.encode(this.scope, null) + "+" + - Soup.URI.encode("https://www.googleapis.com/auth/userinfo.profile", null) + "&" + + "redirect_uri=" + GLib.Uri.escape_string(OAUTH_CALLBACK_URI, null) + "&" + + "scope=" + GLib.Uri.escape_string(this.scope, null) + "+" + + GLib.Uri.escape_string("https://www.googleapis.com/auth/userinfo.profile", null) + "&" + "state=connect&" + "access_type=offline&" + "approval_prompt=force"; @@ -189,48 +228,31 @@ namespace Publishing.Authenticator.Shotwell.Google { debug("EVENT: user authorized scope %s with auth_code %s", scope, auth_code); - do_get_access_tokens(auth_code); + do_get_access_tokens.begin(auth_code); } private void on_web_auth_pane_error() { host.post_error(web_auth_pane.load_error); } - private void do_get_access_tokens(string auth_code) { + private async void do_get_access_tokens(string auth_code) { debug("ACTION: exchanging authorization code for access & refresh tokens"); host.install_login_wait_pane(); GetAccessTokensTransaction tokens_txn = new GetAccessTokensTransaction(session, auth_code); - tokens_txn.completed.connect(on_get_access_tokens_complete); - tokens_txn.network_error.connect(on_get_access_tokens_error); try { - tokens_txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield tokens_txn.execute_async(); + debug("EVENT: network transaction to exchange authorization code for access tokens " + + "completed successfully."); + do_extract_tokens(tokens_txn.get_response()); + } catch (Error err) { + debug("EVENT: network transaction to exchange authorization code for access tokens " + + "failed; response = '%s'", tokens_txn.get_response()); host.post_error(err); } - } - - private void on_get_access_tokens_complete(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_get_access_tokens_complete); - txn.network_error.disconnect(on_get_access_tokens_error); - debug("EVENT: network transaction to exchange authorization code for access tokens " + - "completed successfully."); - - do_extract_tokens(txn.get_response()); - } - - private void on_get_access_tokens_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_get_access_tokens_complete); - txn.network_error.disconnect(on_get_access_tokens_error); - - debug("EVENT: network transaction to exchange authorization code for access tokens " + - "failed; response = '%s'", txn.get_response()); - - host.post_error(err); } private void do_extract_tokens(string response_body) { @@ -297,45 +319,28 @@ namespace Publishing.Authenticator.Shotwell.Google { session.access_token = token; this.params.insert("AccessToken", new Variant.string(token)); - do_fetch_username(); + do_fetch_username.begin(); } - private void do_fetch_username() { + private async void do_fetch_username() { debug("ACTION: running network transaction to fetch username."); host.install_login_wait_pane(); host.set_service_locked(true); UsernameFetchTransaction txn = new UsernameFetchTransaction(session); - txn.completed.connect(on_fetch_username_transaction_completed); - txn.network_error.connect(on_fetch_username_transaction_error); try { - txn.execute(); + yield txn.execute_async(); + debug("EVENT: username fetch transaction completed successfully."); + do_extract_username(txn.get_response()); } catch (Error err) { + debug("EVENT: username fetch transaction caused a network error"); + host.post_error(err); } } - private void on_fetch_username_transaction_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_fetch_username_transaction_completed); - txn.network_error.disconnect(on_fetch_username_transaction_error); - - debug("EVENT: username fetch transaction completed successfully."); - - do_extract_username(txn.get_response()); - } - - private void on_fetch_username_transaction_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_fetch_username_transaction_completed); - txn.network_error.disconnect(on_fetch_username_transaction_error); - - debug("EVENT: username fetch transaction caused a network error"); - - host.post_error(err); - } - private void do_extract_username(string response_body) { debug("ACTION: extracting username from body of server response"); @@ -368,61 +373,57 @@ namespace Publishing.Authenticator.Shotwell.Google { // by the time we get a username, the session should be authenticated, or else something // really tragic has happened assert(session.is_authenticated()); - host.set_config_string("refresh_token", session.refresh_token); + try { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (Google account scope %s@%s)".printf(this.accountname, this.scope), + session.refresh_token, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope); + } catch (Error err) { + critical("Failed to look up password for scope %s: %s", this.scope, err.message); + } this.authenticated(); web_auth_pane.clear(); } - - private void do_exchange_refresh_token_for_access_token() { + private async void do_exchange_refresh_token_for_access_token() { debug("ACTION: exchanging OAuth refresh token for OAuth access token."); host.install_login_wait_pane(); RefreshAccessTokenTransaction txn = new RefreshAccessTokenTransaction(session); - - txn.completed.connect(on_refresh_access_token_transaction_completed); - txn.network_error.connect(on_refresh_access_token_transaction_error); - try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - } - } - - private void on_refresh_access_token_transaction_completed(Publishing.RESTSupport.Transaction - txn) { - txn.completed.disconnect(on_refresh_access_token_transaction_completed); - txn.network_error.disconnect(on_refresh_access_token_transaction_error); + yield txn.execute_async(); + debug("EVENT: refresh access token transaction completed successfully."); - debug("EVENT: refresh access token transaction completed successfully."); - - if (session.is_authenticated()) // ignore these events if the session is already auth'd - return; - - do_extract_tokens(txn.get_response()); - } - - private void on_refresh_access_token_transaction_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_refresh_access_token_transaction_completed); - txn.network_error.disconnect(on_refresh_access_token_transaction_error); - - debug("EVENT: refresh access token transaction caused a network error."); - - if (session.is_authenticated()) // ignore these events if the session is already auth'd - return; - if (txn.get_status_code() == Soup.Status.BAD_REQUEST || - txn.get_status_code() == Soup.Status.UNAUTHORIZED) { - // Refresh token invalid, starting over - host.set_config_string("refresh_token", ""); - Idle.add (() => { this.authenticate(); return false; }); + if (session.is_authenticated()) // ignore these events if the session is already auth'd + return; + + do_extract_tokens(txn.get_response()); + } catch (Error err) { + debug("EVENT: refresh access token transaction caused a network error."); + + if (session.is_authenticated()) // ignore these events if the session is already auth'd + return; + + if (txn.get_status_code() == Soup.Status.BAD_REQUEST || + txn.get_status_code() == Soup.Status.UNAUTHORIZED) { + // Refresh token invalid, starting over + try { + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope); + } catch (Error err) { + critical("Failed to remove password for accountname@scope %s@%s: %s", this.accountname, this.scope, err.message); + } + + Idle.add (() => { this.authenticate(); return false; }); + } + + web_auth_pane.clear(); + host.post_error(err); } - - web_auth_pane.clear(); - host.post_error(err); } private void do_show_service_welcome_pane() { @@ -436,7 +437,5 @@ namespace Publishing.Authenticator.Shotwell.Google { this.do_hosted_web_authentication(); } - - } } diff --git a/plugins/authenticator/shotwell/OAuth1Authenticator.vala b/plugins/authenticator/shotwell/OAuth1Authenticator.vala index 39752ec..e79c6fd 100644 --- a/plugins/authenticator/shotwell/OAuth1Authenticator.vala +++ b/plugins/authenticator/shotwell/OAuth1Authenticator.vala @@ -11,10 +11,23 @@ namespace Publishing.Authenticator.Shotwell.OAuth1 { protected GLib.HashTable<string, Variant> params; protected Publishing.RESTSupport.OAuth1.Session session; protected Spit.Publishing.PluginHost host; - - protected Authenticator(string api_key, string api_secret, Spit.Publishing.PluginHost host) { + private Secret.Schema? schema = null; + private const string SECRET_TYPE_USERNAME = "username"; + private const string SECRET_TYPE_AUTH_TOKEN = "auth-token"; + private const string SECRET_TYPE_AUTH_TOKEN_SECRET = "auth-token-secret"; + private const string SCHEMA_KEY_ACCOUNTNAME = "accountname"; + private const string SCHEMA_KEY_PROFILE_ID = "shotwell-profile-id"; + private string service = null; + private string accountname = "default"; + + protected Authenticator(string service, string api_key, string api_secret, Spit.Publishing.PluginHost host) { base(); this.host = host; + this.service = service; + this.schema = new Secret.Schema("org.gnome.Shotwell." + service, Secret.SchemaFlags.NONE, + SCHEMA_KEY_PROFILE_ID, Secret.SchemaAttributeType.STRING, + SCHEMA_KEY_ACCOUNTNAME, Secret.SchemaAttributeType.STRING, + "type", Secret.SchemaAttributeType.STRING); params = new GLib.HashTable<string, Variant>(str_hash, str_equal); params.insert("ConsumerKey", api_key); @@ -42,11 +55,16 @@ namespace Publishing.Authenticator.Shotwell.OAuth1 { public abstract void refresh(); + public virtual void set_accountname(string name) { + this.accountname = name; + } + public void invalidate_persistent_session() { - set_persistent_access_phase_token(""); - set_persistent_access_phase_token_secret(""); - set_persistent_access_phase_username(""); + set_persistent_access_phase_token(null); + set_persistent_access_phase_token_secret(null); + set_persistent_access_phase_username(null); } + protected bool is_persistent_session_valid() { return (get_persistent_access_phase_username() != null && get_persistent_access_phase_token() != null && @@ -54,30 +72,99 @@ namespace Publishing.Authenticator.Shotwell.OAuth1 { } protected string? get_persistent_access_phase_username() { - return host.get_config_string("access_phase_username", null); + try { + return Secret.password_lookup_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "type", SECRET_TYPE_USERNAME); + } catch (Error err) { + critical("Failed to lookup username from password store: %s", err.message); + return null; + } } - protected void set_persistent_access_phase_username(string username) { - host.set_config_string("access_phase_username", username); + protected void set_persistent_access_phase_username(string? username) { + try { + if (username == null || username == "") { + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_USERNAME); + } else { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (%s@%s)".printf(this.accountname, this.service), + username, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "type", SECRET_TYPE_USERNAME); + } + } catch (Error err) { + critical("Failed to store username in store: %s", err.message); + } } protected string? get_persistent_access_phase_token() { - return host.get_config_string("access_phase_token", null); + try { + return Secret.password_lookup_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN); + } catch (Error err) { + critical("Failed to lookup auth-token from password store: %s", err.message); + return null; + } } - protected void set_persistent_access_phase_token(string token) { - host.set_config_string("access_phase_token", token); + protected void set_persistent_access_phase_token(string? token) { + try { + if (token == null || token == "") { + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN); + } else { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (%s@%s)".printf(this.accountname, this.service), + token, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN); + } + } catch (Error err) { + critical("Failed to store auth-token store: %s", err.message); + } } protected string? get_persistent_access_phase_token_secret() { - return host.get_config_string("access_phase_token_secret", null); + try { + return Secret.password_lookup_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN_SECRET); + } catch (Error err) { + critical("Failed to lookup auth-token-secret from password store: %s", err.message); + return null; + } } - protected void set_persistent_access_phase_token_secret(string secret) { - host.set_config_string("access_phase_token_secret", secret); + protected void set_persistent_access_phase_token_secret(string? secret) { + try { + if (secret == null || secret == "") { + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN_SECRET); + } else { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (%s@%s)".printf(this.accountname, this.service), + secret, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, + "type", SECRET_TYPE_AUTH_TOKEN_SECRET); + } + } catch (Error err) { + critical("Failed to store auth-token-secret store: %s", err.message); + } } - protected void on_session_authenticated() { params.insert("AuthToken", session.get_access_phase_token()); params.insert("AuthTokenSecret", session.get_access_phase_token_secret()); @@ -90,7 +177,5 @@ namespace Publishing.Authenticator.Shotwell.OAuth1 { this.authenticated(); } - } - } diff --git a/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala b/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala index 759822c..01fa3c3 100644 --- a/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala +++ b/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala @@ -26,14 +26,12 @@ namespace Publishing.Authenticator { switch (provider) { case "flickr": return new Shotwell.Flickr.Flickr(host); - case "facebook": - return new Shotwell.Facebook.Facebook(host); case "youtube": return new Shotwell.Google.Google("https://www.googleapis.com/auth/youtube", _("You are not currently logged into YouTube.\n\nYou must have already signed up for a Google account and set it up for use with YouTube to continue. You can set up most accounts by using your browser to log into the YouTube site at least once.\n\nShotwell uses the YouTube API services <a href=\"https://developers.google.com/youtube\">https://developers.google.com/youtube</a> for accessing your YouTube channel and upload the videos. By using Shotwell to access YouTube, you agree to be bound to the YouTube Terms of Service as available at <a href=\"https://www.youtube.com/t/terms\">https://www.youtube.com/t/terms</a>\n\nShotwell's privacy policy regarding the use of data related to your Google account in general and YouTube in particular can be found in our <a href=\"help:shotwell/privacy-policy\">online services privacy policy</a>\n\nFor Google's own privacy policy, please refer to <a href=\"https://policies.google.com/privacy\">https://policies.google.com/privacy</a>"), host); case "tumblr": return new Shotwell.Tumblr.Tumblr(host); case "google-photos": - return new Shotwell.Google.Google("https://www.googleapis.com/auth/photoslibrary", _("You are not currently logged into Google Photos.\n\nYou must have already signed up for a Google account and set it up for use with Google Photos. Shotwell uses the Google Photos API services <a href=\"https://developers.google.com/photos/\">https://developers.google.com/photos/</a> for all interaction with your Google Photos data.You will have to grant access Shotwell to your Google Photos library.\n\nShotwell's privacy policy regarding the use of data related to your Google account in general and Google Photos in particular can be found in our <a href=\"help:shotwell/privacy-policy\">online services privacy policy</a>For Google's own privacy policy, please refer to <a href=\"https://policies.google.com/privacy\">https://policies.google.com/privacy</a>"), host); + return new Shotwell.Google.Google("https://www.googleapis.com/auth/photoslibrary", _("You are not currently logged into Google Photos.\n\nYou must have already signed up for a Google account and set it up for use with Google Photos. Shotwell uses the Google Photos API services <a href=\"https://developers.google.com/photos/\">https://developers.google.com/photos/</a> for all interaction with your Google Photos data. You will have to grant access Shotwell to your Google Photos library.\n\nShotwell's privacy policy regarding the use of data related to your Google account in general and Google Photos in particular can be found in our <a href=\"help:shotwell/privacy-policy\">online services privacy policy</a>. For Google's own privacy policy, please refer to <a href=\"https://policies.google.com/privacy\">https://policies.google.com/privacy</a>"), host); default: return null; } diff --git a/plugins/authenticator/shotwell/TumblrAuthenticator.vala b/plugins/authenticator/shotwell/TumblrAuthenticator.vala index 8907a80..e77814b 100644 --- a/plugins/authenticator/shotwell/TumblrAuthenticator.vala +++ b/plugins/authenticator/shotwell/TumblrAuthenticator.vala @@ -119,7 +119,7 @@ namespace Publishing.Authenticator.Shotwell.Tumblr { public AccessTokenFetchTransaction(Publishing.RESTSupport.OAuth1.Session session, string username, string password) { base.with_uri(session, "https://www.tumblr.com/oauth/access_token", Publishing.RESTSupport.HttpMethod.POST); - add_argument("x_auth_username", Soup.URI.encode(username, ENCODE_RFC_3986_EXTRA)); + add_argument("x_auth_username", username); add_argument("x_auth_password", password); add_argument("x_auth_mode", "client_auth"); } @@ -127,7 +127,7 @@ namespace Publishing.Authenticator.Shotwell.Tumblr { internal class Tumblr : Publishing.Authenticator.Shotwell.OAuth1.Authenticator { public Tumblr(Spit.Publishing.PluginHost host) { - base(API_KEY, API_SECRET, host); + base("Tumblr", API_KEY, API_SECRET, host); } public override void authenticate() { @@ -190,7 +190,7 @@ namespace Publishing.Authenticator.Shotwell.Tumblr { private void on_authentication_pane_login_clicked( string username, string password ) { debug("EVENT: on_authentication_pane_login_clicked"); - do_network_login(username, password); + do_network_login.begin(username, password); } /** @@ -201,41 +201,24 @@ namespace Publishing.Authenticator.Shotwell.Tumblr { * @param username the name of the Tumblr user used to login * @param password the password of the Tumblr user used to login */ - private void do_network_login(string username, string password) { + private async void do_network_login(string username, string password) { debug("ACTION: logging in"); host.set_service_locked(true); host.install_login_wait_pane(); AccessTokenFetchTransaction txn = new AccessTokenFetchTransaction(session,username,password); - txn.completed.connect(on_auth_request_txn_completed); - txn.network_error.connect(on_auth_request_txn_error); - try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield txn.execute_async(); + debug("EVENT: OAuth authentication request transaction completed; response = '%s'", + txn.get_response()); + + do_parse_token_info_from_auth_request(txn.get_response()); + } catch (Error err) { + debug("EVENT: OAuth authentication request transaction caused a network error"); host.post_error(err); } } - private void on_auth_request_txn_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_auth_request_txn_completed); - txn.network_error.disconnect(on_auth_request_txn_error); - - debug("EVENT: OAuth authentication request transaction completed; response = '%s'", - txn.get_response()); - - do_parse_token_info_from_auth_request(txn.get_response()); - } - - private void on_auth_request_txn_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_auth_request_txn_completed); - txn.network_error.disconnect(on_auth_request_txn_error); - - debug("EVENT: OAuth authentication request transaction caused a network error"); - host.post_error(err); - } - private void do_parse_token_info_from_auth_request(string response) { debug("ACTION: extracting access phase credentials from '%s'", response); @@ -255,7 +238,7 @@ namespace Publishing.Authenticator.Shotwell.Tumblr { "least one of these is absent")); this.authentication_failed(); } else { - session.set_access_phase_credentials(token, token_secret, ""); + session.set_access_phase_credentials(token, token_secret, "unused"); } } } diff --git a/plugins/authenticator/shotwell/meson.build b/plugins/authenticator/shotwell/meson.build index 4404242..037ec3b 100644 --- a/plugins/authenticator/shotwell/meson.build +++ b/plugins/authenticator/shotwell/meson.build @@ -1,6 +1,5 @@ authenticator_shotwell_sources = [ 'ShotwellAuthenticatorFactory.vala', - 'FacebookPublishingAuthenticator.vala', 'FlickrPublishingAuthenticator.vala', 'GoogleAuthenticator.vala', 'OAuth1Authenticator.vala', @@ -9,10 +8,10 @@ authenticator_shotwell_sources = [ authenticator_shotwell_resources = gnome.compile_resources('authenticator-resource', 'org.gnome.Shotwell.Authenticator.gresource.xml', - source_dir : meson.source_root()) + source_dir : meson.project_source_root()) authenticator_shotwell_deps = [gee, gtk, gio, soup, json_glib, sw_plugin, - sw_plugin_common_dep, json_glib, xml, webkit] + sw_plugin_common_dep, json_glib, xml, webkit, secret] authenticator = library('shotwell-authenticator', authenticator_shotwell_sources + authenticator_shotwell_resources, diff --git a/plugins/common/OAuth1Support.vala b/plugins/common/OAuth1Support.vala index e5a8545..05ba34f 100644 --- a/plugins/common/OAuth1Support.vala +++ b/plugins/common/OAuth1Support.vala @@ -6,7 +6,6 @@ */ namespace Publishing.RESTSupport.OAuth1 { - internal const string ENCODE_RFC_3986_EXTRA = "!*'();:@&=+$,/?%#[] \\"; public class Session : Publishing.RESTSupport.Session { private string? request_phase_token = null; @@ -62,7 +61,7 @@ namespace Publishing.RESTSupport.OAuth1 { Publishing.RESTSupport.Argument[] sorted_args = Publishing.RESTSupport.Argument.sort(base_string_arguments); - var arguments_string = Argument.serialize_list(sorted_args); + var arguments_string = Argument.serialize_for_sbs(sorted_args); string? signing_key = null; if (access_phase_token_secret != null) { @@ -80,9 +79,9 @@ namespace Publishing.RESTSupport.OAuth1 { signing_key = consumer_secret + "&"; } - string signature_base_string = http_method + "&" + Soup.URI.encode( - txn.get_endpoint_url(), ENCODE_RFC_3986_EXTRA) + "&" + - Soup.URI.encode(arguments_string, ENCODE_RFC_3986_EXTRA); + string signature_base_string = http_method + "&" + GLib.Uri.escape_string( + txn.get_endpoint_url()) + "&" + + GLib.Uri.escape_string (arguments_string); debug("signature base string = '%s'", signature_base_string); @@ -90,7 +89,7 @@ namespace Publishing.RESTSupport.OAuth1 { // compute the signature string signature = RESTSupport.hmac_sha1(signing_key, signature_base_string); - signature = Soup.URI.encode(signature, ENCODE_RFC_3986_EXTRA); + signature = GLib.Uri.escape_string(signature); debug("signature = '%s'", signature); @@ -111,11 +110,8 @@ namespace Publishing.RESTSupport.OAuth1 { } public string get_oauth_nonce() { - TimeVal currtime = TimeVal(); - currtime.get_current_time(); - - return Checksum.compute_for_string(ChecksumType.MD5, currtime.tv_sec.to_string() + - currtime.tv_usec.to_string()); + return Checksum.compute_for_string(ChecksumType.MD5, + GLib.get_real_time().to_string()); } public string get_oauth_timestamp() { @@ -178,12 +174,11 @@ namespace Publishing.RESTSupport.OAuth1 { } } - - public override void execute() throws Spit.Publishing.PublishingError { + public override async void execute_async() throws Spit.Publishing.PublishingError { var signature = ((Session) get_parent_session()).sign_transaction(this); add_argument("oauth_signature", signature); - base.execute(); + yield base.execute_async(); } } @@ -212,7 +207,7 @@ namespace Publishing.RESTSupport.OAuth1 { } public string get_authorization_header_string() { - return "OAuth " + Argument.serialize_list(auth_header_fields, true, ", "); + return "OAuth " + Argument.serialize_list(auth_header_fields, false, true, ", "); } public void authorize() { diff --git a/plugins/common/RESTSupport.vala b/plugins/common/RESTSupport.vala index 0d0a3fb..cc810fe 100644 --- a/plugins/common/RESTSupport.vala +++ b/plugins/common/RESTSupport.vala @@ -26,6 +26,9 @@ public abstract class Session { private string? endpoint_url = null; private Soup.Session soup_session = null; private bool transactions_stopped = false; + private Bytes? body = null; + private Error? transport_error= null; + private bool insecure = false; public signal void wire_message_unqueued(Soup.Message message); public signal void authenticated(); @@ -34,7 +37,18 @@ public abstract class Session { protected Session(string? endpoint_url = null) { this.endpoint_url = endpoint_url; soup_session = new Soup.Session (); - this.soup_session.ssl_use_system_ca_file = true; + if (Environment.get_variable("SHOTWELL_SOUP_LOG") != null) { + var logger = new Soup.Logger(Soup.LoggerLogLevel.BODY); + logger.set_request_filter((logger, msg) => { + var content_type = msg.get_request_headers().get_content_type(null); + if (content_type != null && content_type == "application/octet-stream") { + return Soup.LoggerLogLevel.HEADERS; + } + + return Soup.LoggerLogLevel.BODY; + }); + soup_session.add_feature (logger); + } } protected void notify_wire_message_unqueued(Soup.Message message) { @@ -64,19 +78,33 @@ public abstract class Session { return transactions_stopped; } - public void send_wire_message(Soup.Message message) { - if (are_transactions_stopped()) + public async void send_wire_message_async(Soup.Message message) { + if (are_transactions_stopped()) { return; + } - soup_session.request_unqueued.connect(notify_wire_message_unqueued); - soup_session.send_message(message); - - soup_session.request_unqueued.disconnect(notify_wire_message_unqueued); + try { + this.body = yield soup_session.send_and_read_async(message, GLib.Priority.DEFAULT, null); + } catch (Error error) { + debug ("Failed to send_and_read: %s", error.message); + this.transport_error = error; + } } public void set_insecure () { - this.soup_session.ssl_use_system_ca_file = false; - this.soup_session.ssl_strict = false; + this.insecure = true; + } + + public bool get_is_insecure() { + return this.insecure; + } + + public Error? get_transport_error() { + return this.transport_error; + } + + public Bytes? get_body() { + return this.body; } } @@ -123,11 +151,19 @@ public class Argument { this.value = value; } - public static string serialize_list(Argument[] args, bool escape = false, string? separator = "&") { + public static string serialize_for_sbs(Argument[] args) { + return Argument.serialize_list(args, true, false, "&"); + } + + public static string serialize_for_authorization_header(Argument[] args) { + return Argument.serialize_list(args, false, true, ", "); + } + + public static string serialize_list(Argument[] args, bool encode, bool escape, string? separator) { var builder = new StringBuilder(""); foreach (var arg in args) { - builder.append(arg.to_string(escape)); + builder.append(arg.to_string(escape, encode)); builder.append(separator); } @@ -150,8 +186,10 @@ public class Argument { return sorted_args.to_array(); } - public string to_string (bool escape = false) { - return "%s=%s%s%s".printf (this.key, escape ? "\"" : "", this.value, escape ? "\"" : ""); + public string to_string (bool escape = false, bool encode = false) { + return "%s=%s%s%s".printf (this.key, escape ? "\"" : "", + encode ? GLib.Uri.escape_string(this.value) : this.value, + escape ? "\"" : ""); } } @@ -160,13 +198,12 @@ public class Transaction { private bool is_executed = false; private weak Session parent_session = null; private Soup.Message message = null; - private int bytes_written = 0; - private Spit.Publishing.PublishingError? err = null; + private uint bytes_written = 0; + private ulong request_length; private string? endpoint_url = null; private bool use_custom_payload; - public signal void chunk_transmitted(int bytes_written_so_far, int total_bytes); - public signal void network_error(Spit.Publishing.PublishingError err); + public signal void chunk_transmitted(uint bytes_written_so_far, uint total_bytes); public signal void completed(); @@ -188,31 +225,16 @@ public class Transaction { message = new Soup.Message(method.to_string(), endpoint_url); } - private void on_wrote_body_data(Soup.Buffer written_data) { - bytes_written += (int) written_data.length; - while (Gtk.events_pending()) { - Gtk.main_iteration(); - } - chunk_transmitted(bytes_written, (int) message.request_body.length); - } - - private void on_message_unqueued(Soup.Message message) { - if (this.message != message) - return; - - try { - check_response(message); - } catch (Spit.Publishing.PublishingError err) { - warning("Publishing error: %s", err.message); - warning("response validation failed. bad response = '%s'.", get_response()); - this.err = err; - } + private void on_wrote_body_data(Soup.Message message, uint chunk_size) { + bytes_written += chunk_size; + chunk_transmitted(bytes_written, (uint)request_length); } /* Texts copied from epiphany */ public string detailed_error_from_tls_flags (out TlsCertificate cert) { TlsCertificateFlags tls_errors; - this.message.get_https_status (out cert, out tls_errors); + cert = this.message.get_tls_peer_certificate(); + tls_errors = this.message.get_tls_peer_certificate_errors(); var list = new Gee.ArrayList<string> (); if (TlsCertificateFlags.BAD_IDENTITY in tls_errors) { @@ -263,38 +285,38 @@ public class Transaction { } protected void check_response(Soup.Message message) throws Spit.Publishing.PublishingError { - switch (message.status_code) { - case Soup.KnownStatusCode.OK: - case Soup.KnownStatusCode.CREATED: // HTTP code 201 (CREATED) signals that a new - // resource was created in response to a PUT or POST - break; - - case Soup.KnownStatusCode.CANT_RESOLVE: - case Soup.KnownStatusCode.CANT_RESOLVE_PROXY: + var transport_error = parent_session.get_transport_error(); + if (transport_error != null) { + if (transport_error is GLib.ResolverError) { throw new Spit.Publishing.PublishingError.NO_ANSWER("Unable to resolve %s (error code %u)", - get_endpoint_url(), message.status_code); - - case Soup.KnownStatusCode.CANT_CONNECT: - case Soup.KnownStatusCode.CANT_CONNECT_PROXY: + get_endpoint_url(), message.status_code); + } + if (transport_error is GLib.IOError) { throw new Spit.Publishing.PublishingError.NO_ANSWER("Unable to connect to %s (error code %u)", get_endpoint_url(), message.status_code); - case Soup.KnownStatusCode.SSL_FAILED: + } + if (transport_error is GLib.TlsError) { throw new Spit.Publishing.PublishingError.SSL_FAILED ("Unable to connect to %s: Secure connection failed", get_endpoint_url ()); + } + + throw new Spit.Publishing.PublishingError.NO_ANSWER("Failure communicating with %s (error code %u)", + get_endpoint_url(), message.status_code); + } + switch (message.status_code) { + case Soup.Status.OK: + case Soup.Status.CREATED: // HTTP code 201 (CREATED) signals that a new + // resource was created in response to a PUT or POST + break; default: - // status codes below 100 are used by Soup, 100 and above are defined HTTP codes - if (message.status_code >= 100) { - throw new Spit.Publishing.PublishingError.NO_ANSWER("Service %s returned HTTP status code %u %s", - get_endpoint_url(), message.status_code, message.reason_phrase); - } else { - throw new Spit.Publishing.PublishingError.NO_ANSWER("Failure communicating with %s (error code %u)", - get_endpoint_url(), message.status_code); - } + throw new Spit.Publishing.PublishingError.NO_ANSWER("Service %s returned HTTP status code %u %s", + get_endpoint_url(), message.status_code, message.reason_phrase); } // All valid communication involves body data in the response - if (message.response_body.data == null || message.response_body.data.length == 0) + var body = parent_session.get_body(); + if (body == null || body.get_size() == 0) throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE("No response data from %s", get_endpoint_url()); } @@ -311,22 +333,26 @@ public class Transaction { this.is_executed = is_executed; } - protected void send() throws Spit.Publishing.PublishingError { - parent_session.wire_message_unqueued.connect(on_message_unqueued); - message.wrote_body_data.connect(on_wrote_body_data); - parent_session.send_wire_message(message); - - parent_session.wire_message_unqueued.disconnect(on_message_unqueued); - message.wrote_body_data.disconnect(on_wrote_body_data); - - if (err != null) - network_error(err); - else - completed(); - - if (err != null) - throw err; - } + private bool on_accecpt_certificate(Soup.Message message, TlsCertificate cert, TlsCertificateFlags errors) { + debug ("HTTPS connect error. Will ignore? %s", this.parent_session.get_is_insecure().to_string()); + return this.parent_session.get_is_insecure(); + } + + protected async void send_async() throws Spit.Publishing.PublishingError { + var id = message.wrote_body_data.connect((message, chunk_size) => { + bytes_written = chunk_size; + + chunk_transmitted(bytes_written, (uint)request_length); + }); + message.accept_certificate.connect(on_accecpt_certificate); + + yield parent_session.send_wire_message_async(message); + check_response(message); + + message.disconnect(id); + message.accept_certificate.disconnect(on_accecpt_certificate); + completed(); + } public HttpMethod get_method() { return HttpMethod.from_string(message.method); @@ -354,7 +380,8 @@ public class Transaction { } ulong length = (payload_length > 0) ? payload_length : custom_payload.length; - message.set_request(payload_content_type, Soup.MemoryUse.COPY, custom_payload.data[0:length]); + message.set_request_body_from_bytes(payload_content_type, new Bytes (custom_payload.data[0:length])); + this.request_length = length; use_custom_payload = true; } @@ -364,8 +391,9 @@ public class Transaction { // alone and let the Transaction class manage it for you. You should only need // to install a new message if your subclass has radically different behavior from // normal Transactions -- like multipart encoding. - protected void set_message(Soup.Message message) { + protected void set_message(Soup.Message message, ulong request_length) { this.message = message; + this.request_length = request_length; } public bool get_is_executed() { @@ -377,58 +405,67 @@ public class Transaction { return message.status_code; } - public virtual void execute() throws Spit.Publishing.PublishingError { - // if a custom payload is being used, we don't need to peform the tasks that are necessary - // to prepare a traditional key-value pair REST request; Instead (since we don't - // know anything about the custom payload), we just put it on the wire and return - if (use_custom_payload) { - is_executed = true; - send(); - - return; - } - + private GLib.Uri? prepare_rest_message() { // REST POST requests must transmit at least one argument if (get_method() == HttpMethod.POST) assert(arguments.length > 0); // concatenate the REST arguments array into an HTTP formdata string - string formdata_string = ""; + var formdata_string = new StringBuilder(""); for (int i = 0; i < arguments.length; i++) { - formdata_string += arguments[i].to_string (); + formdata_string.append(arguments[i].to_string()); if (i < arguments.length - 1) - formdata_string += "&"; + formdata_string.append("&"); } // for GET requests with arguments, append the formdata string to the endpoint url after a // query divider ('?') -- but make sure to save the old (caller-specified) endpoint URL // and restore it after the GET so that the underlying Soup message remains consistent - string old_url = null; + GLib.Uri? old_url = null; string url_with_query = null; if (get_method() == HttpMethod.GET && arguments.length > 0) { - old_url = message.get_uri().to_string(false); - url_with_query = get_endpoint_url() + "?" + formdata_string; - message.set_uri(new Soup.URI(url_with_query)); + old_url = message.get_uri(); + url_with_query = get_endpoint_url() + "?" + formdata_string.str; + try { + message.set_uri(GLib.Uri.parse(url_with_query, GLib.UriFlags.ENCODED)); + } catch (Error err) { + error ("Invalid uri for service: %s", err.message); + } } else { - message.set_request("application/x-www-form-urlencoded", Soup.MemoryUse.COPY, - formdata_string.data); + message.set_request_body_from_bytes("application/x-www-form-urlencoded", StringBuilder.free_to_bytes((owned)formdata_string)); } is_executed = true; + return old_url; + } + + public virtual async void execute_async() throws Spit.Publishing.PublishingError { + // if a custom payload is being used, we don't need to peform the tasks that are necessary + // to prepare a traditional key-value pair REST request; Instead (since we don't + // know anything about the custom payload), we just put it on the wire and return + if (use_custom_payload) { + is_executed = true; + yield send_async(); + + return; + } + + var old_url = prepare_rest_message(); + try { - debug("sending message to URI = '%s'", message.get_uri().to_string(false)); - send(); + debug("sending message to URI = '%s'", message.get_uri().to_string()); + yield send_async(); } finally { // if old_url is non-null, then restore it if (old_url != null) - message.set_uri(new Soup.URI(old_url)); + message.set_uri(old_url); } } public string get_response() { assert(get_is_executed()); - return (string) message.response_body.data; + return parent_session.get_body() == null ? "" : (string) parent_session.get_body().get_data(); } public unowned Soup.MessageHeaders get_response_headers() { @@ -510,7 +547,7 @@ public class UploadTransaction : Transaction { GLib.HashTable<string, string> result = new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal); - result.insert("filename", Soup.URI.encode(publishable.get_serialized_file().get_basename(), + result.insert("filename", GLib.Uri.escape_string(publishable.get_serialized_file().get_basename(), null)); return result; @@ -520,7 +557,7 @@ public class UploadTransaction : Transaction { binary_disposition_table = new_disp_table; } - public override void execute() throws Spit.Publishing.PublishingError { + private void prepare_execution() throws Spit.Publishing.PublishingError { Argument[] request_arguments = get_arguments(); assert(request_arguments.length > 0); @@ -529,40 +566,40 @@ public class UploadTransaction : Transaction { foreach (Argument arg in request_arguments) message_parts.append_form_string(arg.key, arg.value); - string payload; - size_t payload_length; + MappedFile? mapped_file = null; try { - FileUtils.get_contents(publishable.get_serialized_file().get_path(), out payload, - out payload_length); - } catch (FileError e) { + mapped_file = new MappedFile(publishable.get_serialized_file().get_path(), false); + } catch (Error e) { throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( _("A temporary file needed for publishing is unavailable")); } - int payload_part_num = message_parts.get_length(); - - Soup.Buffer bindable_data = new Soup.Buffer(Soup.MemoryUse.COPY, payload.data[0:payload_length]); message_parts.append_form_file("", publishable.get_serialized_file().get_path(), mime_type, - bindable_data); + mapped_file.get_bytes()); unowned Soup.MessageHeaders image_part_header; - unowned Soup.Buffer image_part_body; + unowned Bytes image_part_body; + int payload_part_num = message_parts.get_length() - 1; message_parts.get_part(payload_part_num, out image_part_header, out image_part_body); + debug ("Image part header %p", image_part_header); image_part_header.set_content_disposition("form-data", binary_disposition_table); - Soup.Message outbound_message = - Soup.Form.request_new_from_multipart(get_endpoint_url(), message_parts); - // TODO: there must be a better way to iterate over a map + var outbound_message = new Soup.Message.from_multipart(get_endpoint_url(), message_parts); + Gee.MapIterator<string, string> i = message_headers.map_iterator(); bool cont = i.next(); while(cont) { outbound_message.request_headers.append(i.get_key(), i.get_value()); cont = i.next(); } - set_message(outbound_message); + set_message(outbound_message, mapped_file.get_length()); set_is_executed(true); - send(); + } + + public override async void execute_async() throws Spit.Publishing.PublishingError { + prepare_execution(); + yield send_async(); } } @@ -690,9 +727,8 @@ public abstract class BatchUploader { this.session = session; } - private void send_files() { + private async void send_files_async() throws Spit.Publishing.PublishingError { current_file = 0; - bool stop = false; foreach (Spit.Publishing.Publishable publishable in publishables) { GLib.File? file = publishable.get_serialized_file(); @@ -710,26 +746,15 @@ public abstract class BatchUploader { txn.chunk_transmitted.connect(on_chunk_transmitted); - try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { - upload_error(err); - stop = true; - } + yield txn.execute_async(); txn.chunk_transmitted.disconnect(on_chunk_transmitted); - - if (stop) - break; - + current_file++; } - - if (!stop) - upload_complete(current_file); } - - private void on_chunk_transmitted(int bytes_written_so_far, int total_bytes) { + + private void on_chunk_transmitted(uint bytes_written_so_far, uint total_bytes) { double file_span = 1.0 / publishables.length; double this_file_fraction_complete = ((double) bytes_written_so_far) / total_bytes; double fraction_complete = (current_file * file_span) + (this_file_fraction_complete * @@ -748,12 +773,14 @@ public abstract class BatchUploader { } protected abstract Transaction create_transaction(Spit.Publishing.Publishable publishable); - - public void upload(Spit.Publishing.ProgressCallback? status_updated = null) { + + public async int upload_async(Spit.Publishing.ProgressCallback? status_updated = null) throws Spit.Publishing.PublishingError { this.status_updated = status_updated; if (publishables.length > 0) - send_files(); + yield send_files_async(); + + return current_file; } } diff --git a/plugins/common/WebAuthenticationPane.vala b/plugins/common/WebAuthenticationPane.vala index 02cffb2..b9f7280 100644 --- a/plugins/common/WebAuthenticationPane.vala +++ b/plugins/common/WebAuthenticationPane.vala @@ -19,13 +19,8 @@ namespace Shotwell.Plugins.Common { private Gtk.Entry entry; public void clear() { - try { - debug("Clearing the data of WebKit..."); - this.webview.get_website_data_manager().clear.begin(WebKit.WebsiteDataTypes.ALL, (GLib.TimeSpan)0); - } catch (Error e) { - // Do nothing - message("Failed to clear data: %s", e.message); - } + debug("Clearing the data of WebKit..."); + this.webview.get_website_data_manager().clear.begin(WebKit.WebsiteDataTypes.ALL, (GLib.TimeSpan)0); } public override void constructed () { @@ -44,7 +39,6 @@ namespace Shotwell.Plugins.Common { box.pack_start (entry, false, false, 6); this.webview = new WebKit.WebView (); - this.webview.get_settings ().enable_plugins = false; this.webview.load_changed.connect (this.on_page_load_changed); this.webview.load_failed.connect (this.on_page_load_failed); diff --git a/plugins/meson.build b/plugins/meson.build index 9abc0c8..e9c0e49 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -18,7 +18,4 @@ sw_plugin_common_dep = declare_dependency(include_directories : include_director subdir('authenticator') subdir('shotwell-publishing') -if get_option('extra-plugins') - subdir('shotwell-publishing-extras') -endif subdir('shotwell-transitions') diff --git a/plugins/shotwell-publishing-extras/GalleryConnector.vala b/plugins/shotwell-publishing-extras/GalleryConnector.vala deleted file mode 100644 index 9932862..0000000 --- a/plugins/shotwell-publishing-extras/GalleryConnector.vala +++ /dev/null @@ -1,2035 +0,0 @@ -/* Copyright 2012-2013 Joe Sapp nixphoeni@gentoo.org - * - * This software is licensed under the GNU LGPL (version 2.1 or later). - * See the COPYING file in this distribution. - */ - - -const string G3_VERSION = "0.1"; - -const string G3_LICENSE = """ -The Gallery3Publishing module is free software; you can redistribute it -and/or modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either version 2.1 -of the License, or (at your option) any later version. - -The Gallery3Publishing module is distributed in the hope that it will be -useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser -General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with The Gallery3Publishing module; if not, write to the Free -Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -02110-1301 USA -"""; - -const string WEBSITE_URL = - "https://github.com/sappjw/shotwell-gallery3"; - -// This module's Spit.Module -private class ShotwellPublishingGallery3 : Object, Spit.Module { - private Spit.Pluggable[] pluggables = new Spit.Pluggable[0]; - - public ShotwellPublishingGallery3(GLib.File module_file) { - GLib.File resource_directory = module_file.get_parent(); - - pluggables += new Gallery3Service(resource_directory); - } - - public unowned string get_module_name() { - return _("Gallery3 publishing module"); - } - - public unowned string get_version() { - return G3_VERSION; - } - - public unowned string get_id() { - return "org.yorba.shotwell.sharing.gallery3"; - } - - public unowned Spit.Pluggable[]? get_pluggables() { - return pluggables; - } -} - -// The Pluggable -public class Gallery3Service : Object, Spit.Pluggable, - Spit.Publishing.Service { - private const string ICON_FILENAME = "gallery3.png"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public Gallery3Service(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = Resources.load_from_resource - (Resources.RESOURCE_PATH + "/" + ICON_FILENAME); - } - - public int get_pluggable_interface(int min_host_interface, - int max_host_interface) { - return Spit.negotiate_interfaces(min_host_interface, - max_host_interface, - Spit.Publishing.CURRENT_INTERFACE); - } - - public unowned string get_id() { - return "publishing-gallery3"; - } - - public unowned string get_pluggable_name() { - return "Gallery3"; - } - - public void get_info(ref Spit.PluggableInfo info) { - info.authors = "Joe Sapp"; - info.copyright = "2012-2013 Joe Sapp"; - info.translators = Resources.TRANSLATORS; - info.version = G3_VERSION; - info.website_url = WEBSITE_URL; - info.is_license_wordwrapped = false; - info.license = G3_LICENSE; - info.icons = icon_pixbuf_set; - } - - public void activation(bool enabled) { - } - - public Spit.Publishing.Publisher create_publisher( - Spit.Publishing.PluginHost host) { - return new Publishing.Gallery3.GalleryPublisher(this, host); - } - - public Spit.Publishing.Publisher.MediaType get_supported_media() { - return (Spit.Publishing.Publisher.MediaType.PHOTO | - Spit.Publishing.Publisher.MediaType.VIDEO); - } -} - - -namespace Publishing.Gallery3 { -private const string SERVICE_NAME = "Gallery3"; -private const string SERVICE_WELCOME_MESSAGE = - _("You are not currently logged into your Gallery.\n\nYou must have already signed up for a Gallery3 account to complete the login process."); -private const string DEFAULT_ALBUM_DIR = _("Shotwell"); -private const string DEFAULT_ALBUM_TITLE = - _("Shotwell default directory"); -private const string REST_PATH = "/index.php/rest"; - -private class Album { - - // Properties - public string name { get; private set; default = ""; } - public string title { get; private set; default = ""; } - public string summary { get; private set; default = ""; } - public string parentname { get; private set; default = ""; } - public string url { get; private set; default = ""; } - public string path { get; private set; default = ""; } - public bool editable { get; private set; default = false; } - - // Each element is a collection - public Album(Json.Object collection) { - - unowned Json.Object entity = - collection.get_object_member("entity"); - - title = entity.get_string_member("title"); - name = entity.get_string_member("name"); - parentname = entity.get_string_member("parent"); - url = collection.get_string_member("url"); - editable = entity.get_boolean_member("can_edit"); - - // Get the path from the last two elements of the URL. - // This should always be "/item/#" where "#" is a number. - path = strip_session_url(url); - - } - -} - -private class BaseGalleryTransaction : - Publishing.RESTSupport.Transaction { - - protected Json.Parser parser; - - // BaseGalleryTransaction constructor - public BaseGalleryTransaction(Session session, string endpoint_url, - string item_path = "", - Publishing.RESTSupport.HttpMethod method = - Publishing.RESTSupport.HttpMethod.POST) { - - // TODO: eventually we can remove this - if ((item_path != "") && (item_path[0] != '/')) { - warning("Bad item path, this is a bug!"); - error(item_path); - } - - base.with_endpoint_url(session, - endpoint_url + REST_PATH + item_path, - method); - - this.parser = new Json.Parser(); - - } - - protected unowned Json.Node get_root_node() - throws Spit.Publishing.PublishingError { - - string json_object; - unowned Json.Node root_node; - - json_object = get_response(); - - if ((null == json_object) || (0 == json_object.length)) - throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( - "No response data from %s", get_endpoint_url()); - - try { - this.parser.load_from_data(json_object); - } - catch (GLib.Error e) { - // If this didn't work, reset the "executed" state - warning("ERROR: didn't load JSON data"); - set_is_executed(false); - throw new Spit.Publishing.PublishingError.PROTOCOL_ERROR(e.message); - } - - root_node = this.parser.get_root(); - if (root_node.is_null()) - throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( - "Root node is null, doesn't appear to be JSON data"); - - return root_node; - - } - -} - -private class KeyFetchTransaction : BaseGalleryTransaction { - - private string key = ""; - - // KeyFetchTransaction constructor - // - // url: Base gallery URL - public KeyFetchTransaction(Session session, string url, - string username, string password) { - base(session, url); - add_argument("user", username); - add_argument("password", password); - } - - public string get_key() { - - if (key != "") - return key; - - key = get_response(); - - // The returned data isn't actually a JSON object... - if (null == key || "" == key || 0 == key.length) { - warning("No response data from \"%s\"", get_endpoint_url()); - return ""; - } - - // Eliminate quotes surrounding key - key = key[1:-1]; - - return key; - } - -} - -private class GalleryRequestTransaction : BaseGalleryTransaction { - - // GalleryRequestTransaction constructor - // - // item: Item URL component - public GalleryRequestTransaction(Session session, string item, - Publishing.RESTSupport.HttpMethod method = - Publishing.RESTSupport.HttpMethod.GET) { - - if (!session.is_authenticated()) { - error("Not authenticated"); - } - else { - base(session, session.url, item, method); - add_header("X-Gallery-Request-Key", session.key); - add_header("X-Gallery-Request-Method", "GET"); - } - - } - -} - -private class GetAlbumURLsTransaction : GalleryRequestTransaction { - - public GetAlbumURLsTransaction(Session session) { - - base(session, "/item/1"); - add_argument("type", "album"); - add_argument("scope", "all"); - - } - - public string [] get_album_urls() { - - unowned Json.Node root_node; - unowned Json.Array all_members; - - try { - root_node = get_root_node(); - } - catch (Spit.Publishing.PublishingError e) { - error("Could not get root node"); - } - - all_members = - root_node.get_object().get_array_member("members"); - - string [] member_urls = null; - - for (uint i = 0; i <= all_members.get_length() - 1; i++) - member_urls += all_members.get_string_element(i); - - return member_urls; - - } - -} - -private class GetAlbumsTransaction : GalleryRequestTransaction { - - // Properties - // Original list of album URLs - public string [] album_urls { get; private set; default = null; } - // How many URLs have been sent? - public uint urls_sent { get; private set; default = 0; } - // Are there (possibly) more URLs to send? - public bool more_urls { get; private set; default = false; } - - public GetAlbumsTransaction(Session session, string [] _album_urls, - uint start = 0) { - - base(session, "/items"); - add_argument("scope", "all"); - - // Save original list of URLs - album_urls = _album_urls; - - // Wrap each URL in double quotes and separate by a comma, but - // we should try to keep the length of the URL under 255 - // characters. We need to do this to avoid problems with URLs - // that are too long on some web servers (and, really, if there - // are alot of albums, this can get large quickly). - // The Gallery3 API should probably allow this in a POST - // transaction... - string url_list = "["; - string [] my_album_urls = null; - string? endpoint_url = session.get_endpoint_url(); - int url_length = (null != endpoint_url) ? - endpoint_url.length : 0; - url_length += 18; // for: ?scope=all&urls=[] - - // We have to allow at least one URL at a time - if (start <= album_urls.length - 1) { - - urls_sent = start; - do { - my_album_urls += "\"" + album_urls[urls_sent] + "\""; - // Add 3 for: "", - url_length += album_urls[urls_sent].length + 3; - urls_sent++; - } while ((urls_sent <= album_urls.length - 1) && - (url_length + - album_urls[urls_sent].length + 3 <= 255)); - url_list += string.joinv(",", my_album_urls); - - more_urls = (urls_sent <= (album_urls.length - 1)); - - } - url_list += "]"; - - add_argument("urls", url_list); - - } - - public Album [] get_albums() - throws Spit.Publishing.PublishingError { - - Album [] albums = null; - Album tmp_album; - unowned Json.Node root_node = get_root_node(); - unowned Json.Array members = root_node.get_array(); - - // Only add editable items - for (uint i = 0; i <= members.get_length() - 1; i++) { - tmp_album = new Album(members.get_object_element(i)); - - if (tmp_album.editable) - albums += tmp_album; - else - warning(@"Album \"$(tmp_album.title)\" is not editable"); - } - - return albums; - } - -} - -// Class to create or get a tag URL. -// Tag URLs are placed in the "item_tags" object and relate an item and -// its tags. -private class GalleryGetTagTransaction : BaseGalleryTransaction { - - public GalleryGetTagTransaction(Session session, string tag_name) { - - if (!session.is_authenticated()) { - error("Not authenticated"); - } - else { - Json.Generator entity = new Json.Generator(); - Json.Node root_node = new Json.Node(Json.NodeType.OBJECT); - Json.Object obj = new Json.Object(); - - base(session, session.url, - "/tags", - Publishing.RESTSupport.HttpMethod.POST); - add_header("X-Gallery-Request-Key", session.key); - add_header("X-Gallery-Request-Method", "POST"); - - obj.set_string_member("name", tag_name); - root_node.set_object(obj); - entity.set_root(root_node); - - size_t entity_length; - string entity_value = entity.to_data(out entity_length); - - debug("created entity: %s", entity_value); - - add_argument("entity", entity_value); - } - - } - - public string tag_url() { - - unowned Json.Node root_node; - string url; - - try { - root_node = get_root_node(); - } - catch (Spit.Publishing.PublishingError e) { - error("Could not get root node"); - } - - url = - root_node.get_object().get_string_member("url"); - - return url; - - } - -} - -// Get the item_tags URL for a given item -private class GalleryGetItemTagsURLsTransaction : - GalleryRequestTransaction { - - private string item_tags_path = ""; - - public GalleryGetItemTagsURLsTransaction(Session session, - string item_url) { - - base(session, item_url); - - } - - public string get_item_tags_path() { - - unowned Json.Node root_node; - unowned Json.Object relationships, tags; - - if ("" == item_tags_path) { - - try { - root_node = get_root_node(); - } - catch (Spit.Publishing.PublishingError e) { - error("Could not get root node"); - } - - relationships = - root_node.get_object().get_object_member("relationships"); - tags = relationships.get_object_member("tags"); - - item_tags_path = tags.get_string_member("url"); - - // Remove the session URL from the beginning of this URL - item_tags_path = strip_session_url(item_tags_path); - - } - - return item_tags_path; - - } - -} - -// Set a tag relationship with an item -private class GallerySetTagRelationshipTransaction : - BaseGalleryTransaction { - - public GallerySetTagRelationshipTransaction(Session session, - string item_tags_path, string tag_url, string item_url) { - - if (!session.is_authenticated()) { - error("Not authenticated"); - } - else { - Json.Generator entity = new Json.Generator(); - Json.Node root_node = new Json.Node(Json.NodeType.OBJECT); - Json.Object obj = new Json.Object(); - - base(session, session.url, - item_tags_path, - Publishing.RESTSupport.HttpMethod.POST); - add_header("X-Gallery-Request-Key", session.key); - add_header("X-Gallery-Request-Method", "POST"); - - obj.set_string_member("tag", tag_url); - obj.set_string_member("item", item_url); - root_node.set_object(obj); - entity.set_root(root_node); - - size_t entity_length; - string entity_value = entity.to_data(out entity_length); - - debug("created entity: %s", entity_value); - - add_argument("entity", entity_value); - } - - } - -} - -private class GalleryAlbumCreateTransaction : BaseGalleryTransaction { - - // Properties - public PublishingParameters parameters { get; private set; } - // Private variables - private string? session_url; - - // GalleryAlbumCreateTransaction constructor - // - // parameters: New album parameters - public GalleryAlbumCreateTransaction(Session session, - PublishingParameters parameters) { - - if (!session.is_authenticated()) { - error("Not authenticated"); - } - else { - Json.Generator entity = new Json.Generator(); - Json.Node root_node = new Json.Node(Json.NodeType.OBJECT); - Json.Object obj = new Json.Object(); - - base(session, session.url, "/item/1", - Publishing.RESTSupport.HttpMethod.POST); - add_header("X-Gallery-Request-Key", session.key); - add_header("X-Gallery-Request-Method", "POST"); - - this.session_url = session.url; - this.parameters = parameters; - - obj.set_string_member("name", parameters.album_name); - obj.set_string_member("type", "album"); - obj.set_string_member("title", parameters.album_title); - root_node.set_object(obj); - entity.set_root(root_node); - - string entity_value = entity.to_data(null); - - debug("created entity: %s", entity_value); - - add_argument("entity", entity_value); - } - - } - - public string get_new_album_path() { - - unowned Json.Node root_node; - string new_path; - - try { - root_node = get_root_node(); - } - catch (Spit.Publishing.PublishingError e) { - error("Could not get root node"); - } - - new_path = - root_node.get_object().get_string_member("url"); - new_path = strip_session_url(new_path); - - return new_path; - - } - -} - -private class GalleryUploadTransaction : - Publishing.RESTSupport.UploadTransaction { - - private Session session; - private Json.Generator generator; - private PublishingParameters parameters; - private string item_url; - private string item_path; - private string item_tags_path; - - public GalleryUploadTransaction(Session session, - PublishingParameters parameters, - Spit.Publishing.Publishable publishable) { - - // TODO: eventually we can remove this - if (parameters.album_path[0] != '/') { - warning("Bad upload item path, this is a bug!"); - error(parameters.album_path); - } - - base.with_endpoint_url(session, publishable, - session.url + REST_PATH + parameters.album_path); - - this.parameters = parameters; - this.session = session; - - add_header("X-Gallery-Request-Key", session.key); - add_header("X-Gallery-Request-Method", "POST"); - - GLib.HashTable<string, string> disposition_table = - new GLib.HashTable<string, string>(GLib.str_hash, - GLib.str_equal); - string? title = publishable.get_publishing_name(); - string filename = publishable.get_param_string( - Spit.Publishing.Publishable.PARAM_STRING_BASENAME); - if (title == null || title == "") - //TODO: remove extension? - title = filename; - - disposition_table.insert("filename", @"$(filename)"); - disposition_table.insert("name", "file"); - - set_binary_disposition_table(disposition_table); - - // Do the JSON stuff - generator = new Json.Generator(); - string desc = publishable.get_param_string( - Spit.Publishing.Publishable.PARAM_STRING_COMMENT); - string type = (publishable.get_media_type() == - Spit.Publishing.Publisher.MediaType.VIDEO) ? - "movie" : "photo"; - - Json.Node root_node = new Json.Node(Json.NodeType.OBJECT); - Json.Object obj = new Json.Object(); - obj.set_string_member("name", filename); - obj.set_string_member("type", type); - obj.set_string_member("title", title); - obj.set_string_member("description", desc); - - root_node.set_object(obj); - generator.set_root(root_node); - - add_argument("entity", generator.to_data(null)); - } - - private string get_new_item_url() { - - string json_object; - string new_url; - unowned Json.Node root_node; - Json.Parser parser = new Json.Parser(); - - json_object = get_response(); - - if ((null == json_object) || (0 == json_object.length)) { - warning("No response data from %s", get_endpoint_url()); - return ""; - } - - debug("json_object: %s", json_object); - - try { - parser.load_from_data(json_object); - } - catch (GLib.Error e) { - // If this didn't work, reset the "executed" state - // TODO: can we recover from this? - warning("ERROR: didn't load JSON data"); - set_is_executed(false); - error(e.message); - } - - root_node = parser.get_root(); - if (root_node.is_null()) { - warning("Root node is null, doesn't appear to be JSON data"); - return ""; - } - - new_url = - root_node.get_object().get_string_member("url"); - - return new_url; - - } - - private void do_set_tag_relationship(string tag_url) - throws Spit.Publishing.PublishingError { - GallerySetTagRelationshipTransaction tag_txn = - new GallerySetTagRelationshipTransaction( - (Session) get_parent_session(), item_tags_path, - tag_url, item_url); - - tag_txn.execute(); - - debug("Response from setting tag relationship: %s", - tag_txn.get_response()); - } - - private string get_new_item_tags_path() { - GalleryGetItemTagsURLsTransaction tag_urls_txn = - new GalleryGetItemTagsURLsTransaction( - (Session) get_parent_session(), item_path); - - try { - tag_urls_txn.execute(); - } catch (Spit.Publishing.PublishingError err) { - debug("Problem getting the item_tags URL: %s", - err.message); - return ""; - } - - return tag_urls_txn.get_item_tags_path(); - } - - private string get_tag_url(string tag) { - - GalleryGetTagTransaction tag_txn = - new GalleryGetTagTransaction( - (Session) get_parent_session(), tag); - - try { - tag_txn.execute(); - } catch (Spit.Publishing.PublishingError err) { - debug("Problem getting the tags URL: %s", - err.message); - return ""; - } - - return tag_txn.tag_url(); - - } - - private void on_upload_completed() - throws Spit.Publishing.PublishingError { - - debug("EVENT: upload completed"); - - if (!parameters.strip_metadata) { - - string[] keywords; - - debug("EVENT: evaluating tags"); - - keywords = base.publishable.get_publishing_keywords(); - - // If this publishable has no tags, continue - if (null == keywords) { - debug("No tags"); - return; - } - - // Get URLs from the file we just finished uploading - item_url = get_new_item_url(); - item_path = strip_session_url(item_url); - item_tags_path = get_new_item_tags_path(); - debug("new item path is %s", item_path); - debug("item_tags path is %s", item_tags_path); - - // Verify these aren't empty - if (("" == item_path) || ("" == item_tags_path)) { - throw new - Spit.Publishing.PublishingError.COMMUNICATION_FAILED( - "Could not obtain URL of uploaded item or its " + - "\"item_tags\" relationship URL"); - } - - // Do the tagging here - foreach (string tag in keywords) { - debug(@"Found tag: $(tag)"); - string new_tag_url = get_tag_url(tag); - - try { - do_set_tag_relationship(new_tag_url); - } catch (Spit.Publishing.PublishingError err) { - debug("Problem setting the relationship between tag " + - "and item: %s", err.message); - throw err; - } - } - - } - - } - - public override void execute() - throws Spit.Publishing.PublishingError { - base.execute(); - - // Run tagging operations here - on_upload_completed(); - } - -} - - -public class GalleryPublisher : Spit.Publishing.Publisher, GLib.Object { - private const string BAD_FILE_MSG = _("\n\nThe file “%s” may not be supported by or may be too large for this instance of Gallery3."); - private const string BAD_MOVIE_MSG = _("\nNote that Gallery3 only supports the video types that Flowplayer does."); - - private weak Spit.Publishing.PluginHost host = null; - private Spit.Publishing.ProgressCallback progress_reporter = null; - private weak Spit.Publishing.Service service = null; - private Session session = null; - private bool running = false; - private Album[] albums = null; - private string key = null; - - private PublishingOptionsPane publishing_options_pane = null; - - public GalleryPublisher(Spit.Publishing.Service service, - Spit.Publishing.PluginHost host) { - this.service = service; - this.host = host; - this.session = new Session(); - } - - public bool is_running() { - return running; - } - - public Spit.Publishing.Service get_service() { - return service; - } - - public void start() { - if (is_running()) - return; - - if (host == null) - error("GalleryPublisher: start( ): can't start; this " + - "publisher is not restartable."); - - debug("GalleryPublisher: starting interaction."); - - running = true; - - key = get_api_key(); - - if ((null == key) || ("" == key)) - do_show_service_welcome_pane(); - else { - string url = get_gallery_url(); - string username = get_gallery_username(); - - if ((null == username) || (null == key) || (null == url)) - do_show_service_welcome_pane(); - else { - debug("ACTION: attempting network login for user " + - "'%s' at URL '%s' from saved credentials.", - username, url); - - host.install_account_fetch_wait_pane(); - - session.authenticate(url, username, key); - - // Initiate an album transaction - do_fetch_album_urls(); - } - } - } - - public void stop() { - debug("GalleryPublisher: stop( ) invoked."); - - running = false; - } - - // Config getters/setters - // API key - internal string? get_api_key() { - return host.get_config_string("api-key", null); - } - - internal void set_api_key(string key) { - host.set_config_string("api-key", key); - } - - // URL - internal string? get_gallery_url() { - return host.get_config_string("url", null); - } - - internal void set_gallery_url(string url) { - host.set_config_string("url", url); - } - - // Username - internal string? get_gallery_username() { - return host.get_config_string("username", null); - } - - internal void set_gallery_username(string username) { - host.set_config_string("username", username); - } - - internal bool? get_persistent_strip_metadata() { - return host.get_config_bool("strip-metadata", false); - } - - internal void set_persistent_strip_metadata(bool strip_metadata) { - host.set_config_bool("strip-metadata", strip_metadata); - } - - internal int? get_scaling_constraint_id() { - return host.get_config_int("scaling-constraint-id", 0); - } - - internal void set_scaling_constraint_id(int constraint) { - host.set_config_int("scaling-constraint-id", constraint); - } - - internal int? get_scaling_pixels() { - return host.get_config_int("scaling-pixels", 1024); - } - - internal void set_scaling_pixels(int pixels) { - host.set_config_int("scaling-pixels", pixels); - } - - // Pane installation functions - private void do_show_service_welcome_pane() { - debug("ACTION: showing service welcome pane."); - - host.install_welcome_pane(SERVICE_WELCOME_MESSAGE, - on_service_welcome_login); - } - - private void do_show_credentials_pane(CredentialsPane.Mode mode) { - debug("ACTION: showing credentials capture pane in %s mode.", - mode.to_string()); - - session.deauthenticate(); - - CredentialsPane creds_pane = - new CredentialsPane(host, mode, get_gallery_url(), - get_gallery_username(), get_api_key()); - creds_pane.go_back.connect(on_credentials_go_back); - creds_pane.login.connect(on_credentials_login); - - host.install_dialog_pane(creds_pane); - } - - private void do_network_login(string url, string username, - string password) { - debug("ACTION: attempting network login for user '%s' at URL " + - "'%s'.", username, url); - - host.install_login_wait_pane(); - - KeyFetchTransaction fetch_trans = - new KeyFetchTransaction(session, url, username, password); - fetch_trans.network_error.connect(on_key_fetch_error); - fetch_trans.completed.connect(on_key_fetch_complete); - - try { - fetch_trans.execute(); - } catch (Spit.Publishing.PublishingError err) { - debug("Caught an error attempting to login"); - // 403 errors may be recoverable, so don't post the error to - // our host immediately; instead, try to recover from it - on_key_fetch_error(fetch_trans, err); - } - } - - private void do_fetch_album_urls() { - - host.install_account_fetch_wait_pane(); - - GetAlbumURLsTransaction album_trans = - new GetAlbumURLsTransaction(session); - album_trans.network_error.connect(on_album_urls_fetch_error); - album_trans.completed.connect(on_album_urls_fetch_complete); - - try { - album_trans.execute(); - } catch (Spit.Publishing.PublishingError err) { - debug("Caught an error attempting to fetch albums"); - // 403 errors may be recoverable, so don't post the error to - // our host immediately; instead, try to recover from it - on_album_urls_fetch_error(album_trans, err); - } - - } - - private void do_fetch_albums(string [] album_urls, uint start = 0) { - - GetAlbumsTransaction album_trans = - new GetAlbumsTransaction(session, album_urls, start); - album_trans.network_error.connect(on_album_fetch_error); - album_trans.completed.connect(on_album_fetch_complete); - - try { - album_trans.execute(); - } catch (Spit.Publishing.PublishingError err) { - // 403 errors may be recoverable, so don't post the error to - // our host immediately; instead, try to recover from it - on_album_fetch_error(album_trans, err); - } - - } - - private void do_show_publishing_options_pane(string url, - string username) { - - debug("ACTION: showing publishing options pane"); - - Gtk.Builder builder = new Gtk.Builder(); - - try { - builder.add_from_resource(Resources.RESOURCE_PATH + - "/gallery3_publishing_options_pane.ui"); - } - catch (Error e) { - warning("Could not parse UI file! Error: %s.", e.message); - host.post_error( - new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( - _("A file required for publishing is unavailable. Publishing to %s can’t continue.") - .printf(SERVICE_NAME) - ) - ); - return; - } - - publishing_options_pane = - new PublishingOptionsPane(host, url, username, albums, - builder, get_persistent_strip_metadata(), - get_scaling_constraint_id(), get_scaling_pixels()); - publishing_options_pane.publish.connect( - on_publishing_options_pane_publish); - publishing_options_pane.logout.connect( - on_publishing_options_pane_logout); - host.install_dialog_pane(publishing_options_pane); - - } - - private void do_create_album(PublishingParameters parameters) { - - debug("ACTION: creating album"); - - GalleryAlbumCreateTransaction album_trans = - new GalleryAlbumCreateTransaction(session, parameters); - album_trans.network_error.connect(on_album_create_error); - album_trans.completed.connect(on_album_create_complete); - - try { - album_trans.execute(); - } catch (Spit.Publishing.PublishingError err) { - // 403 errors may be recoverable, so don't post the error to - // our host immediately; instead, try to recover from it - on_album_create_error(album_trans, err); - } - - } - - private void do_publish(PublishingParameters parameters) { - - debug("ACTION: publishing items"); - - set_persistent_strip_metadata(parameters.strip_metadata); - set_scaling_constraint_id( - (parameters.photo_major_axis_size <= 0) ? 0 : 1); - set_scaling_pixels(parameters.photo_major_axis_size); - host.set_service_locked(true); - progress_reporter = - host.serialize_publishables(parameters.photo_major_axis_size, - parameters.strip_metadata); - - // Serialization is a long and potentially cancellable - // operation, so before we use the publishables, make sure that - // the publishing interaction is still running. If it isn't, the - // publishing environment may be partially torn down so do a - // short-circuit return. - if (!is_running()) - return; - - Uploader uploader = - new Uploader(session, host.get_publishables(), - parameters); - uploader.upload_complete.connect(on_publish_complete); - uploader.upload_error.connect(on_publish_error); - uploader.upload(on_upload_status_updated); - - } - - private void do_show_success_pane() { - debug("ACTION: showing success pane."); - - host.set_service_locked(false); - host.install_success_pane(); - } - - // Callbacks - private void on_service_welcome_login() { - if (!is_running()) - return; - - debug("EVENT: user clicked 'Login' in welcome pane."); - - do_show_credentials_pane(CredentialsPane.Mode.INTRO); - } - - private void on_credentials_login(string url, string username, - string password) { - if (!is_running()) - return; - - debug("EVENT: user '%s' clicked 'Login' in credentials pane.", - username); - - set_gallery_url(url); - set_gallery_username(username); - do_network_login(url, username, password); - } - - private void on_credentials_go_back() { - if (!is_running()) - return; - - debug("EVENT: user is attempting to go back."); - - do_show_service_welcome_pane(); - } - - private void on_key_fetch_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err) { - bad_txn.completed.disconnect(on_key_fetch_complete); - bad_txn.network_error.disconnect(on_key_fetch_error); - - if (!is_running()) - return; - - // ignore these events if the session is already auth'd - if (session.is_authenticated()) - return; - - debug("EVENT: network transaction to fetch key for login " + - "failed; response = '%s'.", - bad_txn.get_response()); - - // HTTP error 403 is invalid authentication -- if we get this - // error during key fetch then we can just show the login screen - // again with a retry message; if we get any error other than - // 403 though, we can't recover from it, so just post the error - // to the user - if (bad_txn.get_status_code() == 403) { - // TODO: can we give more detail on the problem? - do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY); - } - else if (bad_txn.get_status_code() == 400) { - // This might not be a Gallery URL - // TODO: can we give more detail on the problem? - do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL); - } - else { - host.post_error(err); - } - } - - private void on_key_fetch_complete( - Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_key_fetch_complete); - txn.network_error.disconnect(on_key_fetch_error); - - if (!is_running()) - return; - - // ignore these events if the session is already auth'd - if (session.is_authenticated()) - return; - - key = (txn as KeyFetchTransaction).get_key(); - - if (key == null) error("key doesn\'t exist"); - else { - string url = get_gallery_url(); - string username = get_gallery_username(); - - debug("EVENT: network transaction to fetch key completed " + - "successfully."); - - set_api_key(key); - session.authenticate(url, username, key); - - // Initiate an album transaction - do_fetch_album_urls(); - } - } - - private void on_album_urls_fetch_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err) { - bad_txn.completed.disconnect(on_album_urls_fetch_complete); - bad_txn.network_error.disconnect(on_album_urls_fetch_error); - - if (!is_running()) - return; - - // ignore these events if the session is not auth'd - if (!session.is_authenticated()) - return; - - debug("EVENT: network transaction to fetch album URLs " + - "failed; response = \'%s\'.", - bad_txn.get_response()); - - // HTTP error 403 is invalid authentication -- if we get this - // error during key fetch then we can just show the login screen - // again with a retry message; if we get any error other than - // 403 though, we can't recover from it, so just post the error - // to the user - if (bad_txn.get_status_code() == 403) { - // TODO: can we give more detail on the problem? - do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY); - } - else if (bad_txn.get_status_code() == 400) { - // This might not be a Gallery URL - // TODO: can we give more detail on the problem? - do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL); - } - else { - host.post_error(err); - } - } - - private void on_album_urls_fetch_complete( - Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_album_urls_fetch_complete); - txn.network_error.disconnect(on_album_urls_fetch_error); - - if (!is_running()) - return; - - // ignore these events if the session is not auth'd - if (!session.is_authenticated()) - return; - - debug("EVENT: retrieving all album URLs."); - - string [] album_urls = - (txn as GetAlbumURLsTransaction).get_album_urls(); - - if (null == album_urls) { - - string url = session.url; - string username = session.username; - - do_show_publishing_options_pane(url, username); - - } - else - do_fetch_albums(album_urls); - } - - private void on_album_fetch_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err) { - bad_txn.completed.disconnect(on_album_fetch_complete); - bad_txn.network_error.disconnect(on_album_fetch_error); - - if (!is_running()) - return; - - // ignore these events if the session is not auth'd - if (!session.is_authenticated()) - return; - - debug("EVENT: network transaction to fetch albums " + - "failed; response = \'%s\'.", - bad_txn.get_response()); - - // HTTP error 403 is invalid authentication -- if we get this - // error during key fetch then we can just show the login screen - // again with a retry message; if we get any error other than - // 403 though, we can't recover from it, so just post the error - // to the user - if (bad_txn.get_status_code() == 403) { - // TODO: can we give more detail on the problem? - do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY); - } - else if (bad_txn.get_status_code() == 400) { - // This might not be a Gallery URL - // TODO: can we give more detail on the problem? - do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL); - } - else { - host.post_error(err); - } - } - - private void on_album_fetch_complete( - Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_album_fetch_complete); - txn.network_error.disconnect(on_album_fetch_error); - - Album[] new_albums = null; - - if (!is_running()) - return; - - // ignore these events if the session is not auth'd - if (!session.is_authenticated()) - return; - - debug("EVENT: user is attempting to populate the album list."); - - try { - new_albums = - (txn as GetAlbumsTransaction).get_albums(); - } catch (Spit.Publishing.PublishingError err) { - on_album_fetch_error(txn, err); - } - - // Append new albums to existing - for (int i = 0; i <= new_albums.length - 1; i++) - albums += new_albums[i]; - - if ((txn as GetAlbumsTransaction).more_urls) { - - do_fetch_albums((txn as GetAlbumsTransaction).album_urls, - (txn as GetAlbumsTransaction).urls_sent); - - } - else { - - string url = session.url; - string username = session.username; - - do_show_publishing_options_pane(url, username); - - } - } - - private void on_album_create_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err) { - bad_txn.completed.disconnect(on_album_create_complete); - bad_txn.network_error.disconnect(on_album_create_error); - - if (!is_running()) - return; - - // ignore these events if the session is not auth'd - if (!session.is_authenticated()) - return; - - debug("EVENT: network transaction to create an album " + - "failed; response = \'%s\'.", - bad_txn.get_response()); - - // HTTP error 403 is invalid authentication -- if we get this - // error during key fetch then we can just show the login screen - // again with a retry message; if we get any error other than - // 403 though, we can't recover from it, so just post the error - // to the user - if (bad_txn.get_status_code() == 403) { - // TODO: can we give more detail on the problem? - do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY); - } - else if (bad_txn.get_status_code() == 400) { - // This might not be a Gallery URL - // TODO: can we give more detail on the problem? - do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL); - } - else { - host.post_error(err); - } - } - - private void on_album_create_complete( - Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_album_create_complete); - txn.network_error.disconnect(on_album_create_error); - - if (!is_running()) - return; - - // ignore these events if the session is not auth'd - if (!session.is_authenticated()) - return; - - PublishingParameters new_params = - (txn as GalleryAlbumCreateTransaction).parameters; - new_params.album_path = - (txn as GalleryAlbumCreateTransaction).get_new_album_path(); - - debug("EVENT: user has created an album at \"%s\".", - new_params.album_path); - - do_publish(new_params); - } - - private void on_publish_error( - Publishing.RESTSupport.BatchUploader _uploader, - Spit.Publishing.PublishingError err) { - if (!is_running()) - return; - - Uploader uploader = _uploader as Uploader; - GLib.Error g3_err = err.copy(); - - debug("EVENT: uploader reports upload error = '%s' " + - "for file '%s' (code %d)", err.message, - uploader.current_publishable_name, uploader.status_code); - - uploader.upload_complete.disconnect(on_publish_complete); - uploader.upload_error.disconnect(on_publish_error); - - // Is this a 400 error? Then it may be a bad file. - if (uploader.status_code == 400) { - g3_err.message += - BAD_FILE_MSG.printf(uploader.current_publishable_name); - // Add an additional message if this appears to be a video - // file. - if (uploader.current_publishable_type == - Spit.Publishing.Publisher.MediaType.VIDEO) - g3_err.message += BAD_MOVIE_MSG; - } - host.post_error(g3_err); - } - - private void on_upload_status_updated(int file_number, - double completed_fraction) { - - if (!is_running()) - return; - - debug("EVENT: uploader reports upload %.2f percent complete.", - 100.0 * completed_fraction); - - assert(progress_reporter != null); - - progress_reporter(file_number, completed_fraction); - - } - - private void on_publish_complete( - Publishing.RESTSupport.BatchUploader uploader, - int num_published) { - uploader.upload_complete.disconnect(on_publish_complete); - uploader.upload_error.disconnect(on_publish_error); - - if (!is_running()) - return; - - // ignore these events if the session is not auth'd - if (!session.is_authenticated()) - return; - - debug("EVENT: publishing complete; %d items published", - num_published); - - do_show_success_pane(); - - } - - private void on_publishing_options_pane_logout() { - publishing_options_pane.publish.disconnect( - on_publishing_options_pane_publish); - publishing_options_pane.logout.disconnect( - on_publishing_options_pane_logout); - - if (!is_running()) - return; - - debug("EVENT: user is attempting to log out."); - - session.deauthenticate(); - do_show_service_welcome_pane(); - } - - private void on_publishing_options_pane_publish(PublishingParameters parameters) { - publishing_options_pane.publish.disconnect( - on_publishing_options_pane_publish); - publishing_options_pane.logout.disconnect( - on_publishing_options_pane_logout); - - if (!is_running()) - return; - - debug("EVENT: user is attempting to publish something."); - - if (parameters.is_to_new_album()) { - debug("EVENT: must create new album \"%s\" first.", - parameters.album_name); - do_create_album(parameters); - } - else { - do_publish(parameters); - } - } - -} - -internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { - private const string DEFAULT_ALBUM_NAME = ""; - private const string LAST_ALBUM_CONFIG_KEY = "last-album"; - - private Gtk.Builder builder = null; - - private Gtk.Grid pane_widget = null; - private Gtk.Label title_label = null; - private Gtk.RadioButton use_existing_radio = null; - private Gtk.ComboBoxText existing_albums_combo = null; - private Gtk.RadioButton create_new_radio = null; - private Gtk.Entry new_album_entry = null; - private Gtk.ComboBoxText scaling_combo = null; - private Gtk.Entry pixels = null; - private Gtk.CheckButton strip_metadata_check = null; - private Gtk.Button publish_button = null; - private Gtk.Button logout_button = null; - - private Album[] albums; - private weak Spit.Publishing.PluginHost host; - - public signal void publish(PublishingParameters parameters); - public signal void logout(); - - public PublishingOptionsPane(Spit.Publishing.PluginHost host, - string url, string username, Album[] albums, - Gtk.Builder builder, bool strip_metadata, - int scaling_id, int scaling_pixels) { - this.albums = albums; - this.host = host; - - this.builder = builder; - assert(null != builder); - assert(builder.get_objects().length() > 0); - - // pull in all widgets from builder - pane_widget = builder.get_object("pane_widget") as Gtk.Grid; - title_label = builder.get_object("title_label") as Gtk.Label; - use_existing_radio = builder.get_object("publish_to_existing_radio") as Gtk.RadioButton; - existing_albums_combo = builder.get_object("existing_albums_combo") as Gtk.ComboBoxText; - scaling_combo = builder.get_object("scaling_constraint_combo") as Gtk.ComboBoxText; - pixels = builder.get_object("major_axis_pixels") as Gtk.Entry; - create_new_radio = builder.get_object("publish_new_radio") as Gtk.RadioButton; - new_album_entry = builder.get_object("new_album_name") as Gtk.Entry; - strip_metadata_check = this.builder.get_object("strip_metadata_check") as Gtk.CheckButton; - publish_button = builder.get_object("publish_button") as Gtk.Button; - logout_button = builder.get_object("logout_button") as Gtk.Button; - - // populate any widgets whose contents are - // programmatically-generated - title_label.set_label( - _("Publishing to %s as %s.").printf(url, username)); - strip_metadata_check.set_active(strip_metadata); - scaling_combo.set_active(scaling_id); - pixels.set_text(@"$(scaling_pixels)"); - - // connect all signals - use_existing_radio.clicked.connect(on_use_existing_radio_clicked); - create_new_radio.clicked.connect(on_create_new_radio_clicked); - new_album_entry.changed.connect(on_new_album_entry_changed); - scaling_combo.changed.connect(on_scaling_constraint_changed); - pixels.changed.connect(on_pixels_changed); - logout_button.clicked.connect(on_logout_clicked); - publish_button.clicked.connect(on_publish_clicked); - } - - private void on_publish_clicked() { - string album_name; - int photo_major_axis_size = - (scaling_combo.get_active() == 1) ? - int.parse(pixels.get_text()) : -1; - PublishingParameters param; - - if (create_new_radio.get_active()) { - album_name = new_album_entry.get_text(); - host.set_config_string(LAST_ALBUM_CONFIG_KEY, album_name); - param = - new PublishingParameters.to_new_album(album_name); - debug("Trying to publish to \"%s\"", album_name); - } else { - album_name = - albums[existing_albums_combo.get_active()].title; - host.set_config_string(LAST_ALBUM_CONFIG_KEY, album_name); - string album_path = - albums[existing_albums_combo.get_active()].path; - param = - new PublishingParameters.to_existing_album(album_path); - } - - param.photo_major_axis_size = photo_major_axis_size; - param.strip_metadata = strip_metadata_check.get_active(); - - publish(param); - } - - private void on_use_existing_radio_clicked() { - existing_albums_combo.set_sensitive(true); - new_album_entry.set_sensitive(false); - existing_albums_combo.grab_focus(); - update_publish_button_sensitivity(); - } - - private void on_create_new_radio_clicked() { - new_album_entry.set_sensitive(true); - existing_albums_combo.set_sensitive(false); - new_album_entry.grab_focus(); - update_publish_button_sensitivity(); - } - - private void on_logout_clicked() { - logout(); - } - - private void update_publish_button_sensitivity() { - string album_name = new_album_entry.get_text(); - publish_button.set_sensitive(!(album_name.strip() == "" && - create_new_radio.get_active())); - } - - private void on_new_album_entry_changed() { - update_publish_button_sensitivity(); - } - - private void update_pixel_entry_sensitivity() { - pixels.set_sensitive(scaling_combo.get_active() == 1); - } - - private void on_scaling_constraint_changed() { - update_pixel_entry_sensitivity(); - } - - private void on_pixels_changed() { - string orig_text = pixels.get_text(); - char last_char = orig_text[orig_text.length - 1]; - - if (orig_text.length > 0) { - if (!last_char.isdigit()) - pixels.set_text(orig_text.substring(0, - orig_text.length - 1)); - } - } - - public void installed() { - int default_album_id = -1; - string last_album = - host.get_config_string(LAST_ALBUM_CONFIG_KEY, ""); - for (int i = 0; i <= albums.length - 1; i++) { - existing_albums_combo.append_text(albums[i].title); - if ((albums[i].title == last_album) || - ((DEFAULT_ALBUM_NAME == albums[i].title) && - (-1 == default_album_id))) - default_album_id = i; - } - - if (albums.length == 0) { - existing_albums_combo.set_sensitive(false); - use_existing_radio.set_sensitive(false); - create_new_radio.set_active(true); - new_album_entry.grab_focus(); - new_album_entry.set_text(DEFAULT_ALBUM_NAME); - } else { - if (default_album_id >= 0) { - use_existing_radio.set_active(true); - existing_albums_combo.set_active(default_album_id); - new_album_entry.set_sensitive(false); - } else { - create_new_radio.set_active(true); - existing_albums_combo.set_active(0); - new_album_entry.set_text(DEFAULT_ALBUM_NAME); - new_album_entry.grab_focus(); - } - } - update_publish_button_sensitivity(); - update_pixel_entry_sensitivity(); - } - - public Gtk.Widget get_widget() { - return pane_widget; - } - - public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { - return Spit.Publishing.DialogPane.GeometryOptions.NONE; - } - - public void on_pane_installed() { - installed(); - } - - public void on_pane_uninstalled() { - } -} - -internal class PublishingParameters { - - // Private variables for properties - private string _album_title = ""; - - // Properties - public string album_title { - get { - assert(is_to_new_album()); - return _album_title; - } - private set { _album_title = value; } - } - public string album_name { get; private set; default = ""; } - public string album_path { get; set; default = ""; } - public string entity_title { get; private set; default = ""; } - public int photo_major_axis_size { get; set; default = -1; } - public bool strip_metadata { get; set; default = false; } - - private PublishingParameters() { - } - - public PublishingParameters.to_new_album(string album_title) { - this.album_name = album_title.delimit(" ", '-'); - //this.album_name = this.album_name.delimit("\"\'", ''); - this.album_title = album_title; - } - - public PublishingParameters.to_existing_album(string album_path) { - this.album_path = album_path; - } - - public bool is_to_new_album() { - return (album_name != ""); - } -} - -internal class CredentialsPane : Spit.Publishing.DialogPane, GLib.Object { - public enum Mode { - INTRO, - FAILED_RETRY, - NOT_GALLERY_URL; - - public string to_string() { - switch (this) { - case Mode.INTRO: - return "INTRO"; - - case Mode.FAILED_RETRY: - return "FAILED_RETRY"; - - case Mode.NOT_GALLERY_URL: - return "NOT_GALLERY_URL"; - - default: - error("unrecognized CredentialsPane.Mode enumeration value"); - } - } - } - - private CredentialsGrid frame = null; - private Gtk.Widget grid_widget = null; - - public signal void go_back(); - public signal void login(string url, string uname, string password, - string key); - - public CredentialsPane(Spit.Publishing.PluginHost host, - Mode mode = Mode.INTRO, - string? url = null, string? username = null, - string? key = null) { - - Gtk.Builder builder = new Gtk.Builder(); - - try { - builder.add_from_resource (Resources.RESOURCE_PATH + - "/gallery3_authentication_pane.ui"); - } - catch (Error e) { - warning("Could not parse UI file! Error: %s.", e.message); - host.post_error( - new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( - _("A file required for publishing is unavailable. Publishing to %s can’t continue.") - .printf(SERVICE_NAME) - ) - ); - - return; - } - - frame = new CredentialsGrid(host, mode, url, username, key, builder); - grid_widget = frame.pane_widget as Gtk.Widget; - } - - protected void notify_go_back() { - go_back(); - } - - protected void notify_login(string url, string uname, - string password, string key) { - login(url, uname, password, key); - } - - public Gtk.Widget get_widget() { - assert(null != grid_widget); - return grid_widget; - } - - public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { - return Spit.Publishing.DialogPane.GeometryOptions.NONE; - } - - public void on_pane_installed() { - frame.go_back.connect(notify_go_back); - frame.login.connect(notify_login); - - frame.installed(); - } - - public void on_pane_uninstalled() { - frame.go_back.disconnect(notify_go_back); - frame.login.disconnect(notify_login); - } -} - -internal class CredentialsGrid : GLib.Object { - private const string INTRO_MESSAGE = _("Enter the URL for your Gallery3 site and the username and password (or API key) for your Gallery3 account."); - private const string FAILED_RETRY_MESSAGE = _("The username and password or API key were incorrect. To try again, re-enter your username and password below."); - private const string NOT_GALLERY_URL_MESSAGE = _("The URL entered does not appear to be the main directory of a Gallery3 instance. Please make sure you typed it correctly and it does not have any trailing components (e.g., index.php)."); - - public Gtk.Grid pane_widget { get; private set; default = null; } - - private weak Spit.Publishing.PluginHost host = null; - private Gtk.Builder builder = null; - private Gtk.Label intro_message_label = null; - private Gtk.Entry url_entry = null; - private Gtk.Entry username_entry = null; - private Gtk.Entry password_entry = null; - private Gtk.Entry key_entry = null; - private Gtk.Button login_button = null; - private Gtk.Button go_back_button = null; - private string? url = null; - private string? username = null; - private string? key = null; - - public signal void go_back(); - public signal void login(string url, string username, - string password, string key); - - public CredentialsGrid(Spit.Publishing.PluginHost host, - CredentialsPane.Mode mode = CredentialsPane.Mode.INTRO, - string? url = null, string? username = null, - string? key = null, - Gtk.Builder builder) { - this.host = host; - this.url = url; - this.key = key; - this.username = username; - - this.builder = builder; - assert(builder != null); - assert(builder.get_objects().length() > 0); - - // pull in all widgets from builder - pane_widget = builder.get_object("gallery3_auth_pane_widget") as Gtk.Grid; - intro_message_label = builder.get_object("intro_message_label") as Gtk.Label; - url_entry = builder.get_object("url_entry") as Gtk.Entry; - username_entry = builder.get_object("username_entry") as Gtk.Entry; - key_entry = builder.get_object("key_entry") as Gtk.Entry; - password_entry = builder.get_object("password_entry") as Gtk.Entry; - go_back_button = builder.get_object("go_back_button") as Gtk.Button; - login_button = builder.get_object("login_button") as Gtk.Button; - - // Intro message - switch (mode) { - case CredentialsPane.Mode.INTRO: - intro_message_label.set_markup(INTRO_MESSAGE); - break; - - case CredentialsPane.Mode.FAILED_RETRY: - intro_message_label.set_markup("<b>%s</b>\n\n%s".printf(_( - "Unrecognized User"), FAILED_RETRY_MESSAGE)); - break; - - case CredentialsPane.Mode.NOT_GALLERY_URL: - intro_message_label.set_markup("<b>%s</b>\n\n%s".printf( - _(SERVICE_NAME + " Site Not Found"), - NOT_GALLERY_URL_MESSAGE)); - break; - - default: - error("Invalid CredentialsPane mode"); - } - - // Gallery URL - if (url != null) { - url_entry.set_text(url); - username_entry.grab_focus(); - } - url_entry.changed.connect(on_url_or_username_changed); - // User name - if (username != null) { - username_entry.set_text(username); - password_entry.grab_focus(); - } - username_entry.changed.connect(on_url_or_username_changed); - - // Key - if (key != null) { - key_entry.set_text(key); - key_entry.grab_focus(); - } - key_entry.changed.connect(on_url_or_username_changed); - - // Buttons - go_back_button.clicked.connect(on_go_back_button_clicked); - login_button.clicked.connect(on_login_button_clicked); - login_button.set_sensitive((url != null) && (username != null)); - } - - private void on_login_button_clicked() { - login(url_entry.get_text(), username_entry.get_text(), - password_entry.get_text(), key_entry.get_text()); - } - - private void on_go_back_button_clicked() { - go_back(); - } - - private void on_url_or_username_changed() { - login_button.set_sensitive( - ((url_entry.get_text() != "") && - (username_entry.get_text() != "")) || - (key_entry.get_text() != "")); - } - - public void installed() { - host.set_service_locked(false); - - // TODO: following line necessary? - host.set_dialog_default_widget(login_button); - } -} - -internal class Session : Publishing.RESTSupport.Session { - - // Properties - public string? url { get; private set; default = null; } - public string? username { get; private set; default = null; } - public string? key { get; private set; default = null; } - - public Session() { - } - - public override bool is_authenticated() { - return (null != key); - } - - public void authenticate(string gallery_url, string username, string key) { - this.url = gallery_url; - this.username = username; - this.key = key; - - notify_authenticated(); - } - - public void deauthenticate() { - url = null; - username = null; - key = null; - } - -} - -internal class Uploader : Publishing.RESTSupport.BatchUploader { - - private PublishingParameters parameters; - private string _current_publishable_name; - private Spit.Publishing.Publisher.MediaType _current_media_type; - private Publishing.RESTSupport.Transaction? _current_transaction; - - /* Properties */ - public string current_publishable_name { - get { - return _current_publishable_name; - } - } - public uint status_code { - get { - return _current_transaction.get_status_code(); - } - } - public Spit.Publishing.Publisher.MediaType - current_publishable_type { - get { - return _current_media_type; - } - } - - public Uploader(Session session, - Spit.Publishing.Publishable[] publishables, - PublishingParameters parameters) { - - base(session, publishables); - - this.parameters = parameters; - - } - - protected override Publishing.RESTSupport.Transaction - create_transaction(Spit.Publishing.Publishable publishable) { - - Spit.Publishing.Publishable p = get_current_publishable(); - _current_publishable_name = - p.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); - _current_media_type = p.get_media_type(); - - _current_transaction = - new GalleryUploadTransaction((Session) get_session(), - parameters, p); - return _current_transaction; - - } - -} - -private string strip_session_url(string url) { - - // Remove the session URL from the beginning of this URL - debug("Searching for \"%s\" in \"%s\"", - REST_PATH, url); - int item_loc = - url.last_index_of(REST_PATH); - - if (-1 == item_loc) - error("Did not find \"%s\" in the base of the new item " + - "URL \"%s\"", REST_PATH, url); - - return url.substring(item_loc + REST_PATH.length); - -} - -} - -// vi:ts=4:sw=4:et diff --git a/plugins/shotwell-publishing-extras/RajcePublishing.vala b/plugins/shotwell-publishing-extras/RajcePublishing.vala deleted file mode 100644 index a088274..0000000 --- a/plugins/shotwell-publishing-extras/RajcePublishing.vala +++ /dev/null @@ -1,1554 +0,0 @@ -/* Copyright 2014 rajce.net - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -public class RajceService : Object, Spit.Pluggable, Spit.Publishing.Service -{ - private const string ICON_FILENAME = "rajce.png"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public RajceService(GLib.File resource_directory) - { - if (icon_pixbuf_set == null) - icon_pixbuf_set = - Resources.load_from_resource(Resources.RESOURCE_PATH + "/" + - ICON_FILENAME); - } - - public int get_pluggable_interface(int min_host_interface, int max_host_interface) - { - return Spit.negotiate_interfaces(min_host_interface, max_host_interface, - Spit.Publishing.CURRENT_INTERFACE); - } - - public unowned string get_id() - { - return "org.yorba.shotwell.publishing.rajce"; - } - - public unowned string get_pluggable_name() - { - return "Rajce"; - } - - public void get_info(ref Spit.PluggableInfo info) - { - info.authors = "rajce.net developers"; - info.copyright = _("Copyright © 2013 rajce.net"); - info.translators = Resources.TRANSLATORS; - info.version = _VERSION; - info.website_name = Resources.WEBSITE_NAME; - info.website_url = Resources.WEBSITE_URL; - info.is_license_wordwrapped = false; - info.license = Resources.LICENSE; - info.icons = icon_pixbuf_set; - } - - public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) - { - return new Publishing.Rajce.RajcePublisher(this, host); - } - - public Spit.Publishing.Publisher.MediaType get_supported_media() - { - return( Spit.Publishing.Publisher.MediaType.PHOTO /*| Spit.Publishing.Publisher.MediaType.VIDEO*/ ); - } - - public void activation(bool enabled) {} -} - -namespace Publishing.Rajce -{ - -public class RajcePublisher : Spit.Publishing.Publisher, GLib.Object -{ - private Spit.Publishing.PluginHost host = null; - private Spit.Publishing.ProgressCallback progress_reporter = null; - private Spit.Publishing.Service service = null; - private bool running = false; - private Session session; -// private string username = ""; -// private string token = ""; -// private int last_photo_size = -1; -// private bool hide_album = false; -// private bool show_album = true; -// private bool remember = false; -// private bool strip_metadata = false; - private Album[] albums = null; - private PublishingParameters parameters = null; - private Spit.Publishing.Publisher.MediaType media_type = Spit.Publishing.Publisher.MediaType.NONE; - - public RajcePublisher(Spit.Publishing.Service service, Spit.Publishing.PluginHost host) - { - debug("RajcePublisher created."); - this.service = service; - this.host = host; - this.session = new Session(); - - foreach(Spit.Publishing.Publishable p in host.get_publishables()) - media_type |= p.get_media_type(); - } - - private string get_rajce_url() - { - return "http://www.rajce.idnes.cz/liveAPI/index.php"; - } - - // Publisher interface implementation - - public Spit.Publishing.Service get_service() { return service; } - public Spit.Publishing.PluginHost get_host() { return host; } - public bool is_running() { return running; } - - public void start() - { - if (is_running()) - return; - - debug("RajcePublisher: start"); - running = true; - - if (session.is_authenticated()) - { - debug("RajcePublisher: session is authenticated."); - do_fetch_albums(); - } - else - { - debug("RajcePublisher: session is not authenticated."); - string? persistent_username = get_username(); - string? persistent_token = get_token(); - bool? persistent_remember = get_remember(); - if (persistent_username != null && persistent_token != null) - do_network_login(persistent_username, persistent_token, persistent_remember ); - else - do_show_authentication_pane(); - } - } - - public void stop() - { - debug("RajcePublisher: stop"); - running = false; - } - - // persistent data - - public string? get_url() { return get_rajce_url(); } - public string? get_username() { return host.get_config_string("username", null); } - private void set_username(string username) { host.set_config_string("username", username); } - public string? get_token() { return host.get_config_string("token", null); } - private void set_token(string? token) { host.set_config_string("token", token); } -// public int get_last_photo_size() { return host.get_config_int("last-photo-size", -1); } -// private void set_last_photo_size(int last_photo_size) { host.set_config_int("last-photo-size", last_photo_size); } - public bool get_remember() { return host.get_config_bool("remember", false); } - private void set_remember(bool remember) { host.set_config_bool("remember", remember); } - public bool get_hide_album() { return host.get_config_bool("hide-album", false); } - public void set_hide_album(bool hide_album) { host.set_config_bool("hide-album", hide_album); } - public bool get_show_album() { return host.get_config_bool("show-album", true); } - public void set_show_album(bool show_album) { host.set_config_bool("show-album", show_album); } -// public bool get_strip_metadata() { return host.get_config_bool("strip-metadata", false); } -// private void set_strip_metadata(bool strip_metadata) { host.set_config_bool("strip-metadata", strip_metadata); } - - // Actions and events - - /** - * Action that shows the authentication pane. - */ - private void do_show_authentication_pane(AuthenticationPane.Mode mode = AuthenticationPane.Mode.INTRO) - { - debug("ACTION: installing authentication pane"); - - host.set_service_locked(false); - AuthenticationPane authentication_pane = new AuthenticationPane(this, mode); - authentication_pane.login.connect(on_authentication_pane_login_clicked); - host.install_dialog_pane(authentication_pane, Spit.Publishing.PluginHost.ButtonMode.CLOSE); - host.set_dialog_default_widget(authentication_pane.get_default_widget()); - } - - /** - * Event triggered when the login button in the authentication panel is clicked. - */ - private void on_authentication_pane_login_clicked( string username, string token, bool remember ) - { - debug("EVENT: on_authentication_pane_login_clicked"); - if (!running) - return; - do_network_login(username, token, remember); - } - - /** - * Action to perform a network login to a Rajce service. - */ - private void do_network_login(string username, string token, bool remember) - { - debug("ACTION: logging in"); - host.set_service_locked(true); - host.install_login_wait_pane(); - set_remember( remember ); - set_username( username ); - set_token( remember ? token : null ); - SessionLoginTransaction login_trans = new SessionLoginTransaction(session, get_url(), username, token); - login_trans.network_error.connect(on_login_network_error); - login_trans.completed.connect(on_login_network_complete); - try - { - login_trans.execute(); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: do_network_login"); - do_show_error(err); - } - } - - /** - * Event triggered when the network login action is complete and successful. - */ - private void on_login_network_complete(Publishing.RESTSupport.Transaction txn) - { - debug("EVENT: on_login_network_complete"); - txn.completed.disconnect(on_login_network_complete); - txn.network_error.disconnect(on_login_network_error); - - try - { - Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml); - Xml.Node* response = doc.get_root_node(); - Xml.Node* sessionToken = doc.get_named_child( response, "sessionToken" ); - Xml.Node* maxWidth = doc.get_named_child( response, "maxWidth" ); - Xml.Node* maxHeight = doc.get_named_child( response, "maxHeight" ); - Xml.Node* quality = doc.get_named_child( response, "quality" ); - Xml.Node* nick = doc.get_named_child( response, "nick" ); - int maxW = int.parse( maxWidth->get_content() ); - int maxH = int.parse( maxHeight->get_content() ); - if( maxW > maxH ) - { - maxH = maxW; - } - session.authenticate( sessionToken->get_content(), nick->get_content(), 0, maxH, int.parse( quality->get_content() ) ); - } - catch (Spit.Publishing.PublishingError err) - { - int code_int = int.parse(err.message); - if (code_int == 999) - { - debug("ERROR: on_login_network_complete, code 999"); - do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_USER); - } - else - { - debug("ERROR: on_login_network_complete"); - do_show_error(err); - } - return; - } - do_fetch_albums(); - } - - /** - * Event triggered when a network login action fails due to a network error. - */ - private void on_login_network_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err ) - { - debug("EVENT: on_login_network_error"); - bad_txn.completed.disconnect(on_login_network_complete); - bad_txn.network_error.disconnect(on_login_network_error); - do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_USER); - } - - /** - * Action that fetches all user albums from the Rajce. - */ - private void do_fetch_albums() - { - debug("ACTION: fetching albums"); - host.set_service_locked(true); - host.install_account_fetch_wait_pane(); - - GetAlbumsTransaction get_albums_trans = new GetAlbumsTransaction(session, get_url() ); - get_albums_trans.network_error.connect(on_albums_fetch_error); - get_albums_trans.completed.connect(on_albums_fetch_complete); - - try - { - get_albums_trans.execute(); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: do_fetch_albums"); - do_show_error(err); - } - } - - /** - * Event triggered when the fetch albums action completes successfully. - */ - private void on_albums_fetch_complete(Publishing.RESTSupport.Transaction txn) - { - debug("EVENT: on_albums_fetch_complete"); - txn.completed.disconnect(on_albums_fetch_complete); - txn.network_error.disconnect(on_albums_fetch_error); - debug("RajcePlugin: list of albums: %s", txn.get_response()); - if (albums != null) - { - albums = null; - } - Gee.ArrayList<Album> list = new Gee.ArrayList<Album>(); - try - { - Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml); - Xml.Node* response = doc.get_root_node(); - Xml.Node* sessionToken = doc.get_named_child( response, "sessionToken" ); - Xml.Node* nodealbums = doc.get_named_child( response, "albums" ); - for( Xml.Node* album = nodealbums->children; album != null; album = album->next ) - { - int id = int.parse( album->get_prop("id") ); - string albumName = doc.get_named_child( album, "albumName" )->get_content(); - string url = doc.get_named_child( album, "url" )->get_content(); - string thumbUrl = doc.get_named_child( album, "thumbUrl" )->get_content(); - string createDate = doc.get_named_child( album, "createDate" )->get_content(); - string updateDate = doc.get_named_child( album, "updateDate" )->get_content(); - bool hidden = ( int.parse( doc.get_named_child( album, "hidden" )->get_content() ) > 0 ? true : false ); - bool secure = ( int.parse( doc.get_named_child( album, "secure" )->get_content() ) > 0 ? true : false ); - int photoCount = int.parse( doc.get_named_child( album, "photoCount" )->get_content() ); - list.insert( 0, new Album( id, albumName, url, thumbUrl, createDate, updateDate, hidden, secure, photoCount ) ); - } - list.sort( Album.compare_albums ); - albums = list.to_array(); - session.set_usertoken( sessionToken->get_content() ); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: on_albums_fetch_complete"); - do_show_error(err); - return; - } - do_show_publishing_options_pane(); - } - - /** - * Event triggered when the fetch albums transaction fails due to a network error. - */ - private void on_albums_fetch_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err ) - { - debug("EVENT: on_albums_fetch_error"); - bad_txn.completed.disconnect(on_albums_fetch_complete); - bad_txn.network_error.disconnect(on_albums_fetch_error); - on_network_error(bad_txn, err); - } - - /** - * Action that shows the publishing options pane. - */ - private void do_show_publishing_options_pane() - { - debug("ACTION: installing publishing options pane"); - host.set_service_locked(false); - PublishingOptionsPane opts_pane = new PublishingOptionsPane( this, session.get_username(), albums ); - opts_pane.logout.connect(on_publishing_options_pane_logout_clicked); - opts_pane.publish.connect(on_publishing_options_pane_publish_clicked); - host.install_dialog_pane(opts_pane, Spit.Publishing.PluginHost.ButtonMode.CLOSE); - host.set_dialog_default_widget(opts_pane.get_default_widget()); - } - - /** - * Event triggered when the user clicks logout in the publishing options pane. - */ - private void on_publishing_options_pane_logout_clicked() - { - debug("EVENT: on_publishing_options_pane_logout_clicked"); - session.deauthenticate(); - do_show_authentication_pane( AuthenticationPane.Mode.INTRO ); - } - - /** - * Event triggered when the user clicks publish in the publishing options pane. - * - * @param parameters the publishing parameters - */ - private void on_publishing_options_pane_publish_clicked( PublishingParameters parameters ) - { - debug("EVENT: on_publishing_options_pane_publish_clicked"); - this.parameters = parameters; - do_begin_upload(); - } - - /** - * Begin upload action: open existing album or create a new one - */ - private void do_begin_upload() - { - host.set_service_locked(true); - if( parameters.album_id == 0 ) - { - // new album - debug("ACTION: closing album"); - CreateAlbumTransaction create_album_trans = new CreateAlbumTransaction(session, get_url(), parameters.album_name, this.parameters.album_hidden ); - create_album_trans.network_error.connect(on_create_album_error); - create_album_trans.completed.connect(on_create_album_complete); - try - { - create_album_trans.execute(); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: create album"); - do_show_error(err); - } - } - else - { - // existing album - debug("ACTION: opening album"); - OpenAlbumTransaction open_album_trans = new OpenAlbumTransaction(session, get_url(), parameters.album_id ); - open_album_trans.network_error.connect(on_open_album_error); - open_album_trans.completed.connect(on_open_album_complete); - try - { - open_album_trans.execute(); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: open album"); - do_show_error(err); - } - } - } - - /** - * Event triggered when the create album completes successfully. - */ - private void on_create_album_complete( Publishing.RESTSupport.Transaction txn) - { - debug("EVENT: on_create_album_complete"); - txn.completed.disconnect(on_create_album_complete); - txn.network_error.disconnect(on_create_album_error); - debug("RajcePlugin: create album: %s", txn.get_response()); - try - { - Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml); - Xml.Node* response = doc.get_root_node(); - string sessionToken = doc.get_named_child( response, "sessionToken" )->get_content(); - string albumToken = doc.get_named_child( response, "albumToken" )->get_content(); - parameters.album_id = int.parse( doc.get_named_child( response, "albumID" )->get_content() ); - session.set_usertoken( sessionToken ); - session.set_albumtoken( albumToken ); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: on_create_album_complete"); - do_show_error(err); - return; - } - do_upload_photos(); - } - - /** - * Event triggered when the create album transaction fails due to a network error. - */ - private void on_create_album_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err ) - { - debug("EVENT: on_create_album_error"); - bad_txn.completed.disconnect(on_create_album_complete); - bad_txn.network_error.disconnect(on_create_album_error); - on_network_error(bad_txn, err); - } - - /** - * Event triggered when the open album completes successfully. - */ - private void on_open_album_complete(Publishing.RESTSupport.Transaction txn) - { - debug("EVENT: on_open_album_complete"); - txn.completed.disconnect(on_open_album_complete); - txn.network_error.disconnect(on_open_album_error); - debug("RajcePlugin: open album: %s", txn.get_response()); - try - { - Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml); - Xml.Node* response = doc.get_root_node(); - string sessionToken = doc.get_named_child( response, "sessionToken" )->get_content(); - string albumToken = doc.get_named_child( response, "albumToken" )->get_content(); - session.set_usertoken( sessionToken ); - session.set_albumtoken( albumToken ); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: on_open_album_complete"); - do_show_error(err); - return; - } - do_upload_photos(); - } - - /** - * Event triggered when the open album transaction fails due to a network error. - */ - private void on_open_album_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err ) - { - debug("EVENT: on_open_album_error"); - bad_txn.completed.disconnect(on_open_album_complete); - bad_txn.network_error.disconnect(on_open_album_error); - on_network_error(bad_txn, err); - } - - /** - * Upload photos: the key part of the plugin - */ - private void do_upload_photos() - { - debug("ACTION: uploading photos"); - progress_reporter = host.serialize_publishables( session.get_maxsize() ); - Spit.Publishing.Publishable[] publishables = host.get_publishables(); - - Uploader uploader = new Uploader( session, get_url(), publishables, parameters ); - uploader.upload_complete.connect( on_upload_photos_complete ); - uploader.upload_error.connect( on_upload_photos_error ); - uploader.upload( on_upload_photos_status_updated ); - } - - /** - * Event triggered when the batch uploader reports that at least one of the - * network transactions encapsulating uploads has completed successfully - */ - private void on_upload_photos_complete(Publishing.RESTSupport.BatchUploader uploader, int num_published) - { - debug("EVENT: on_upload_photos_complete"); - uploader.upload_complete.disconnect(on_upload_photos_complete); - uploader.upload_error.disconnect(on_upload_photos_error); - - // TODO: should a message be displayed to the user if num_published is zero? - do_end_upload(); - } - - /** - * Event triggered when the batch uploader reports that at least one of the - * network transactions encapsulating uploads has caused a network error - */ - private void on_upload_photos_error( Publishing.RESTSupport.BatchUploader uploader, Spit.Publishing.PublishingError err) - { - debug("EVENT: on_upload_photos_error"); - uploader.upload_complete.disconnect(on_upload_photos_complete); - uploader.upload_error.disconnect(on_upload_photos_error); - do_show_error(err); - } - - /** - * Event triggered when upload progresses and the status needs to be updated. - */ - private void on_upload_photos_status_updated(int file_number, double completed_fraction) - { - if( is_running() ) - { - debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction); - assert(progress_reporter != null); - progress_reporter(file_number, completed_fraction); - } - } - - private void do_end_upload() - { - if( get_show_album() ) - { - do_get_album_url(); - } - else - { - do_close_album(); - } - } - - /** - * End upload action: get album url - */ - private void do_get_album_url() - { - debug("ACTION: getting album URL"); - host.set_service_locked(true); - GetAlbumUrlTransaction get_album_url_trans = new GetAlbumUrlTransaction(session, get_url() ); - get_album_url_trans.network_error.connect(on_get_album_url_error); - get_album_url_trans.completed.connect(on_get_album_url_complete); - try - { - get_album_url_trans.execute(); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: close album"); - do_show_error(err); - } - } - - /** - * Event triggered when the get album url completes successfully. - */ - private void on_get_album_url_complete(Publishing.RESTSupport.Transaction txn) - { - debug("EVENT: on_get_album_url_complete"); - txn.completed.disconnect(on_get_album_url_complete); - txn.network_error.disconnect(on_get_album_url_error); - debug("RajcePlugin: get album url: %s", txn.get_response()); - try - { - Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml); - Xml.Node* response = doc.get_root_node(); - string sessionToken = doc.get_named_child( response, "sessionToken" )->get_content(); - string url = doc.get_named_child( response, "url" )->get_content(); - session.set_usertoken( sessionToken ); - session.set_albumticket( url ); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: on_get_album_url_complete"); - // ignore this error -// do_show_error(err); -// return; - } - do_close_album(); - } - - /** - * Event triggered when the get album url transaction fails due to a network error. - */ - private void on_get_album_url_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err ) - { - debug("EVENT: on_get_album_url_error"); - bad_txn.completed.disconnect(on_get_album_url_complete); - bad_txn.network_error.disconnect(on_get_album_url_error); - // ignore this error -// on_network_error(bad_txn, err); - do_close_album(); - } - - - /** - * End upload action: close album - */ - private void do_close_album() - { - debug("ACTION: closing album"); - host.set_service_locked(true); - CloseAlbumTransaction close_album_trans = new CloseAlbumTransaction(session, get_url() ); - close_album_trans.network_error.connect(on_close_album_error); - close_album_trans.completed.connect(on_close_album_complete); - try - { - close_album_trans.execute(); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: close album"); - do_show_error(err); - } - } - - /** - * Event triggered when the close album completes successfully. - */ - private void on_close_album_complete(Publishing.RESTSupport.Transaction txn) - { - debug("EVENT: on_close_album_complete"); - txn.completed.disconnect(on_close_album_complete); - txn.network_error.disconnect(on_close_album_error); - debug("RajcePlugin: close album: %s", txn.get_response()); - try - { - Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml); - Xml.Node* response = doc.get_root_node(); - string sessionToken = doc.get_named_child( response, "sessionToken" )->get_content(); - session.set_usertoken( sessionToken ); - session.set_albumtoken( null ); - } - catch (Spit.Publishing.PublishingError err) - { - debug("ERROR: on_close_album_complete"); - do_show_error(err); - return; - } - do_show_success_pane(); - } - - /** - * Event triggered when the close album transaction fails due to a network error. - */ - private void on_close_album_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err ) - { - debug("EVENT: on_close_album_error"); - bad_txn.completed.disconnect(on_close_album_complete); - bad_txn.network_error.disconnect(on_close_album_error); - // ignore this error -// on_network_error(bad_txn, err); - do_show_success_pane(); - } - - - /** - * Action to display the success pane in the publishing dialog. - */ - private void do_show_success_pane() - { - debug("ACTION: installing success pane"); - if( get_show_album() && session.get_albumticket() != null ) - { - try - { - GLib.Process.spawn_command_line_async( "xdg-open " + session.get_albumticket() ); - } - catch( GLib.SpawnError e ) - { - } - } - host.set_service_locked(false); - host.install_success_pane(); - } - - /** - * Helper event to handle network errors. - */ - private void on_network_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err ) - { - debug("EVENT: on_network_error"); - do_show_error(err); - } - - /** - * Action to display an error to the user. - */ - private void do_show_error(Spit.Publishing.PublishingError e) - { - debug("ACTION: do_show_error"); - string error_type = "UNKNOWN"; - if (e is Spit.Publishing.PublishingError.NO_ANSWER) - { - do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_USER); - return; - } else if(e is Spit.Publishing.PublishingError.COMMUNICATION_FAILED) { - error_type = "COMMUNICATION_FAILED"; - } else if(e is Spit.Publishing.PublishingError.PROTOCOL_ERROR) { - error_type = "PROTOCOL_ERROR"; - } else if(e is Spit.Publishing.PublishingError.SERVICE_ERROR) { - error_type = "SERVICE_ERROR"; - } else if(e is Spit.Publishing.PublishingError.MALFORMED_RESPONSE) { - error_type = "MALFORMED_RESPONSE"; - } else if(e is Spit.Publishing.PublishingError.LOCAL_FILE_ERROR) { - error_type = "LOCAL_FILE_ERROR"; - } else if(e is Spit.Publishing.PublishingError.EXPIRED_SESSION) { - error_type = "EXPIRED_SESSION"; - } - - debug("Unhandled error: type=%s; message='%s'".printf(error_type, e.message)); - do_show_error_message(_("An error message occurred when publishing to Rajce. Please try again.")); - } - - /** - * Action to display an error message to the user. - */ - private void do_show_error_message(string message) - { - debug("ACTION: do_show_error_message"); - host.install_static_message_pane(message, Spit.Publishing.PluginHost.ButtonMode.CLOSE); - } - -} - -// Rajce Album -internal class Album -{ - public int id; - public string albumName; - public string url; - public string thumbUrl; - public string createDate; - public string updateDate; - public bool hidden; - public bool secure; - public int photoCount; - - public Album( int id, string albumName, string url, string thumbUrl, string createDate, string updateDate, bool hidden, bool secure, int photoCount ) - { - this.id = id; - this.albumName = albumName; - this.url = url; - this.thumbUrl = thumbUrl; - this.createDate = createDate; - this.updateDate = updateDate; - this.hidden = hidden; - this.secure = secure; - this.photoCount = photoCount; - } - public static int compare_albums(Album? a, Album? b) - { - if( a == null && b == null ) - { - return 0; - } - else if( a == null && b != null ) - { - return 1; - } - else if( a != null && b == null ) - { - return -1; - } - return( b.updateDate.ascii_casecmp( a.updateDate ) ); - } -} - -// Uploader -internal class Uploader : Publishing.RESTSupport.BatchUploader -{ - private PublishingParameters parameters; - private string url; - - public Uploader(Session session, string url, Spit.Publishing.Publishable[] publishables, PublishingParameters parameters) - { - base(session, publishables); - this.parameters = parameters; - this.url = url; - } - - protected override Publishing.RESTSupport.Transaction create_transaction( Spit.Publishing.Publishable publishable ) - { - return new AddPhotoTransaction((Session) get_session(), url, parameters, publishable); - } -} - -// UI elements - -/** - * The authentication pane used when asking service URL, user name and password - * from the user. - */ -internal class AuthenticationPane : Spit.Publishing.DialogPane, Object -{ - public enum Mode - { - INTRO, - FAILED_RETRY_USER - } - private static string INTRO_MESSAGE = _("Enter email and password associated with your Rajce account."); - private static string FAILED_RETRY_USER_MESSAGE = _("Invalid email and/or password. Please try again"); - - private Gtk.Box pane_widget = null; - private Gtk.Builder builder; - private Gtk.Entry username_entry; - private Gtk.Entry password_entry; - private Gtk.CheckButton remember_checkbutton; - private Gtk.Button login_button; - private bool crypt = true; - - public signal void login( string user, string token, bool remember ); - - public AuthenticationPane( RajcePublisher publisher, Mode mode = Mode.INTRO ) - { - this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); - try - { - builder = new Gtk.Builder(); - builder.add_from_resource (Resources.RESOURCE_PATH + - "/rajce_authentication_pane.ui"); - builder.connect_signals(null); - var content = builder.get_object ("content") as Gtk.Box; - Gtk.Label message_label = builder.get_object("message_label") as Gtk.Label; - switch (mode) - { - case Mode.INTRO: - message_label.set_text(INTRO_MESSAGE); - break; - - case Mode.FAILED_RETRY_USER: - message_label.set_markup("<b>%s</b>\n\n%s".printf(_( - "Invalid User Email or Password"), FAILED_RETRY_USER_MESSAGE)); - break; - } - username_entry = builder.get_object ("username_entry") as Gtk.Entry; - string? persistent_username = publisher.get_username(); - if (persistent_username != null) - { - username_entry.set_text(persistent_username); - } - password_entry = builder.get_object ("password_entry") as Gtk.Entry; - string? persistent_token = publisher.get_token(); - if (persistent_token != null) - { - password_entry.set_text(persistent_token); - this.crypt = false; - } - else - { - this.crypt = true; - } - remember_checkbutton = builder.get_object ("remember_checkbutton") as Gtk.CheckButton; - remember_checkbutton.set_active(publisher.get_remember()); - login_button = builder.get_object("login_button") as Gtk.Button; - - Gtk.Label label2 = builder.get_object("label2") as Gtk.Label; - Gtk.Label label3 = builder.get_object("label3") as Gtk.Label; - - label2.set_label(_("_Email address") ); - label3.set_label(_("_Password") ); - remember_checkbutton.set_label(_("_Remember") ); - login_button.set_label(_("Log in") ); - - username_entry.changed.connect(on_user_changed); - password_entry.changed.connect(on_password_changed); - login_button.clicked.connect(on_login_button_clicked); - content.parent.remove (content); - pane_widget.add (content); - publisher.get_host().set_dialog_default_widget(login_button); - } - catch (Error e) - { - warning("Could not load UI: %s", e.message); - } - } - - public Gtk.Widget get_default_widget() - { - return login_button; - } - - private void on_login_button_clicked() - { - string token = password_entry.get_text(); - if( this.crypt ) - { - token = GLib.Checksum.compute_for_string( GLib.ChecksumType.MD5, token ); - } - login(username_entry.get_text(), token, remember_checkbutton.get_active()); - } - - private void on_user_changed() - { - update_login_button_sensitivity(); - } - - private void on_password_changed() - { - this.crypt = true; - update_login_button_sensitivity(); - } - - private void update_login_button_sensitivity() - { - login_button.set_sensitive(username_entry.text_length > 0 && - password_entry.text_length > 0); - } - - public Gtk.Widget get_widget() - { - return pane_widget; - } - - public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() - { - return Spit.Publishing.DialogPane.GeometryOptions.NONE; - } - - public void on_pane_installed() - { - username_entry.grab_focus(); - password_entry.set_activates_default(true); - login_button.can_default = true; - update_login_button_sensitivity(); - } - public void on_pane_uninstalled() {} - -} - -internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object -{ - RajcePublisher publisher; - private Album[] albums; - private string username; - - private Gtk.Builder builder = null; - private Gtk.Box pane_widget = null; - private Gtk.Label login_identity_label = null; - private Gtk.Label publish_to_label = null; - private Gtk.RadioButton use_existing_radio = null; - private Gtk.ComboBoxText existing_albums_combo = null; - private Gtk.RadioButton create_new_radio = null; - private Gtk.Entry new_album_entry = null; - private Gtk.CheckButton hide_check = null; - private Gtk.CheckButton show_check = null; - private Gtk.Button publish_button = null; - private Gtk.Button logout_button = null; - - public signal void publish( PublishingParameters parameters ); - public signal void logout(); - - public PublishingOptionsPane( RajcePublisher publisher, string username, Album[] albums ) - { - this.username = username; - this.albums = albums; - this.publisher = publisher; - this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); - - try - { - this.builder = new Gtk.Builder(); - builder.add_from_resource (Resources.RESOURCE_PATH + "/rajce_publishing_options_pane.ui"); - builder.connect_signals(null); - - pane_widget = (Gtk.Box) builder.get_object("rajce_pane_widget"); - login_identity_label = (Gtk.Label) builder.get_object("login_identity_label"); - publish_to_label = (Gtk.Label) builder.get_object("publish_to_label"); - use_existing_radio = (Gtk.RadioButton) builder.get_object("use_existing_radio"); - existing_albums_combo = (Gtk.ComboBoxText) builder.get_object("existing_albums_combo"); - create_new_radio = (Gtk.RadioButton) builder.get_object("create_new_radio"); - new_album_entry = (Gtk.Entry) builder.get_object("new_album_entry"); - hide_check = (Gtk.CheckButton) builder.get_object("hide_check"); - hide_check.set_label(_("_Hide album") ); - show_check = (Gtk.CheckButton) builder.get_object("show_check"); - publish_button = (Gtk.Button) builder.get_object("publish_button"); - logout_button = (Gtk.Button) builder.get_object("logout_button"); - - hide_check.set_active( publisher.get_hide_album() ); - show_check.set_active( publisher.get_show_album() ); - login_identity_label.set_label(_("You are logged into Rajce as %s.").printf(username)); - publish_to_label.set_label(_("Photos will appear in:")); - use_existing_radio.set_label(_("An _existing album:") ); - create_new_radio.set_label(_("A _new album named:") ); - show_check.set_label(_("Open target _album in browser") ); - publish_button.set_label(_("_Publish") ); - logout_button.set_label(_("_Logout") ); - - use_existing_radio.clicked.connect(on_use_existing_radio_clicked); - create_new_radio.clicked.connect(on_create_new_radio_clicked); - new_album_entry.changed.connect(on_new_album_entry_changed); - logout_button.clicked.connect(on_logout_clicked); - publish_button.clicked.connect(on_publish_clicked); - } - catch (Error e) - { - warning("Could not load UI: %s", e.message); - } - - } - - private void on_publish_clicked() - { - bool show_album = show_check.get_active(); - publisher.set_show_album( show_album ); - if (create_new_radio.get_active()) - { - string album_name = new_album_entry.get_text(); - bool hide_album = hide_check.get_active(); - publisher.set_hide_album( hide_album ); - publish( new PublishingParameters.to_new_album( album_name, hide_album ) ); - } - else - { - int id = albums[existing_albums_combo.get_active()].id; - string album_name = albums[existing_albums_combo.get_active()].albumName; - publish( new PublishingParameters.to_existing_album( album_name, id ) ); - } - } - - private void on_use_existing_radio_clicked() - { - existing_albums_combo.set_sensitive(true); - new_album_entry.set_sensitive(false); - existing_albums_combo.grab_focus(); - update_publish_button_sensitivity(); - hide_check.set_sensitive(false); - } - - private void on_create_new_radio_clicked() - { - new_album_entry.set_sensitive(true); - existing_albums_combo.set_sensitive(false); - new_album_entry.grab_focus(); - update_publish_button_sensitivity(); - hide_check.set_sensitive(true); - } - - private void on_logout_clicked() - { - logout(); - } - private void update_publish_button_sensitivity() - { - string album_name = new_album_entry.get_text(); - publish_button.set_sensitive( album_name.strip() != "" || !create_new_radio.get_active()); - } - private void on_new_album_entry_changed() - { - update_publish_button_sensitivity(); - } - public void installed() - { - for (int i = 0; i < albums.length; i++) - { - // TODO: sort albums according to their updateDate property - existing_albums_combo.append_text( albums[i].albumName ); - } - if (albums.length == 0) - { - existing_albums_combo.set_sensitive(false); - use_existing_radio.set_sensitive(false); - } - else - { - existing_albums_combo.set_active(0); - existing_albums_combo.set_sensitive(true); - use_existing_radio.set_sensitive(true); - } - create_new_radio.set_active(true); - on_create_new_radio_clicked(); - } - - protected void notify_publish(PublishingParameters parameters) - { - publish( parameters ); - } - - protected void notify_logout() - { - logout(); - } - - public Gtk.Widget get_default_widget() - { - return logout_button; - } - public Gtk.Widget get_widget() - { - return pane_widget; - } - - public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() - { - return Spit.Publishing.DialogPane.GeometryOptions.NONE; - } - - public void on_pane_installed() - { - installed(); - publish.connect(notify_publish); - logout.connect(notify_logout); - } - - public void on_pane_uninstalled() - { - publish.disconnect(notify_publish); - logout.disconnect(notify_logout); - } -} - -internal class PublishingParameters -{ - public string? album_name; - public bool? album_hidden; - public int? album_id; - - private PublishingParameters() - { - } - public PublishingParameters.to_new_album( string album_name, bool album_hidden ) - { - this.album_name = album_name; - this.album_hidden = album_hidden; - this.album_id = 0; - } - public PublishingParameters.to_existing_album( string album_name, int album_id ) - { - this.album_name = album_name; - this.album_hidden = null; - this.album_id = album_id; - } -} - -// REST support classes -/** - * Session class that keeps track of the credentials - */ -internal class Session : Publishing.RESTSupport.Session { - private string? usertoken = null; - private string? albumtoken = null; - private string? albumticket = null; - private string? username = null; - private int? userid = null; - private int? maxsize = null; - private int? quality = null; - - public Session() - { - base(""); - } - - public override bool is_authenticated() - { - return (userid != null && usertoken != null && username != null); - } - - public void authenticate(string token, string name, int id, int maxsize, int quality ) - { - this.usertoken = token; - this.username = name; - this.userid = id; - this.maxsize = maxsize; - this.quality = quality; - } - - public void deauthenticate() - { - usertoken = null; - albumtoken = null; - albumticket = null; - username = null; - userid = null; - maxsize = null; - quality = null; - } - - public void set_usertoken( string? usertoken ){ this.usertoken = usertoken; } - public void set_albumtoken( string? albumtoken ){ this.albumtoken = albumtoken; } - public void set_albumticket( string? albumticket ){ this.albumticket = albumticket; } - - public string get_usertoken() { return usertoken; } - public string get_albumtoken() { return albumtoken; } - public string get_albumticket() { return albumticket; } - public string get_username() { return username; } -// public int get_userid() { return userid; } - public int get_maxsize() { return maxsize; } -// public int get_quality() { return quality; } -} - -internal class ArgItem -{ - public string? key; - public string? val; - public ArgItem[] children; - - public ArgItem( string? k, string? v ) - { - key = k; - val = v; - children = new ArgItem[0]; - } - public void AddChild( ArgItem child ) - { - children += child; - } - public void AddChildren( ArgItem[] newchildren ) - { - foreach( ArgItem child in newchildren ) - { - AddChild( child ); - } - } - ~ArgItem() - { - foreach( ArgItem child in children ) - { - child = null; - } - } -} - -/// <summary> -/// implementation of Rajce Live API -/// </summary> -internal class LiveApiRequest -{ - private ArgItem[] _params; - private string _cmd; - public LiveApiRequest( string cmd ) - { - _params = new ArgItem[0]; - _cmd = cmd; - } - /// <summary> - /// add string parameter - /// </summary> - public void AddParam( string name, string val ) - { - _params += new ArgItem( name, val ); - } - /// <summary> - /// add boolean parameter - /// </summary> - public void AddParamBool( string name, bool val ) - { - AddParam( name, val ? "1" : "0" ); - } - /// <summary> - /// add integer parameter - /// </summary> - public void AddParamInt( string name, int val ) - { - AddParam( name, val.to_string() ); - } -/* /// <summary> - /// add double parameter - /// </summary> - public void AddParamDouble( string name, double val ) - { - AddParam( name, val.to_string() ); - } -*/ /// <summary> - /// add compound parameter - /// </summary> - public void AddParamNode( string name, ArgItem[] val ) - { - ArgItem newItem = new ArgItem( name, null ); - newItem.AddChildren( val ); - _params += newItem; - } - /// <summary> - /// create XML fragment containing all parameters - /// </summary> - public string Params2XmlString( bool urlencode = true ) - { - Xml.Doc* doc = new Xml.Doc( "1.0" ); - Xml.Node* root = new Xml.Node( null, "request" ); - doc->set_root_element( root ); - root->new_text_child( null, "command", _cmd ); - Xml.Node* par = root->new_text_child( null, "parameters", "" ); - foreach( ArgItem arg in _params ) - { - WriteParam( par, arg ); - } - string xmlstr; - doc->dump_memory_enc( out xmlstr ); - delete doc; - if( urlencode ) - { - return Soup.URI.encode( xmlstr, "&;" ); - } - return xmlstr; - } - /// <summary> - /// write single or compound (recursively) parameter into XML - /// </summary> - private static void WriteParam( Xml.Node* node, ArgItem arg ) - { - if( arg.children.length == 0 ) - { - node->new_text_child( null, arg.key, arg.val ); - } - else - { - Xml.Node* subnode = node->new_text_child( null, arg.key, "" ); - foreach( ArgItem child in arg.children ) - { - WriteParam( subnode, child ); - } - } - } -} - - -/** - * Generic REST transaction class. - * - * This class implements the generic logic for all REST transactions used - * by the Rajce publishing plugin. - */ -internal class Transaction : Publishing.RESTSupport.Transaction -{ - public Transaction(Session session) - { - base(session); - } - - public static string? validate_xml(Publishing.RESTSupport.XmlDocument doc) - { - Xml.Node* root = doc.get_root_node(); - if( root == null ) - { - return "No XML returned from server"; - } - string name = root->name; - - // treat malformed root as an error condition - if( name == null || name != "response" ) - { - return "No response from Rajce in XML"; - } - Xml.Node* errcode; - Xml.Node* result; - try - { - errcode = doc.get_named_child(root, "errorCode"); - result = doc.get_named_child(root, "result"); - } - catch (Spit.Publishing.PublishingError err) - { - return null; - } - return "999 Rajce Error [%d]: %s".printf( int.parse( errcode->get_content() ), result->get_content() ); - } -} - -/** - * Transaction used to implement the network login interaction. - */ -internal class SessionLoginTransaction : Transaction -{ - public SessionLoginTransaction(Session session, string url, string username, string token) - { - debug("SessionLoginTransaction: URL: %s", url); - base.with_endpoint_url(session, url); - LiveApiRequest req = new LiveApiRequest( "login" ); - req.AddParam( "clientID", "RajceShotwellPlugin" ); - req.AddParam( "currentVersion", "1.1.1.1" ); - req.AddParam( "login", username ); - req.AddParam( "password", token ); - string xml = req.Params2XmlString(); - add_argument("data", xml); - } -} - -/** - * Transaction used to implement the get albums interaction. - */ -internal class GetAlbumsTransaction : Transaction -{ - public GetAlbumsTransaction(Session session, string url) - { - base.with_endpoint_url(session, url); - LiveApiRequest req = new LiveApiRequest( "getAlbumList" ); - req.AddParam( "token", session.get_usertoken() ); - ArgItem[] columns = new ArgItem[0]; - columns += new ArgItem( "column", "viewCount" ); - columns += new ArgItem( "column", "isFavourite" ); - columns += new ArgItem( "column", "descriptionHtml" ); - columns += new ArgItem( "column", "coverPhotoID" ); - columns += new ArgItem( "column", "localPath" ); - req.AddParamNode( "columns", columns ); - string xml = req.Params2XmlString(); - add_argument("data", xml ); - } -} - -/** - * Transaction used to implement the create album interaction. - */ -internal class CreateAlbumTransaction : Transaction -{ - public CreateAlbumTransaction( Session session, string url, string albumName, bool hidden ) - { - base.with_endpoint_url(session, url); - LiveApiRequest req = new LiveApiRequest( "createAlbum" ); - req.AddParam( "token", session.get_usertoken() ); - req.AddParam( "albumName", albumName ); - req.AddParam( "albumDescription", "" ); - req.AddParamBool( "albumVisible", !hidden ); - string xml = req.Params2XmlString(); - add_argument("data", xml); - } -} - -/** - * Transaction used to implement the open album interaction. - */ -internal class OpenAlbumTransaction : Transaction -{ - public OpenAlbumTransaction( Session session, string url, int albumID ) - { - base.with_endpoint_url(session, url); - LiveApiRequest req = new LiveApiRequest( "openAlbum" ); - req.AddParam( "token", session.get_usertoken() ); - req.AddParamInt( "albumID", albumID ); - string xml = req.Params2XmlString(); - add_argument("data", xml); - } -} - -/** - * Transaction used to implement the close album interaction. - */ -internal class GetAlbumUrlTransaction : Transaction -{ - public GetAlbumUrlTransaction( Session session, string url ) - { - base.with_endpoint_url(session, url); - LiveApiRequest req = new LiveApiRequest( "getAlbumUrl" ); - req.AddParam( "token", session.get_usertoken() ); - req.AddParam( "albumToken", session.get_albumtoken() ); - string xml = req.Params2XmlString(); - add_argument("data", xml); - } -} - -/** - * Transaction used to implement the close album interaction. - */ -internal class CloseAlbumTransaction : Transaction -{ - public CloseAlbumTransaction( Session session, string url ) - { - base.with_endpoint_url(session, url); - LiveApiRequest req = new LiveApiRequest( "closeAlbum" ); - req.AddParam( "token", session.get_usertoken() ); - req.AddParam( "albumToken", session.get_albumtoken() ); - string xml = req.Params2XmlString(); - add_argument("data", xml); - } -} - -/** - * Transaction used to implement the get categories interaction. - */ -internal class GetCategoriesTransaction : Transaction -{ - public GetCategoriesTransaction( Session session, string url ) - { - base.with_endpoint_url(session, url); - LiveApiRequest req = new LiveApiRequest( "getCategories" ); - req.AddParam( "token", session.get_usertoken() ); - string xml = req.Params2XmlString(); - add_argument("data", xml); - } -} - -/** - * Transaction used to implement the upload photo. - */ -private class AddPhotoTransaction : Publishing.RESTSupport.UploadTransaction -{ - private PublishingParameters parameters = null; - - public AddPhotoTransaction(Session session, string url, PublishingParameters parameters, Spit.Publishing.Publishable publishable) - { - base.with_endpoint_url( session, publishable, url ); - this.parameters = parameters; - - debug("RajcePlugin: Uploading photo %s to%s album %s", publishable.get_serialized_file().get_basename(), ( parameters.album_id > 0 ? "" : " new" ), parameters.album_name ); - - string basename = publishable.get_param_string( Spit.Publishing.Publishable.PARAM_STRING_BASENAME ); - string comment = publishable.get_param_string( Spit.Publishing.Publishable.PARAM_STRING_COMMENT ); - string pubname = publishable.get_publishing_name(); - - int width = session.get_maxsize(); - int height = session.get_maxsize(); - - LiveApiRequest req = new LiveApiRequest( "addPhoto" ); - req.AddParam( "token", session.get_usertoken() ); - req.AddParamInt( "width", width ); - req.AddParamInt( "height", height ); - req.AddParam( "albumToken", session.get_albumtoken() ); - req.AddParam( "photoName", pubname ); - req.AddParam( "fullFileName", basename ); - req.AddParam( "description", ( comment != null ? comment : "" ) ); - string xml = req.Params2XmlString( false ); - add_argument( "data", xml ); - - GLib.HashTable<string, string> disposition_table = new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal); - disposition_table.insert("name", "photo"); - disposition_table.insert("filename", Soup.URI.encode( basename, null ) ); - set_binary_disposition_table( disposition_table ); - } - -} - - -} - diff --git a/plugins/shotwell-publishing-extras/YandexPublishing.vala b/plugins/shotwell-publishing-extras/YandexPublishing.vala deleted file mode 100644 index 04b28d2..0000000 --- a/plugins/shotwell-publishing-extras/YandexPublishing.vala +++ /dev/null @@ -1,642 +0,0 @@ -/* Copyright 2010+ Evgeniy Polyakov <zbr@ioremap.net> - * - * This software is licensed under the GNU LGPL (version 2.1 or later). - * See the COPYING file in this distribution. - */ - -public class YandexService : Object, Spit.Pluggable, Spit.Publishing.Service { - public int get_pluggable_interface(int min_host_interface, int max_host_interface) { - return Spit.negotiate_interfaces(min_host_interface, max_host_interface, Spit.Publishing.CURRENT_INTERFACE); - } - - public unowned string get_id() { - return "org.yorba.shotwell.publishing.yandex-fotki"; - } - - public unowned string get_pluggable_name() { - return "Yandex.Fotki"; - } - - public void get_info(ref Spit.PluggableInfo info) { - info.authors = "Evgeniy Polyakov <zbr@ioremap.net>"; - info.copyright = _("Copyright 2010+ Evgeniy Polyakov <zbr@ioremap.net>"); - info.translators = Resources.TRANSLATORS; - info.version = _VERSION; - info.website_name = _("Visit the Yandex.Fotki web site"); - info.website_url = "https://fotki.yandex.ru/"; - info.is_license_wordwrapped = false; - info.license = Resources.LICENSE; - } - - public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { - return new Publishing.Yandex.YandexPublisher(this, host); - } - - public Spit.Publishing.Publisher.MediaType get_supported_media() { - return (Spit.Publishing.Publisher.MediaType.PHOTO); - } - - public void activation(bool enabled) { - } -} - -namespace Publishing.Yandex { - -internal const string SERVICE_NAME = "Yandex.Fotki"; - -private const string client_id = "52be4756dee3438792c831a75d7cd360"; - -internal class Transaction: Publishing.RESTSupport.Transaction { - public Transaction.with_url(Session session, string url, Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.GET) { - base.with_endpoint_url(session, url, method); - add_headers(); - } - - private void add_headers() { - if (((Session) get_parent_session()).is_authenticated()) { - add_header("Authorization", "OAuth %s".printf(((Session) get_parent_session()).get_auth_token())); - add_header("Connection", "close"); - } - } - - public Transaction(Session session, Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.GET) { - base(session, method); - add_headers(); - } - - public void add_data(string type, string data) { - set_custom_payload(data, type); - } -} - -internal class Session : Publishing.RESTSupport.Session { - private string? auth_token = null; - - public Session() { - } - - public override bool is_authenticated() { - return (auth_token != null); - } - - public void deauthenticate() { - auth_token = null; - } - - public void set_auth_token(string token) { - this.auth_token = token; - } - - public string? get_auth_token() { - return auth_token; - } -} - -internal class WebAuthPane : Shotwell.Plugins.Common.WebAuthenticationPane { - private Regex re; - - public signal void login_succeeded(string success_url); - public signal void login_failed(); - - public WebAuthPane(string login_url) { - Object (login_uri : login_url, - preferred_geometry : - Spit.Publishing.DialogPane.GeometryOptions.RESIZABLE); - } - - public override void constructed () { - try { - this.re = new Regex("(.*)#access_token=([a-zA-Z0-9]*)&"); - } catch (RegexError e) { - assert_not_reached (); - } - - this.get_view ().decide_policy.connect (on_decide_policy); - } - - public override void on_page_load () { } - - private bool on_decide_policy (WebKit.PolicyDecision decision, - WebKit.PolicyDecisionType type) { - switch (type) { - case WebKit.PolicyDecisionType.NAVIGATION_ACTION: - WebKit.NavigationPolicyDecision n_decision = (WebKit.NavigationPolicyDecision) decision; - WebKit.NavigationAction action = n_decision.navigation_action; - string uri = action.get_request().uri; - debug("Navigating to '%s'", uri); - - MatchInfo info = null; - - if (re.match(uri, 0, out info)) { - string access_token = info.fetch_all()[2]; - - debug("Load completed: %s", access_token); - this.set_cursor (Gdk.CursorType.LEFT_PTR); - if (access_token != null) { - login_succeeded(access_token); - decision.ignore(); - break; - } else - login_failed(); - } - decision.use(); - break; - case WebKit.PolicyDecisionType.RESPONSE: - decision.use(); - break; - default: - return false; - } - return true; - } -} - -internal class PublishOptions { - public bool disable_comments = false; - public bool hide_original = false; - public string access_type; - - public string destination_album = null; - public string destination_album_url = null; -} - -internal class PublishingOptionsPane: Spit.Publishing.DialogPane, GLib.Object { - private Gtk.Box box; - private Gtk.Builder builder; - private Gtk.Button logout_button; - private Gtk.Button publish_button; - private Gtk.ComboBoxText album_list; - - private weak PublishOptions options; - - public signal void publish(); - public signal void logout(); - - public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { - return Spit.Publishing.DialogPane.GeometryOptions.NONE; - } - public void on_pane_installed() { - } - public void on_pane_uninstalled() { - } - public Gtk.Widget get_widget() { - return box; - } - - public PublishingOptionsPane(PublishOptions options, Gee.HashMap<string, string> list, - Spit.Publishing.PluginHost host) { - this.options = options; - - box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); - - try { - builder = new Gtk.Builder(); - builder.add_from_resource (Resources.RESOURCE_PATH + "/yandex_publish_model.ui"); - - builder.connect_signals(null); - var content = builder.get_object ("content") as Gtk.Widget; - - album_list = builder.get_object ("album_list") as Gtk.ComboBoxText; - foreach (string key in list.keys) - album_list.append_text(key); - - album_list.set_active(0); - - publish_button = builder.get_object("publish_button") as Gtk.Button; - logout_button = builder.get_object("logout_button") as Gtk.Button; - - publish_button.clicked.connect(on_publish_clicked); - logout_button.clicked.connect(on_logout_clicked); - - content.parent.remove (content); - box.pack_start (content, true, true, 0); - } catch (Error e) { - warning("Could not load UI: %s", e.message); - } - } - - private void on_logout_clicked() { - logout(); - } - - private void on_publish_clicked() { - options.destination_album = album_list.get_active_text(); - - Gtk.CheckButton tmp = builder.get_object("hide_original_check") as Gtk.CheckButton; - options.hide_original = tmp.active; - - tmp = builder.get_object("disable_comments_check") as Gtk.CheckButton; - options.disable_comments = tmp.active; - - Gtk.ComboBoxText access_type = builder.get_object("access_type_list") as Gtk.ComboBoxText; - options.access_type = access_type.get_active_text(); - - publish(); - } -} - -private class Uploader: Publishing.RESTSupport.BatchUploader { - private weak PublishOptions options; - - public Uploader(Session session, PublishOptions options, Spit.Publishing.Publishable[] photos) { - base(session, photos); - - this.options = options; - } - - protected override Publishing.RESTSupport.Transaction create_transaction(Spit.Publishing.Publishable publishable) { - debug("create transaction"); - return new UploadTransaction(((Session) get_session()), options, get_current_publishable()); - } -} - -private class UploadTransaction: Transaction { - public UploadTransaction(Session session, PublishOptions options, Spit.Publishing.Publishable photo) { - base.with_url(session, options.destination_album_url, Publishing.RESTSupport.HttpMethod.POST); - - set_custom_payload("qwe", "image/jpeg", 1); - - debug("Uploading '%s' -> %s : %s", photo.get_publishing_name(), options.destination_album, options.destination_album_url); - - Soup.Multipart message_parts = new Soup.Multipart("multipart/form-data"); - message_parts.append_form_string("title", photo.get_publishing_name()); - message_parts.append_form_string("hide_original", options.hide_original.to_string()); - message_parts.append_form_string("disable_comments", options.disable_comments.to_string()); - message_parts.append_form_string("access", options.access_type.down()); - - string photo_data; - size_t data_length; - - try { - FileUtils.get_contents(photo.get_serialized_file().get_path(), out photo_data, out data_length); - } catch (GLib.FileError e) { - critical("Failed to read data file '%s': %s", photo.get_serialized_file().get_path(), e.message); - } - - int image_part_num = message_parts.get_length(); - - Soup.Buffer bindable_data = new Soup.Buffer(Soup.MemoryUse.COPY, photo_data.data[0:data_length]); - message_parts.append_form_file("", photo.get_serialized_file().get_path(), "image/jpeg", bindable_data); - - unowned Soup.MessageHeaders image_part_header; - unowned Soup.Buffer image_part_body; - message_parts.get_part(image_part_num, out image_part_header, out image_part_body); - - GLib.HashTable<string, string> result = new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal); - result.insert("name", "image"); - result.insert("filename", "unused"); - - image_part_header.set_content_disposition("form-data", result); - - Soup.Message outbound_message = Soup.Form.request_new_from_multipart(get_endpoint_url(), message_parts); - outbound_message.request_headers.append("Authorization", ("OAuth %s").printf(session.get_auth_token())); - outbound_message.request_headers.append("Connection", "close"); - set_message(outbound_message); - } -} - -public class YandexPublisher : Spit.Publishing.Publisher, GLib.Object { - private weak Spit.Publishing.PluginHost host = null; - private Spit.Publishing.ProgressCallback progress_reporter = null; - private weak Spit.Publishing.Service service = null; - - private string service_url = null; - - private Gee.HashMap<string, string> album_list = null; - private PublishOptions options; - - private bool running = false; - - private WebAuthPane web_auth_pane = null; - - private Session session; - - public YandexPublisher(Spit.Publishing.Service service, Spit.Publishing.PluginHost host) { - this.service = service; - this.host = host; - this.session = new Session(); - this.album_list = new Gee.HashMap<string, string>(); - this.options = new PublishOptions(); - } - - internal string? get_persistent_auth_token() { - return host.get_config_string("auth_token", null); - } - - internal void set_persistent_auth_token(string auth_token) { - host.set_config_string("auth_token", auth_token); - } - - internal void invalidate_persistent_session() { - host.unset_config_key("auth_token"); - } - - internal bool is_persistent_session_available() { - return (get_persistent_auth_token() != null); - } - - public bool is_running() { - return running; - } - - public Spit.Publishing.Service get_service() { - return service; - } - - private new string? check_response(Publishing.RESTSupport.XmlDocument doc) { - return null; - } - - private void parse_album_entry(Xml.Node *e) throws Spit.Publishing.PublishingError { - string title = null; - string link = null; - - for (Xml.Node* c = e->children ; c != null; c = c->next) { - if (c->name == "title") - title = c->get_content(); - - if ((c->name == "link") && (c->get_prop("rel") == "photos")) - link = c->get_prop("href"); - - if (title != null && link != null) { - debug("Added album: '%s', link: %s", title, link); - album_list.set(title, link); - title = null; - link = null; - break; - } - } - } - - public void parse_album_creation(string data) throws Spit.Publishing.PublishingError { - Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string(data, check_response); - Xml.Node *root = doc.get_root_node(); - - parse_album_entry(root); - } - - public void parse_album_list(string data) throws Spit.Publishing.PublishingError { - Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string(data, check_response); - Xml.Node *root = doc.get_root_node(); - - for (Xml.Node *e = root->children ; e != null; e = e->next) { - if (e->name != "entry") - continue; - - parse_album_entry(e); - } - } - - private void album_creation_error(Publishing.RESTSupport.Transaction t, Spit.Publishing.PublishingError err) { - t.completed.disconnect(album_creation_complete); - t.network_error.disconnect(album_creation_error); - - warning("Album creation error: %s", err.message); - } - - private void album_creation_complete(Publishing.RESTSupport.Transaction t) { - t.completed.disconnect(album_creation_complete); - t.network_error.disconnect(album_creation_error); - - try { - parse_album_creation(t.get_response()); - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - return; - } - - if (album_list.get(options.destination_album) != null) - start_upload(); - else - host.post_error(new Spit.Publishing.PublishingError.PROTOCOL_ERROR("Server did not create album")); - } - - private void create_destination_album() { - string album = options.destination_album; - string data = "<entry xmlns=\"http://www.w3.org/2005/Atom\" xmlns:f=\"yandex:fotki\"><title>%s</title></entry>".printf(album); - - Transaction t = new Transaction.with_url(session, service_url, Publishing.RESTSupport.HttpMethod.POST); - - t.add_data("application/atom+xml; charset=utf-8; type=entry", data); - - t.completed.connect(album_creation_complete); - t.network_error.connect(album_creation_error); - - try { - t.execute(); - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - } - } - - private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, int num_published) { - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - if (num_published == 0) - host.post_error(new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR("")); - - host.set_service_locked(false); - - host.install_success_pane(); - } - - private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, Spit.Publishing.PublishingError err) { - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - warning("Photo upload error: %s", err.message); - } - - private void on_upload_status_updated(int file_number, double completed_fraction) { - debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction); - - assert(progress_reporter != null); - - progress_reporter(file_number, completed_fraction); - } - - private void start_upload() { - host.set_service_locked(true); - - progress_reporter = host.serialize_publishables(0); - - options.destination_album_url = album_list.get(options.destination_album); - Spit.Publishing.Publishable[] publishables = host.get_publishables(); - Uploader uploader = new Uploader(session, options, publishables); - - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); - uploader.upload(on_upload_status_updated); - } - - private void on_logout() { - if (!is_running()) - return; - - session.deauthenticate(); - invalidate_persistent_session(); - - running = false; - - start(); - } - - private void on_publish() { - debug("Going to publish to '%s' : %s", options.destination_album, album_list.get(options.destination_album)); - if (album_list.get(options.destination_album) == null) - create_destination_album(); - else - start_upload(); - } - - public void service_get_album_list_error(Publishing.RESTSupport.Transaction t, Spit.Publishing.PublishingError err) { - t.completed.disconnect(service_get_album_list_complete); - t.network_error.disconnect(service_get_album_list_error); - - invalidate_persistent_session(); - warning("Failed to get album list: %s", err.message); - } - - public void service_get_album_list_complete(Publishing.RESTSupport.Transaction t) { - t.completed.disconnect(service_get_album_list_complete); - t.network_error.disconnect(service_get_album_list_error); - - debug("service_get_album_list_complete: %s", t.get_response()); - try { - parse_album_list(t.get_response()); - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - } - - PublishingOptionsPane publishing_options_pane = new PublishingOptionsPane(options, album_list, - host); - - publishing_options_pane.publish.connect(on_publish); - publishing_options_pane.logout.connect(on_logout); - host.install_dialog_pane(publishing_options_pane); - } - - public void service_get_album_list(string url) { - service_url = url; - - Transaction t = new Transaction.with_url(session, url); - t.completed.connect(service_get_album_list_complete); - t.network_error.connect(service_get_album_list_error); - - try { - t.execute(); - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - } - } - - public void fetch_account_error(Publishing.RESTSupport.Transaction t, Spit.Publishing.PublishingError err) { - t.completed.disconnect(fetch_account_complete); - t.network_error.disconnect(fetch_account_error); - - warning("Failed to fetch account info: %s", err.message); - } - - public void fetch_account_complete(Publishing.RESTSupport.Transaction t) { - t.completed.disconnect(fetch_account_complete); - t.network_error.disconnect(fetch_account_error); - - debug("account info: %s", t.get_response()); - try { - Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string(t.get_response(), check_response); - Xml.Node* root = doc.get_root_node(); - - for (Xml.Node* work = root->children ; work != null; work = work->next) { - if (work->name != "workspace") - continue; - for (Xml.Node* c = work->children ; c != null; c = c->next) { - if (c->name != "collection") - continue; - - if (c->get_prop("id") == "album-list") { - string url = c->get_prop("href"); - - set_persistent_auth_token(session.get_auth_token()); - service_get_album_list(url); - break; - } - } - } - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - } - } - - public void fetch_account_information(string auth_token) { - session.set_auth_token(auth_token); - - Transaction t = new Transaction.with_url(session, "https://api-fotki.yandex.ru/api/me/"); - t.completed.connect(fetch_account_complete); - t.network_error.connect(fetch_account_error); - - try { - t.execute(); - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - } - } - - private void web_auth_login_succeeded(string access_token) { - debug("login succeeded with token %s", access_token); - - host.set_service_locked(true); - host.install_account_fetch_wait_pane(); - - fetch_account_information(access_token); - } - - private void web_auth_login_failed() { - debug("login failed"); - } - - private void start_web_auth() { - host.set_service_locked(false); - - web_auth_pane = new WebAuthPane(("https://oauth.yandex.ru/authorize?client_id=%s&response_type=token").printf(client_id)); - web_auth_pane.login_succeeded.connect(web_auth_login_succeeded); - web_auth_pane.login_failed.connect(web_auth_login_failed); - - host.install_dialog_pane(web_auth_pane, Spit.Publishing.PluginHost.ButtonMode.CANCEL); - } - - private void show_welcome_page() { - host.install_welcome_pane(_("You are not currently logged into Yandex.Fotki."), - start_web_auth); - } - - public void start() { - if (is_running()) - return; - - if (host == null) - error("YandexPublisher: start( ): can't start; this publisher is not restartable."); - - debug("YandexPublisher: starting interaction."); - - running = true; - - if (is_persistent_session_available()) { - session.set_auth_token(get_persistent_auth_token()); - - fetch_account_information(get_persistent_auth_token()); - } else { - show_welcome_page(); - } - } - - public void stop() { - debug("YandexPublisher: stop( ) invoked."); - - host = null; - running = false; - } -} - -} - diff --git a/plugins/shotwell-publishing-extras/gallery3.png b/plugins/shotwell-publishing-extras/gallery3.png Binary files differdeleted file mode 100644 index 9e3c5cc..0000000 --- a/plugins/shotwell-publishing-extras/gallery3.png +++ /dev/null diff --git a/plugins/shotwell-publishing-extras/gallery3_authentication_pane.ui b/plugins/shotwell-publishing-extras/gallery3_authentication_pane.ui deleted file mode 100644 index 3317805..0000000 --- a/plugins/shotwell-publishing-extras/gallery3_authentication_pane.ui +++ /dev/null @@ -1,216 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> -<interface> - <requires lib="gtk+" version="3.14"/> - <object class="GtkGrid" id="gallery3_auth_pane_widget"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <child> - <object class="GtkLabel" id="intro_message_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="ypad">15</property> - <property name="label">Intro message replaced at runtime</property> - <property name="use_markup">True</property> - <property name="wrap">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - <property name="width">5</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="url_entry_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_bottom">30</property> - <property name="label" translatable="yes">_Gallery3 URL:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">url_entry</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="url_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="has_focus">True</property> - <property name="margin_bottom">30</property> - <property name="invisible_char">●</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> - <property name="width">4</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="username_entry_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">_User name:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">username_entry</property> - </object> - <packing> - <property name="left_attach">2</property> - <property name="top_attach">2</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="password_entry_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">_Password:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">password_entry</property> - </object> - <packing> - <property name="left_attach">2</property> - <property name="top_attach">3</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="username_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="invisible_char">●</property> - </object> - <packing> - <property name="left_attach">3</property> - <property name="top_attach">2</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="password_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="visibility">False</property> - <property name="invisible_char">●</property> - <property name="activates_default">True</property> - </object> - <packing> - <property name="left_attach">3</property> - <property name="top_attach">3</property> - </packing> - </child> - <child> - <object class="GtkGrid" id="buttons_grid"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_top">30</property> - <child> - <object class="GtkButton" id="go_back_button"> - <property name="label" translatable="yes">Go _Back</property> - <property name="width_request">102</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="halign">center</property> - <property name="valign">center</property> - <property name="hexpand">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="login_button"> - <property name="label" translatable="yes">_Log in</property> - <property name="width_request">102</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="is_focus">True</property> - <property name="can_default">True</property> - <property name="has_default">True</property> - <property name="receives_default">True</property> - <property name="halign">center</property> - <property name="valign">center</property> - <property name="hexpand">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> - </child> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">6</property> - <property name="width">5</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="key_entry_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">API _Key:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">key_entry</property> - </object> - <packing> - <property name="left_attach">2</property> - <property name="top_attach">5</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="key_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="invisible_char">●</property> - <property name="activates_default">True</property> - <property name="width_chars">33</property> - </object> - <packing> - <property name="left_attach">3</property> - <property name="top_attach">5</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="or_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">or</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">4</property> - <property name="width">5</property> - </packing> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - </object> -</interface> diff --git a/plugins/shotwell-publishing-extras/gallery3_publishing_options_pane.ui b/plugins/shotwell-publishing-extras/gallery3_publishing_options_pane.ui deleted file mode 100644 index f27fd2d..0000000 --- a/plugins/shotwell-publishing-extras/gallery3_publishing_options_pane.ui +++ /dev/null @@ -1,238 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> -<interface domain="shotwell"> - <requires lib="gtk+" version="3.14"/> - <object class="GtkGrid" id="pane_widget"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <child> - <object class="GtkLabel" id="title_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="margin_top">16</property> - <property name="margin_bottom">16</property> - <property name="label">'Publishing to $url as $username' (populated in application code)</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - <property name="width">2</property> - </packing> - </child> - <child> - <object class="GtkGrid" id="options_grid"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_bottom">16</property> - <property name="hexpand">True</property> - <property name="row_spacing">8</property> - <property name="column_spacing">32</property> - <property name="column_homogeneous">True</property> - <child> - <object class="GtkRadioButton" id="publish_to_existing_radio"> - <property name="label" translatable="yes">An _existing album</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="use_underline">True</property> - <property name="draw_indicator">True</property> - <property name="group">publish_new_radio</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkComboBoxText" id="existing_albums_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkRadioButton" id="publish_new_radio"> - <property name="label" translatable="yes">A _new album</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="use_underline">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="new_album_name"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="invisible_char">●</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkCheckButton" id="strip_metadata_check"> - <property name="label" translatable="yes">_Remove location, tag and camera-identifying data before uploading</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="valign">center</property> - <property name="margin_top">16</property> - <property name="hexpand">True</property> - <property name="use_underline">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">5</property> - <property name="width">2</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="major_axis_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="label" translatable="yes">Scaling constraint:</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">3</property> - </packing> - </child> - <child> - <object class="GtkGrid" id="pixels_grid"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="column_spacing">5</property> - <child> - <object class="GtkLabel" id="pixels_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">pixels</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="major_axis_pixels"> - <property name="visible">True</property> - <property name="sensitive">False</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="invisible_char">●</property> - <property name="truncate_multiline">True</property> - <property name="caps_lock_warning">False</property> - <property name="input_purpose">number</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> - </child> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">4</property> - </packing> - </child> - <child> - <object class="GtkComboBoxText" id="scaling_constraint_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <items> - <item translatable="yes">Original size</item> - <item translatable="yes">Longest edge</item> - </items> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">3</property> - </packing> - </child> - <child> - <object class="GtkSeparator" id="album_separator"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">5</property> - <property name="margin_right">5</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">2</property> - <property name="width">2</property> - </packing> - </child> - <child> - <placeholder/> - </child> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - <property name="width">2</property> - </packing> - </child> - <child> - <object class="GtkGrid" id="buttons_grid"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">112</property> - <property name="margin_right">112</property> - <property name="margin_top">48</property> - <property name="margin_bottom">24</property> - <property name="hexpand">True</property> - <property name="column_spacing">128</property> - <property name="column_homogeneous">True</property> - <child> - <object class="GtkButton" id="logout_button"> - <property name="label" translatable="yes">_Logout</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="publish_button"> - <property name="label" translatable="yes">_Publish</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="is_focus">True</property> - <property name="can_default">True</property> - <property name="has_default">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> - </child> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">2</property> - <property name="width">2</property> - </packing> - </child> - </object> -</interface> diff --git a/plugins/shotwell-publishing-extras/meson.build b/plugins/shotwell-publishing-extras/meson.build deleted file mode 100644 index 34f3e12..0000000 --- a/plugins/shotwell-publishing-extras/meson.build +++ /dev/null @@ -1,21 +0,0 @@ -shotwell_publishing_extra_sources = [ - 'GalleryConnector.vala', - 'RajcePublishing.vala', - 'shotwell-publishing-extras.vala', - 'YandexPublishing.vala' - ] - -shotwell_publishing_extra_resources = gnome.compile_resources('publishing-extra-resource', - 'org.gnome.Shotwell.Publishing.Extras.gresource.xml') - -shared_module('shotwell-publishing-extras', - shotwell_publishing_extra_sources + shotwell_publishing_extra_resources, - dependencies : [gee, gtk, xml, soup, gdk_pixbuf, sw_plugin, - sw_plugin_common_dep, json_glib, webkit], - vala_args : [ - '--gresources', 'org.gnome.Shotwell.Publishing.Extras.gresource.xml' - ], - c_args : ['-DPLUGIN_RESOURCE_PATH="/org/gnome/Shotwell/Publishing/Extras"', - '-DGCR_API_SUBJECT_TO_CHANGE'], - install: true, - install_dir : shotwell_plugin_dir) diff --git a/plugins/shotwell-publishing-extras/org.gnome.Shotwell.Publishing.Extras.gresource.xml b/plugins/shotwell-publishing-extras/org.gnome.Shotwell.Publishing.Extras.gresource.xml deleted file mode 100644 index 5916f82..0000000 --- a/plugins/shotwell-publishing-extras/org.gnome.Shotwell.Publishing.Extras.gresource.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<gresources> - <gresource prefix="/org/gnome/Shotwell/Publishing/Extras"> - <file>gallery3.png</file> - <file>rajce.png</file> - <file>gallery3_authentication_pane.ui</file> - <file>gallery3_publishing_options_pane.ui</file> - <file>rajce_authentication_pane.ui</file> - <file>rajce_publishing_options_pane.ui</file> - <file>yandex_publish_model.ui</file> - </gresource> -</gresources> diff --git a/plugins/shotwell-publishing-extras/rajce.png b/plugins/shotwell-publishing-extras/rajce.png Binary files differdeleted file mode 100644 index 8ab0995..0000000 --- a/plugins/shotwell-publishing-extras/rajce.png +++ /dev/null diff --git a/plugins/shotwell-publishing-extras/rajce_authentication_pane.ui b/plugins/shotwell-publishing-extras/rajce_authentication_pane.ui deleted file mode 100644 index 2ef9f3d..0000000 --- a/plugins/shotwell-publishing-extras/rajce_authentication_pane.ui +++ /dev/null @@ -1,142 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> -<interface domain="shotwell"> - <requires lib="gtk+" version="3.14"/> - <object class="GtkWindow" id="authentication_pane"> - <property name="can_focus">False</property> - <child> - <object class="GtkBox" id="content"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">30</property> - <property name="margin_right">30</property> - <property name="orientation">vertical</property> - <property name="spacing">8</property> - <child> - <object class="GtkLabel" id="message_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="hexpand">True</property> - <property name="vexpand">True</property> - <property name="label">label</property> - <property name="wrap">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkGrid" id="field_table"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="row_spacing">2</property> - <property name="column_spacing">8</property> - <child> - <object class="GtkLabel" id="label2"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="label" translatable="yes">_Email address</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">username_entry</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="username_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="invisible_char">●</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="password_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="vexpand">False</property> - <property name="visibility">False</property> - <property name="invisible_char">●</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="label3"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="label" translatable="yes">_Password</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">password_entry</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - </packing> - </child> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="position">1</property> - </packing> - </child> - <child> - <object class="GtkCheckButton" id="remember_checkbutton"> - <property name="label" translatable="yes">_Remember</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="use_underline">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="position">2</property> - </packing> - </child> - <child> - <object class="GtkButtonBox" id="hbuttonbox1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <child> - <object class="GtkButton" id="login_button"> - <property name="label" translatable="yes">Log in</property> - <property name="use_action_appearance">False</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">0</property> - </packing> - </child> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="position">3</property> - </packing> - </child> - </object> - </child> - </object> -</interface> diff --git a/plugins/shotwell-publishing-extras/rajce_publishing_options_pane.ui b/plugins/shotwell-publishing-extras/rajce_publishing_options_pane.ui deleted file mode 100644 index 28011f1..0000000 --- a/plugins/shotwell-publishing-extras/rajce_publishing_options_pane.ui +++ /dev/null @@ -1,246 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> -<interface domain="shotwell"> - <requires lib="gtk+" version="3.14"/> - <object class="GtkBox" id="rajce_pane_widget"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="orientation">vertical</property> - <property name="spacing">1</property> - <child> - <placeholder/> - </child> - <child> - <object class="GtkBox" id="user_area_box"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">center</property> - <property name="margin_left">36</property> - <property name="margin_right">36</property> - <property name="margin_top">24</property> - <property name="margin_bottom">24</property> - <property name="spacing">12</property> - <child> - <object class="GtkLabel" id="login_identity_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="valign">center</property> - <property name="label">you are logged in rajce as $name</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="padding">4</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="logout_button"> - <property name="label" translatable="yes">_Logout</property> - <property name="width_request">64</property> - <property name="height_request">24</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="valign">center</property> - <property name="hexpand">True</property> - <property name="vexpand">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">1</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">1</property> - </packing> - </child> - <child> - <object class="GtkBox" id="album_gallery_layout_box"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">16</property> - <property name="margin_right">16</property> - <property name="orientation">vertical</property> - <child> - <placeholder/> - </child> - <child> - <object class="GtkGrid" id="album_choice_area_grid"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">1</property> - <property name="row_spacing">8</property> - <child> - <object class="GtkComboBoxText" id="existing_albums_combo"> - <property name="width_request">320</property> - <property name="visible">True</property> - <property name="can_focus">False</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="new_album_entry"> - <property name="width_request">320</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="invisible_char">•</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">2</property> - </packing> - </child> - <child> - <object class="GtkRadioButton" id="use_existing_radio"> - <property name="label" translatable="yes">An _existing album:</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="margin_left">4</property> - <property name="margin_right">4</property> - <property name="use_underline">True</property> - <property name="active">True</property> - <property name="draw_indicator">True</property> - <property name="group">create_new_radio</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkRadioButton" id="create_new_radio"> - <property name="label" translatable="yes">A _new album named:</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="margin_left">4</property> - <property name="margin_right">4</property> - <property name="use_underline">True</property> - <property name="active">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">2</property> - </packing> - </child> - <child> - <object class="GtkCheckButton" id="hide_check"> - <property name="label" translatable="yes">_Hide album</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="use_underline">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">3</property> - </packing> - </child> - <child> - <object class="GtkCheckButton" id="show_check"> - <property name="label" translatable="yes">Open target _album in browser</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="use_underline">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">4</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="publish_to_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="margin_top">4</property> - <property name="margin_bottom">8</property> - <property name="label">$mediatype will appear in</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - <property name="width">2</property> - </packing> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="padding">4</property> - <property name="position">1</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">2</property> - </packing> - </child> - <child> - <placeholder/> - </child> - <child> - <object class="GtkBox" id="button_area_box"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">196</property> - <property name="margin_right">196</property> - <property name="margin_top">24</property> - <property name="margin_bottom">24</property> - <property name="spacing">128</property> - <property name="homogeneous">True</property> - <child> - <object class="GtkButton" id="publish_button"> - <property name="label" translatable="yes">_Publish</property> - <property name="width_request">96</property> - <property name="height_request">30</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="halign">center</property> - <property name="valign">center</property> - <property name="hexpand">True</property> - <property name="vexpand">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">4</property> - </packing> - </child> - </object> -</interface> diff --git a/plugins/shotwell-publishing-extras/shotwell-publishing-extras.vala b/plugins/shotwell-publishing-extras/shotwell-publishing-extras.vala deleted file mode 100644 index fb622fa..0000000 --- a/plugins/shotwell-publishing-extras/shotwell-publishing-extras.vala +++ /dev/null @@ -1,51 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -extern const string _VERSION; - -private class ShotwellPublishingExtraServices : Object, Spit.Module { - private Spit.Pluggable[] pluggables = new Spit.Pluggable[0]; - - public ShotwellPublishingExtraServices(GLib.File module_file) { -#if HAVE_YANDEX - pluggables += new YandexService(); -#endif - -#if HAVE_RAJCE - pluggables += new RajceService(module_file.get_parent()); -#endif - -#if HAVE_GALLERY3 - pluggables += new Gallery3Service(module_file.get_parent()); -#endif - } - - public unowned string get_module_name() { - return _("Shotwell Extra Publishing Services"); - } - - public unowned string get_version() { - return _VERSION; - } - - public unowned string get_id() { - return "org.yorba.shotwell.publishing.extras"; - } - - public unowned Spit.Pluggable[]? get_pluggables() { - return pluggables; - } -} - -// This entry point is required for all SPIT modules. -public Spit.Module? spit_entry_point(Spit.EntryPointParams *params) { - params->module_spit_interface = Spit.negotiate_interfaces(params->host_min_spit_interface, - params->host_max_spit_interface, Spit.CURRENT_INTERFACE); - - return (params->module_spit_interface != Spit.UNSUPPORTED_INTERFACE) - ? new ShotwellPublishingExtraServices(params->module_file) : null; -} - diff --git a/plugins/shotwell-publishing-extras/yandex_publish_model.ui b/plugins/shotwell-publishing-extras/yandex_publish_model.ui deleted file mode 100644 index e1cc54d..0000000 --- a/plugins/shotwell-publishing-extras/yandex_publish_model.ui +++ /dev/null @@ -1,182 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> -<interface> - <requires lib="gtk+" version="3.14"/> - <object class="GtkListStore" id="liststore1"> - <columns> - <!-- column-name text --> - <column type="gchararray"/> - </columns> - <data> - <row> - <col id="0" translatable="yes">Public</col> - </row> - <row> - <col id="0" translatable="yes">Friends</col> - </row> - <row> - <col id="0" translatable="yes">Private</col> - </row> - </data> - </object> - <object class="GtkListStore" id="liststore2"> - <columns> - <!-- column-name gchararray1 --> - <column type="gchararray"/> - </columns> - </object> - <object class="GtkWindow" id="publish_options_window"> - <property name="can_focus">False</property> - <child> - <object class="GtkBox" id="content"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">center</property> - <property name="valign">center</property> - <property name="margin_left">30</property> - <property name="margin_right">30</property> - <property name="orientation">vertical</property> - <child> - <object class="GtkGrid" id="table1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <child> - <object class="GtkLabel" id="label2"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">_Albums (or write new):</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">album_list</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkComboBoxText" id="access_type_list"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="active">0</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkComboBoxText" id="album_list"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="active">0</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="label1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="margin_left">6</property> - <property name="margin_right">6</property> - <property name="label" translatable="yes">Access _type:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">access_type_list</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> - </child> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> - </child> - <child> - <placeholder/> - </child> - <child> - <object class="GtkCheckButton" id="disable_comments_check"> - <property name="label" translatable="yes">Disable _comments</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="use_underline">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="padding">2</property> - <property name="position">2</property> - </packing> - </child> - <child> - <object class="GtkCheckButton" id="hide_original_check"> - <property name="label" translatable="yes">_Forbid downloading original photo</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="use_underline">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="position">3</property> - </packing> - </child> - <child> - <object class="GtkButtonBox" id="hbuttonbox1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="spacing">2</property> - <property name="layout_style">spread</property> - <child> - <object class="GtkButton" id="logout_button"> - <property name="label" translatable="yes">_Logout</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="publish_button"> - <property name="label" translatable="yes">_Publish</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">1</property> - </packing> - </child> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="padding">12</property> - <property name="position">4</property> - </packing> - </child> - </object> - </child> - </object> -</interface> diff --git a/plugins/shotwell-publishing/FacebookPublishing.vala b/plugins/shotwell-publishing/FacebookPublishing.vala deleted file mode 100644 index 1633269..0000000 --- a/plugins/shotwell-publishing/FacebookPublishing.vala +++ /dev/null @@ -1,1392 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -public class FacebookService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "facebook.png"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public FacebookService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = Resources.load_from_resource - (Resources.RESOURCE_PATH + "/" + ICON_FILENAME); - } - - public int get_pluggable_interface(int min_host_interface, int max_host_interface) { - return Spit.negotiate_interfaces(min_host_interface, max_host_interface, - Spit.Publishing.CURRENT_INTERFACE); - } - - public unowned string get_id() { - return "org.yorba.shotwell.publishing.facebook"; - } - - public unowned string get_pluggable_name() { - return "Facebook"; - } - - public void get_info(ref Spit.PluggableInfo info) { - info.authors = "Lucas Beeler"; - info.copyright = _("Copyright 2016 Software Freedom Conservancy Inc."); - info.translators = Resources.TRANSLATORS; - info.version = _VERSION; - info.website_name = Resources.WEBSITE_NAME; - info.website_url = Resources.WEBSITE_URL; - info.is_license_wordwrapped = false; - info.license = Resources.LICENSE; - info.icons = icon_pixbuf_set; - } - - public void activation(bool enabled) { - } - - public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { - return new Publishing.Facebook.FacebookPublisher(this, host); - } - - public Spit.Publishing.Publisher.MediaType get_supported_media() { - return (Spit.Publishing.Publisher.MediaType.PHOTO | - Spit.Publishing.Publisher.MediaType.VIDEO); - } -} - -namespace Publishing.Facebook { -// global parameters for the Facebook publishing plugin -- don't touch these (unless you really, -// truly, deep-down know what you're doing) -public const string SERVICE_NAME = "facebook"; -internal const string USER_VISIBLE_NAME = "Facebook"; -internal const string DEFAULT_ALBUM_NAME = _("Shotwell Connect"); -internal const int EXPIRED_SESSION_STATUS_CODE = 400; - -internal class Album { - public string name; - public string id; - - public Album(string name, string id) { - this.name = name; - this.id = id; - } -} - -internal enum Resolution { - STANDARD, - HIGH; - - public string get_name() { - switch (this) { - case STANDARD: - return _("Standard (720 pixels)"); - - case HIGH: - return _("Large (2048 pixels)"); - - default: - error("Unknown resolution %s", this.to_string()); - } - } - - public int get_pixels() { - switch (this) { - case STANDARD: - return 720; - - case HIGH: - return 2048; - - default: - error("Unknown resolution %s", this.to_string()); - } - } -} - -internal class PublishingParameters { - public const int UNKNOWN_ALBUM = -1; - - public bool strip_metadata; - public Album[] albums; - public int target_album; - public string? new_album_name; // the name of the new album being created during this - // publishing interaction or null if publishing to an existing - // album - - public string? privacy_object; // a serialized JSON object encoding the privacy settings of the - // published resources - public Resolution resolution; - - public PublishingParameters() { - this.albums = null; - this.privacy_object = null; - this.target_album = UNKNOWN_ALBUM; - this.new_album_name = null; - this.strip_metadata = false; - this.resolution = Resolution.HIGH; - } - - public void add_album(string name, string id) { - if (albums == null) - albums = new Album[0]; - - Album new_album = new Album(name, id); - albums += new_album; - } - - public void set_target_album_by_name(string? name) { - if (name == null) { - target_album = UNKNOWN_ALBUM; - return; - } - - for (int i = 0; i < albums.length; i++) { - - if (albums[i].name == name) { - target_album = i; - return; - } - } - - target_album = UNKNOWN_ALBUM; - } - - public string? get_target_album_name() { - if (albums == null || target_album == UNKNOWN_ALBUM) - return null; - - return albums[target_album].name; - } - - public string? get_target_album_id() { - if (albums == null || target_album == UNKNOWN_ALBUM) - return null; - - return albums[target_album].id; - } -} - -public class FacebookPublisher : Spit.Publishing.Publisher, GLib.Object { - private PublishingParameters publishing_params; - private weak Spit.Publishing.PluginHost host = null; - private Spit.Publishing.ProgressCallback progress_reporter = null; - private weak Spit.Publishing.Service service = null; - private Spit.Publishing.Authenticator authenticator = null; - private bool running = false; - private GraphSession graph_session; - private PublishingOptionsPane? publishing_options_pane = null; - private Uploader? uploader = null; - private string? uid = null; - private string? username = null; - - public FacebookPublisher(Spit.Publishing.Service service, - Spit.Publishing.PluginHost host) { - debug("FacebookPublisher instantiated."); - - this.service = service; - this.host = host; - - this.publishing_params = new PublishingParameters(); - this.authenticator = - Publishing.Authenticator.Factory.get_instance().create("facebook", - host); - - this.graph_session = new GraphSession(); - graph_session.authenticated.connect(on_session_authenticated); - } - - private bool get_persistent_strip_metadata() { - return host.get_config_bool("strip_metadata", false); - } - - private void set_persistent_strip_metadata(bool strip_metadata) { - host.set_config_bool("strip_metadata", strip_metadata); - } - - // Part of the fix for #3232. These have to be - // public so the legacy options pane may use them. - public int get_persistent_default_size() { - return host.get_config_int("default_size", 0); - } - - public void set_persistent_default_size(int size) { - host.set_config_int("default_size", size); - } - - /* - private void do_test_connection_to_endpoint() { - debug("ACTION: testing connection to Facebook endpoint."); - host.set_service_locked(true); - - host.install_static_message_pane(_("Testing connection to Facebook…")); - - GraphMessage endpoint_test_message = graph_session.new_endpoint_test(); - endpoint_test_message.completed.connect(on_endpoint_test_completed); - endpoint_test_message.failed.connect(on_endpoint_test_error); - - graph_session.send_message(endpoint_test_message); - } - */ - - private void do_fetch_user_info() { - debug("ACTION: fetching user information."); - - host.set_service_locked(true); - host.install_account_fetch_wait_pane(); - - GraphMessage user_info_message = graph_session.new_query("/me"); - - user_info_message.completed.connect(on_fetch_user_info_completed); - user_info_message.failed.connect(on_fetch_user_info_error); - - graph_session.send_message(user_info_message); - } - - private void do_fetch_album_descriptions() { - debug("ACTION: fetching album list."); - - host.set_service_locked(true); - host.install_account_fetch_wait_pane(); - - GraphMessage albums_message = graph_session.new_query("/%s/albums".printf(uid)); - - albums_message.completed.connect(on_fetch_albums_completed); - albums_message.failed.connect(on_fetch_albums_error); - - graph_session.send_message(albums_message); - } - - private void do_extract_user_info_from_json(string json) { - debug("ACTION: extracting user info from JSON response."); - - try { - Json.Parser parser = new Json.Parser(); - parser.load_from_data(json); - - Json.Node root = parser.get_root(); - Json.Object response_object = root.get_object(); - uid = response_object.get_string_member("id"); - username = response_object.get_string_member("name"); - } catch (Error error) { - host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message)); - return; - } - - on_user_info_extracted(); - } - - private void do_extract_albums_from_json(string json) { - debug("ACTION: extracting album info from JSON response."); - - try { - Json.Parser parser = new Json.Parser(); - parser.load_from_data(json); - - Json.Node root = parser.get_root(); - Json.Object response_object = root.get_object(); - Json.Array album_list = response_object.get_array_member("data"); - - publishing_params.albums = new Album[0]; - - for (int i = 0; i < album_list.get_length(); i++) { - Json.Object current_album = album_list.get_object_element(i); - string album_id = current_album.get_string_member("id"); - string album_name = current_album.get_string_member("name"); - - // Note that we are completely ignoring the "can_upload" flag in the list of albums - // that we pulled from facebook eariler -- effectively, we add every album to the - // publishing_params album list regardless of the value of its can_upload flag. In - // the future we may wish to make adding to the publishing_params album list - // conditional on the value of the can_upload flag being true - publishing_params.add_album(album_name, album_id); - } - } catch (Error error) { - host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message)); - return; - } - - on_albums_extracted(); - } - - private void do_create_new_album() { - debug("ACTION: creating a new album named \"%s\".\n", publishing_params.new_album_name); - - host.set_service_locked(true); - host.install_static_message_pane(_("Creating album…")); - - GraphMessage create_album_message = graph_session.new_create_album( - publishing_params.new_album_name, publishing_params.privacy_object); - - create_album_message.completed.connect(on_create_album_completed); - create_album_message.failed.connect(on_create_album_error); - - graph_session.send_message(create_album_message); - } - - private void do_show_publishing_options_pane() { - debug("ACTION: showing publishing options pane."); - - host.set_service_locked(false); - Gtk.Builder builder = new Gtk.Builder(); - - try { - // the trailing get_path() is required, since add_from_file can't cope - // with File objects directly and expects a pathname instead. - builder.add_from_resource (Resources.RESOURCE_PATH + "/" + - "facebook_publishing_options_pane.ui"); - } catch (Error e) { - warning("Could not parse UI file! Error: %s.", e.message); - host.post_error( - new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( - _("A file required for publishing is unavailable. Publishing to Facebook can’t continue."))); - return; - } - - publishing_options_pane = new PublishingOptionsPane(username, publishing_params.albums, - host.get_publishable_media_type(), this, builder, get_persistent_strip_metadata(), - authenticator.can_logout()); - publishing_options_pane.logout.connect(on_publishing_options_pane_logout); - publishing_options_pane.publish.connect(on_publishing_options_pane_publish); - host.install_dialog_pane(publishing_options_pane, - Spit.Publishing.PluginHost.ButtonMode.CANCEL); - } - - private void do_logout() { - debug("ACTION: clearing persistent session information and restaring interaction."); - this.authenticator.logout(); - - running = false; - start(); - } - - private void do_add_new_local_album_from_json(string album_name, string json) { - try { - Json.Parser parser = new Json.Parser(); - parser.load_from_data(json); - - Json.Node root = parser.get_root(); - Json.Object response_object = root.get_object(); - string album_id = response_object.get_string_member("id"); - - publishing_params.add_album(album_name, album_id); - } catch (Error error) { - host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message)); - return; - } - - publishing_params.set_target_album_by_name(album_name); - do_upload(); - } - - - private void on_authenticator_succeeded() { - debug("EVENT: Authenticator login succeeded."); - - do_authenticate_session(); - } - - private void on_authenticator_failed() { - } - - private void do_authenticate_session() { - var parameter = this.authenticator.get_authentication_parameter(); - Variant access_token; - if (!parameter.lookup_extended("AccessToken", null, out access_token)) { - critical("Authenticator signalled success, but does not provide access token"); - assert_not_reached(); - } - graph_session.authenticated.connect(on_session_authenticated); - graph_session.authenticate(access_token.get_string()); - } - - private void do_upload() { - debug("ACTION: uploading photos to album '%s'", - publishing_params.target_album == PublishingParameters.UNKNOWN_ALBUM ? "(none)" : - publishing_params.get_target_album_name()); - - host.set_service_locked(true); - - progress_reporter = host.serialize_publishables(publishing_params.resolution.get_pixels(), - publishing_params.strip_metadata); - - // Serialization is a long and potentially cancellable operation, so before we use - // the publishables, make sure that the publishing interaction is still running. If it - // isn't the publishing environment may be partially torn down so do a short-circuit - // return - if (!is_running()) - return; - - Spit.Publishing.Publishable[] publishables = host.get_publishables(); - uploader = new Uploader(graph_session, publishing_params, publishables); - - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); - - uploader.upload(on_upload_status_updated); - } - - private void do_show_success_pane() { - debug("ACTION: showing success pane."); - - host.set_service_locked(false); - host.install_success_pane(); - } - - private void on_generic_error(Spit.Publishing.PublishingError error) { - if (error is Spit.Publishing.PublishingError.EXPIRED_SESSION) - do_logout(); - else - host.post_error(error); - } - -#if 0 - private void on_endpoint_test_completed(GraphMessage message) { - message.completed.disconnect(on_endpoint_test_completed); - message.failed.disconnect(on_endpoint_test_error); - - if (!is_running()) - return; - - debug("EVENT: endpoint test transaction detected that the Facebook endpoint is alive."); - - do_hosted_web_authentication(); - } - - private void on_endpoint_test_error(GraphMessage message, - Spit.Publishing.PublishingError error) { - message.completed.disconnect(on_endpoint_test_completed); - message.failed.disconnect(on_endpoint_test_error); - - if (!is_running()) - return; - - debug("EVENT: endpoint test transaction failed to detect a connection to the Facebook " + - "endpoint" + error.message); - - on_generic_error(error); - } -#endif - - private void on_session_authenticated() { - graph_session.authenticated.disconnect(on_session_authenticated); - - if (!is_running()) - return; - - assert(graph_session.is_authenticated()); - debug("EVENT: an authenticated session has become available."); - - do_fetch_user_info(); - } - - private void on_fetch_user_info_completed(GraphMessage message) { - message.completed.disconnect(on_fetch_user_info_completed); - message.failed.disconnect(on_fetch_user_info_error); - - if (!is_running()) - return; - - debug("EVENT: user info fetch completed; response = '%s'.", message.get_response_body()); - - do_extract_user_info_from_json(message.get_response_body()); - } - - private void on_fetch_user_info_error(GraphMessage message, - Spit.Publishing.PublishingError error) { - message.completed.disconnect(on_fetch_user_info_completed); - message.failed.disconnect(on_fetch_user_info_error); - - if (!is_running()) - return; - - debug("EVENT: fetching user info generated and error."); - - on_generic_error(error); - } - - private void on_user_info_extracted() { - if (!is_running()) - return; - - debug("EVENT: user info extracted from JSON response: uid = %s; name = %s.", uid, username); - - do_fetch_album_descriptions(); - } - - private void on_fetch_albums_completed(GraphMessage message) { - message.completed.disconnect(on_fetch_albums_completed); - message.failed.disconnect(on_fetch_albums_error); - - if (!is_running()) - return; - - debug("EVENT: album descriptions fetch transaction completed; response = '%s'.", - message.get_response_body()); - - do_extract_albums_from_json(message.get_response_body()); - } - - private void on_fetch_albums_error(GraphMessage message, - Spit.Publishing.PublishingError err) { - message.completed.disconnect(on_fetch_albums_completed); - message.failed.disconnect(on_fetch_albums_error); - - if (!is_running()) - return; - - debug("EVENT: album description fetch attempt generated an error."); - - on_generic_error(err); - } - - private void on_albums_extracted() { - if (!is_running()) - return; - - debug("EVENT: successfully extracted %d albums from JSON response", - publishing_params.albums.length); - - do_show_publishing_options_pane(); - } - - private void on_publishing_options_pane_logout() { - publishing_options_pane.publish.disconnect(on_publishing_options_pane_publish); - publishing_options_pane.logout.disconnect(on_publishing_options_pane_logout); - - if (!is_running()) - return; - - debug("EVENT: user clicked 'Logout' in publishing options pane."); - - do_logout(); - } - - private void on_publishing_options_pane_publish(string? target_album, string privacy_setting, - Resolution resolution, bool strip_metadata) { - publishing_options_pane.publish.disconnect(on_publishing_options_pane_publish); - publishing_options_pane.logout.disconnect(on_publishing_options_pane_logout); - - if (!is_running()) - return; - - debug("EVENT: user clicked 'Publish' in publishing options pane."); - - publishing_params.strip_metadata = strip_metadata; - set_persistent_strip_metadata(strip_metadata); - publishing_params.resolution = resolution; - set_persistent_default_size(resolution); - publishing_params.privacy_object = privacy_setting; - - if (target_album != null) { - // we are publishing at least one photo so we need the name of an album to which - // we'll upload the photo(s) - publishing_params.set_target_album_by_name(target_album); - if (publishing_params.target_album != PublishingParameters.UNKNOWN_ALBUM) { - do_upload(); - } else { - publishing_params.new_album_name = target_album; - do_create_new_album(); - } - } else { - // we're publishing only videos and we don't need an album name - do_upload(); - } - } - - private void on_create_album_completed(GraphMessage message) { - message.completed.disconnect(on_create_album_completed); - message.failed.disconnect(on_create_album_error); - - assert(publishing_params.new_album_name != null); - - if (!is_running()) - return; - - debug("EVENT: created new album resource on remote host; response body = %s.\n", - message.get_response_body()); - - do_add_new_local_album_from_json(publishing_params.new_album_name, - message.get_response_body()); - } - - private void on_create_album_error(GraphMessage message, Spit.Publishing.PublishingError err) { - message.completed.disconnect(on_create_album_completed); - message.failed.disconnect(on_create_album_error); - - if (!is_running()) - return; - - debug("EVENT: attempt to create new album generated an error."); - - on_generic_error(err); - } - - private void on_upload_status_updated(int file_number, double completed_fraction) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction); - - assert(progress_reporter != null); - - progress_reporter(file_number, completed_fraction); - } - - private void on_upload_complete(Uploader uploader, int num_published) { - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - if (!is_running()) - return; - - debug("EVENT: uploader reports upload complete; %d items published.", num_published); - - do_show_success_pane(); - } - - private void on_upload_error(Uploader uploader, Spit.Publishing.PublishingError err) { - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - if (!is_running()) - return; - - debug("EVENT: uploader reports upload error = '%s'.", err.message); - - host.post_error(err); - } - - public Spit.Publishing.Service get_service() { - return service; - } - - public string get_service_name() { - return SERVICE_NAME; - } - - public string get_user_visible_name() { - return USER_VISIBLE_NAME; - } - - public void start() { - if (is_running()) - return; - - debug("FacebookPublisher: starting interaction."); - - running = true; - - // reset all publishing parameters to their default values -- in case this start is - // actually a restart - publishing_params = new PublishingParameters(); - - this.authenticator.authenticated.connect(on_authenticator_succeeded); - this.authenticator.authentication_failed.connect(on_authenticator_failed); - this.authenticator.authenticate(); - } - - public void stop() { - debug("FacebookPublisher: stop( ) invoked."); - - if (graph_session != null) - graph_session.stop_transactions(); - - host = null; - running = false; - } - - public bool is_running() { - return running; - } -} - -internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { - private Gtk.Builder builder; - private Gtk.Box pane_widget = null; - private Gtk.RadioButton use_existing_radio = null; - private Gtk.RadioButton create_new_radio = null; - private Gtk.ComboBoxText existing_albums_combo = null; - private Gtk.ComboBoxText visibility_combo = null; - private Gtk.Entry new_album_entry = null; - private Gtk.CheckButton strip_metadata_check = null; - private Gtk.Button publish_button = null; - private Gtk.Button logout_button = null; - private Gtk.Label how_to_label = null; - private Album[] albums = null; - private FacebookPublisher publisher = null; - private PrivacyDescription[] privacy_descriptions; - - private Resolution[] possible_resolutions; - private Gtk.ComboBoxText resolution_combo = null; - - private Spit.Publishing.Publisher.MediaType media_type; - - private const string HEADER_LABEL_TEXT = _("You are logged into Facebook as %s.\n\n"); - private const string PHOTOS_LABEL_TEXT = _("Where would you like to publish the selected photos?"); - private const string RESOLUTION_LABEL_TEXT = _("Upload _size:"); - private const int CONTENT_GROUP_SPACING = 32; - private const int STANDARD_ACTION_BUTTON_WIDTH = 128; - - public signal void logout(); - public signal void publish(string? target_album, string privacy_setting, - Resolution target_resolution, bool strip_metadata); - - private class PrivacyDescription { - public string description; - public string privacy_setting; - - public PrivacyDescription(string description, string privacy_setting) { - this.description = description; - this.privacy_setting = privacy_setting; - } - } - - public PublishingOptionsPane(string username, Album[] albums, - Spit.Publishing.Publisher.MediaType media_type, FacebookPublisher publisher, - Gtk.Builder builder, bool strip_metadata, bool can_logout) { - - this.builder = builder; - assert(builder != null); - assert(builder.get_objects().length() > 0); - - this.albums = albums; - this.privacy_descriptions = create_privacy_descriptions(); - - this.possible_resolutions = create_resolution_list(); - this.publisher = publisher; - - // we'll need to know if the user is importing video or not when sorting out visibility. - this.media_type = media_type; - - pane_widget = (Gtk.Box) builder.get_object("facebook_pane_box"); - pane_widget.set_border_width(16); - - use_existing_radio = (Gtk.RadioButton) this.builder.get_object("use_existing_radio"); - create_new_radio = (Gtk.RadioButton) this.builder.get_object("create_new_radio"); - existing_albums_combo = (Gtk.ComboBoxText) this.builder.get_object("existing_albums_combo"); - visibility_combo = (Gtk.ComboBoxText) this.builder.get_object("visibility_combo"); - publish_button = (Gtk.Button) this.builder.get_object("publish_button"); - logout_button = (Gtk.Button) this.builder.get_object("logout_button"); - if (!can_logout) { - logout_button.parent.remove (logout_button); - } - new_album_entry = (Gtk.Entry) this.builder.get_object("new_album_entry"); - resolution_combo = (Gtk.ComboBoxText) this.builder.get_object("resolution_combo"); - how_to_label = (Gtk.Label) this.builder.get_object("how_to_label"); - strip_metadata_check = (Gtk.CheckButton) this.builder.get_object("strip_metadata_check"); - - create_new_radio.clicked.connect(on_create_new_toggled); - use_existing_radio.clicked.connect(on_use_existing_toggled); - - string label_text = HEADER_LABEL_TEXT.printf(username); - if ((media_type & Spit.Publishing.Publisher.MediaType.PHOTO) != 0) - label_text += PHOTOS_LABEL_TEXT; - how_to_label.set_label(label_text); - strip_metadata_check.set_active(strip_metadata); - - setup_visibility_combo(); - visibility_combo.set_active(0); - - publish_button.clicked.connect(on_publish_button_clicked); - logout_button.clicked.connect(on_logout_button_clicked); - - setup_resolution_combo(); - resolution_combo.set_active(publisher.get_persistent_default_size()); - resolution_combo.changed.connect(on_size_changed); - - // Ticket #3175, part 2: make sure this widget starts out sensitive - // if it needs to by checking whether we're starting with a video - // or a new gallery. - visibility_combo.set_sensitive( - (create_new_radio != null && create_new_radio.active) || - ((media_type & Spit.Publishing.Publisher.MediaType.VIDEO) != 0)); - - // if publishing only videos, disable all photo-specific controls - if (media_type == Spit.Publishing.Publisher.MediaType.VIDEO) { - strip_metadata_check.set_active(false); - strip_metadata_check.set_sensitive(false); - resolution_combo.set_sensitive(false); - use_existing_radio.set_sensitive(false); - create_new_radio.set_sensitive(false); - existing_albums_combo.set_sensitive(false); - new_album_entry.set_sensitive(false); - } - } - - private bool publishing_photos() { - return (media_type & Spit.Publishing.Publisher.MediaType.PHOTO) != 0; - } - - private void setup_visibility_combo() { - foreach (PrivacyDescription p in privacy_descriptions) - visibility_combo.append_text(p.description); - } - - private void setup_resolution_combo() { - foreach (Resolution res in possible_resolutions) - resolution_combo.append_text(res.get_name()); - } - - private void on_use_existing_toggled() { - if (use_existing_radio.active) { - existing_albums_combo.set_sensitive(true); - new_album_entry.set_sensitive(false); - - // Ticket #3175 - if we're not adding a new gallery - // or a video, then we shouldn't be allowed tof - // choose visibility, since it has no effect. - visibility_combo.set_sensitive((media_type & Spit.Publishing.Publisher.MediaType.VIDEO) != 0); - - existing_albums_combo.grab_focus(); - } - } - - private void on_create_new_toggled() { - if (create_new_radio.active) { - existing_albums_combo.set_sensitive(false); - new_album_entry.set_sensitive(true); - new_album_entry.grab_focus(); - - // Ticket #3175 - if we're creating a new gallery, make sure this is - // active, since it may have possibly been set inactive. - visibility_combo.set_sensitive(true); - } - } - - private void on_size_changed() { - publisher.set_persistent_default_size(resolution_combo.get_active()); - } - - private void on_logout_button_clicked() { - logout(); - } - - private void on_publish_button_clicked() { - string album_name; - string privacy_setting = privacy_descriptions[visibility_combo.get_active()].privacy_setting; - - Resolution resolution_setting; - - if (publishing_photos()) { - resolution_setting = possible_resolutions[resolution_combo.get_active()]; - if (use_existing_radio.active) { - album_name = existing_albums_combo.get_active_text(); - } else { - album_name = new_album_entry.get_text(); - } - } else { - resolution_setting = Resolution.STANDARD; - album_name = null; - } - - publish(album_name, privacy_setting, resolution_setting, strip_metadata_check.get_active()); - } - - private PrivacyDescription[] create_privacy_descriptions() { - PrivacyDescription[] result = new PrivacyDescription[0]; - - result += new PrivacyDescription(_("Just me"), "{ 'value' : 'SELF' }"); - result += new PrivacyDescription(_("Friends"), "{ 'value' : 'ALL_FRIENDS' }"); - result += new PrivacyDescription(_("Everyone"), "{ 'value' : 'EVERYONE' }"); - - return result; - } - - private Resolution[] create_resolution_list() { - Resolution[] result = new Resolution[0]; - - result += Resolution.STANDARD; - result += Resolution.HIGH; - - return result; - } - - public void installed() { - if (publishing_photos()) { - if (albums.length == 0) { - create_new_radio.set_active(true); - new_album_entry.set_text(DEFAULT_ALBUM_NAME); - existing_albums_combo.set_sensitive(false); - use_existing_radio.set_sensitive(false); - } else { - int default_album_seq_num = -1; - int ticker = 0; - foreach (Album album in albums) { - existing_albums_combo.append_text(album.name); - if (album.name == DEFAULT_ALBUM_NAME) - default_album_seq_num = ticker; - ticker++; - } - if (default_album_seq_num != -1) { - existing_albums_combo.set_active(default_album_seq_num); - use_existing_radio.set_active(true); - new_album_entry.set_sensitive(false); - } - else { - create_new_radio.set_active(true); - existing_albums_combo.set_active(0); - existing_albums_combo.set_sensitive(false); - new_album_entry.set_text(DEFAULT_ALBUM_NAME); - } - } - } - - publish_button.grab_focus(); - } - - private void notify_logout() { - logout(); - } - - private void notify_publish(string? target_album, string privacy_setting, Resolution target_resolution) { - publish(target_album, privacy_setting, target_resolution, strip_metadata_check.get_active()); - } - - public Gtk.Widget get_widget() { - return pane_widget; - } - - public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { - return Spit.Publishing.DialogPane.GeometryOptions.NONE; - } - - public void on_pane_installed() { - logout.connect(notify_logout); - publish.connect(notify_publish); - - installed(); - } - - public void on_pane_uninstalled() { - logout.disconnect(notify_logout); - publish.disconnect(notify_publish); - } -} - -internal enum Endpoint { - DEFAULT, - VIDEO, - TEST_CONNECTION; - - public string to_uri() { - switch (this) { - case DEFAULT: - return "https://graph.facebook.com/"; - - case VIDEO: - return "https://graph-video.facebook.com/"; - - case TEST_CONNECTION: - return "https://www.facebook.com/"; - - default: - assert_not_reached(); - } - } -} - -internal abstract class GraphMessage { - public signal void completed(); - public signal void failed(Spit.Publishing.PublishingError err); - public signal void data_transmitted(int bytes_sent_so_far, int total_bytes); - - public abstract string get_uri(); - public abstract string get_response_body(); -} - -internal class GraphSession { - private abstract class GraphMessageImpl : GraphMessage { - public Publishing.RESTSupport.HttpMethod method; - public string uri; - public string access_token; - public Soup.Message soup_message; - public weak GraphSession host_session; - public int bytes_so_far; - - protected GraphMessageImpl(GraphSession host_session, Publishing.RESTSupport.HttpMethod method, - string relative_uri, string access_token, Endpoint endpoint = Endpoint.DEFAULT) { - this.method = method; - this.access_token = access_token; - this.host_session = host_session; - this.bytes_so_far = 0; - - string endpoint_uri = endpoint.to_uri(); - try { - Regex starting_slashes = new Regex("^/+"); - this.uri = endpoint_uri + starting_slashes.replace(relative_uri, -1, 0, ""); - } catch (RegexError err) { - assert_not_reached(); - } - } - - public virtual bool prepare_for_transmission() { - return true; - } - - public override string get_uri() { - return uri; - } - - public override string get_response_body() { - return (string) soup_message.response_body.data; - } - - public void on_wrote_body_data(Soup.Buffer chunk) { - bytes_so_far += (int) chunk.length; - - data_transmitted(bytes_so_far, (int) soup_message.request_body.length); - } - } - - private class GraphQueryMessage : GraphMessageImpl { - public GraphQueryMessage(GraphSession host_session, string relative_uri, - string access_token) { - base(host_session, Publishing.RESTSupport.HttpMethod.GET, relative_uri, access_token); - - Soup.URI destination_uri = new Soup.URI(uri + "?access_token=" + access_token); - soup_message = new Soup.Message.from_uri(method.to_string(), destination_uri); - soup_message.wrote_body_data.connect(on_wrote_body_data); - } - } - - private class GraphEndpointProbeMessage : GraphMessageImpl { - public GraphEndpointProbeMessage(GraphSession host_session) { - base(host_session, Publishing.RESTSupport.HttpMethod.GET, "/", "", - Endpoint.TEST_CONNECTION); - - soup_message = new Soup.Message.from_uri(method.to_string(), new Soup.URI(uri)); - soup_message.wrote_body_data.connect(on_wrote_body_data); - } - } - - private class GraphUploadMessage : GraphMessageImpl { - private MappedFile mapped_file = null; - private Spit.Publishing.Publishable publishable; - - public GraphUploadMessage(GraphSession host_session, string access_token, - string relative_uri, Spit.Publishing.Publishable publishable, - bool suppress_titling, string? resource_privacy = null) { - base(host_session, Publishing.RESTSupport.HttpMethod.POST, relative_uri, access_token, - (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) ? - Endpoint.VIDEO : Endpoint.DEFAULT); - - // Video uploads require a privacy string at the per-resource level. Since they aren't - // placed in albums, they can't inherit their privacy settings from their containing - // album like photos do - assert(publishable.get_media_type() != Spit.Publishing.Publisher.MediaType.VIDEO || - resource_privacy != null); - - this.publishable = publishable; - - // attempt to map the binary payload from disk into memory - try { - this.mapped_file = new MappedFile(publishable.get_serialized_file().get_path(), - false); - } catch (FileError e) { - return; - } - - this.soup_message = new Soup.Message.from_uri(method.to_string(), new Soup.URI(uri)); - soup_message.wrote_body_data.connect(on_wrote_body_data); - - unowned uint8[] payload = (uint8[]) mapped_file.get_contents(); - payload.length = (int) mapped_file.get_length(); - - Soup.Buffer image_data = new Soup.Buffer(Soup.MemoryUse.TEMPORARY, payload); - - Soup.Multipart mp_envelope = new Soup.Multipart("multipart/form-data"); - - mp_envelope.append_form_string("access_token", access_token); - - if (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) - mp_envelope.append_form_string("privacy", resource_privacy); - - //Get photo title and post it as message on FB API - string publishable_title = publishable.get_param_string("title"); - if (!suppress_titling && publishable_title != null) - mp_envelope.append_form_string("name", publishable_title); - - //Set 'message' data field with EXIF comment field. Title has precedence. - string publishable_comment = publishable.get_param_string("comment"); - if (!suppress_titling && publishable_comment != null) - mp_envelope.append_form_string("message", publishable_comment); - - //Sets correct date of the picture - if (!suppress_titling) - mp_envelope.append_form_string("backdated_time", publishable.get_exposure_date_time().to_string()); - - string source_file_mime_type = - (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) ? - "video" : "image/jpeg"; - mp_envelope.append_form_file("source", publishable.get_serialized_file().get_basename(), - source_file_mime_type, image_data); - - mp_envelope.to_message(soup_message.request_headers, soup_message.request_body); - } - - public override bool prepare_for_transmission() { - if (mapped_file == null) { - failed(new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( - "File %s is unavailable.".printf(publishable.get_serialized_file().get_path()))); - return false; - } else { - return true; - } - } - } - - private class GraphCreateAlbumMessage : GraphMessageImpl { - public GraphCreateAlbumMessage(GraphSession host_session, string access_token, - string album_name, string album_privacy) { - base(host_session, Publishing.RESTSupport.HttpMethod.POST, "/me/albums", access_token); - - assert(album_privacy != null && album_privacy != ""); - - this.soup_message = new Soup.Message.from_uri(method.to_string(), new Soup.URI(uri)); - - Soup.Multipart mp_envelope = new Soup.Multipart("multipart/form-data"); - - mp_envelope.append_form_string("access_token", access_token); - mp_envelope.append_form_string("name", album_name); - mp_envelope.append_form_string("privacy", album_privacy); - - mp_envelope.to_message(soup_message.request_headers, soup_message.request_body); - } - } - - public signal void authenticated(); - - private Soup.Session soup_session; - private string? access_token; - private GraphMessage? current_message; - - public GraphSession() { - this.soup_session = new Soup.Session (); - this.soup_session.request_unqueued.connect (on_request_unqueued); - this.soup_session.timeout = 15; - this.access_token = null; - this.current_message = null; - this.soup_session.ssl_use_system_ca_file = true; - } - - ~GraphSession() { - soup_session.request_unqueued.disconnect (on_request_unqueued); - } - - private void manage_message(GraphMessage msg) { - assert(current_message == null); - - current_message = msg; - } - - private void unmanage_message(GraphMessage msg) { - assert(current_message != null); - - current_message = null; - } - - private void on_request_unqueued(Soup.Message msg) { - assert(current_message != null); - GraphMessageImpl real_message = (GraphMessageImpl) current_message; - assert(real_message.soup_message == msg); - - // these error types are always recoverable given the unique behavior of the Facebook - // endpoint, so try again - if (msg.status_code == Soup.KnownStatusCode.IO_ERROR || - msg.status_code == Soup.KnownStatusCode.MALFORMED || - msg.status_code == Soup.KnownStatusCode.TRY_AGAIN) { - real_message.bytes_so_far = 0; - soup_session.queue_message(msg, null); - return; - } - - unmanage_message(real_message); - msg.wrote_body_data.disconnect(real_message.on_wrote_body_data); - - Spit.Publishing.PublishingError? error = null; - switch (msg.status_code) { - case Soup.KnownStatusCode.OK: - case Soup.KnownStatusCode.CREATED: // HTTP code 201 (CREATED) signals that a new - // resource was created in response to a PUT - // or POST - break; - - case EXPIRED_SESSION_STATUS_CODE: - error = new Spit.Publishing.PublishingError.EXPIRED_SESSION( - "OAuth Access Token has Expired. Logout user."); - break; - - case Soup.KnownStatusCode.CANT_RESOLVE: - case Soup.KnownStatusCode.CANT_RESOLVE_PROXY: - error = new Spit.Publishing.PublishingError.NO_ANSWER( - "Unable to resolve %s (error code %u)", real_message.get_uri(), msg.status_code); - break; - - case Soup.KnownStatusCode.CANT_CONNECT: - case Soup.KnownStatusCode.CANT_CONNECT_PROXY: - error = new Spit.Publishing.PublishingError.NO_ANSWER( - "Unable to connect to %s (error code %u)", real_message.get_uri(), msg.status_code); - break; - - default: - // status codes below 100 are used by Soup, 100 and above are defined HTTP - // codes - if (msg.status_code >= 100) { - error = new Spit.Publishing.PublishingError.NO_ANSWER( - "Service %s returned HTTP status code %u %s", real_message.get_uri(), - msg.status_code, msg.reason_phrase); - } else { - debug(msg.reason_phrase); - error = new Spit.Publishing.PublishingError.NO_ANSWER( - "Failure communicating with %s (error code %u)", real_message.get_uri(), - msg.status_code); - } - break; - } - - // All valid communication with Facebook involves body data in the response - if (error == null) - if (msg.response_body.data == null || msg.response_body.data.length == 0) - error = new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( - "No response data from %s", real_message.get_uri()); - - if (error == null) - real_message.completed(); - else - real_message.failed(error); - } - - public void authenticate(string access_token) { - this.access_token = access_token; - authenticated(); - } - - public bool is_authenticated() { - return access_token != null; - } - -#if 0 - public GraphMessage new_endpoint_test() { - return new GraphEndpointProbeMessage(this); - } -#endif - - public GraphMessage new_query(string resource_path) { - return new GraphQueryMessage(this, resource_path, access_token); - } - - public GraphMessage new_upload(string resource_path, Spit.Publishing.Publishable publishable, - bool suppress_titling, string? resource_privacy = null) { - return new GraphUploadMessage(this, access_token, resource_path, publishable, - suppress_titling, resource_privacy); - } - - public GraphMessage new_create_album(string album_name, string privacy) { - return new GraphSession.GraphCreateAlbumMessage(this, access_token, album_name, privacy); - } - - public void send_message(GraphMessage message) { - GraphMessageImpl real_message = (GraphMessageImpl) message; - - debug("making HTTP request to URI: " + real_message.soup_message.uri.to_string(false)); - - if (real_message.prepare_for_transmission()) { - manage_message(message); - soup_session.queue_message(real_message.soup_message, null); - } - } - - public void stop_transactions() { - soup_session.abort(); - } -} - -internal class Uploader { - private int current_file; - private Spit.Publishing.Publishable[] publishables; - private GraphSession session; - private PublishingParameters publishing_params; - private unowned Spit.Publishing.ProgressCallback? status_updated = null; - - public signal void upload_complete(int num_photos_published); - public signal void upload_error(Spit.Publishing.PublishingError err); - - public Uploader(GraphSession session, PublishingParameters publishing_params, - Spit.Publishing.Publishable[] publishables) { - this.current_file = 0; - this.publishables = publishables; - this.session = session; - this.publishing_params = publishing_params; - } - - private void send_current_file() { - Spit.Publishing.Publishable publishable = publishables[current_file]; - GLib.File? file = publishable.get_serialized_file(); - - // if the current publishable hasn't been serialized, then skip it - if (file == null) { - current_file++; - return; - } - - string resource_uri = - (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.PHOTO) ? - "/%s/photos".printf(publishing_params.get_target_album_id()) : "/me/videos"; - string? resource_privacy = - (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) ? - publishing_params.privacy_object : null; - GraphMessage upload_message = session.new_upload(resource_uri, publishable, - publishing_params.strip_metadata, resource_privacy); - - upload_message.data_transmitted.connect(on_chunk_transmitted); - upload_message.completed.connect(on_message_completed); - upload_message.failed.connect(on_message_failed); - - session.send_message(upload_message); - } - - private void send_files() { - current_file = 0; - send_current_file(); - } - - private void on_chunk_transmitted(int bytes_written_so_far, int total_bytes) { - double file_span = 1.0 / publishables.length; - double this_file_fraction_complete = ((double) bytes_written_so_far) / total_bytes; - double fraction_complete = (current_file * file_span) + (this_file_fraction_complete * - file_span); - - if (status_updated != null) - status_updated(current_file + 1, fraction_complete); - } - - private void on_message_completed(GraphMessage message) { - message.data_transmitted.disconnect(on_chunk_transmitted); - message.completed.disconnect(on_message_completed); - message.failed.disconnect(on_message_failed); - - current_file++; - if (current_file < publishables.length) { - send_current_file(); - } else { - upload_complete(current_file); - } - } - - private void on_message_failed(GraphMessage message, Spit.Publishing.PublishingError error) { - message.data_transmitted.disconnect(on_chunk_transmitted); - message.completed.disconnect(on_message_completed); - message.failed.disconnect(on_message_failed); - - upload_error(error); - } - - public void upload(Spit.Publishing.ProgressCallback? status_updated = null) { - this.status_updated = status_updated; - - if (publishables.length > 0) - send_files(); - } -} - -} - diff --git a/plugins/shotwell-publishing/FlickrPublishing.vala b/plugins/shotwell-publishing/FlickrPublishing.vala index 5a80284..8c7e9a1 100644 --- a/plugins/shotwell-publishing/FlickrPublishing.vala +++ b/plugins/shotwell-publishing/FlickrPublishing.vala @@ -5,15 +5,8 @@ */ public class FlickrService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "flickr.png"; - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public FlickrService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = Resources.load_from_resource - (Resources.RESOURCE_PATH + "/" + ICON_FILENAME); - } + public FlickrService() {} public int get_pluggable_interface(int min_host_interface, int max_host_interface) { return Spit.negotiate_interfaces(min_host_interface, max_host_interface, @@ -21,23 +14,19 @@ public class FlickrService : Object, Spit.Pluggable, Spit.Publishing.Service { } public unowned string get_id() { - return "org.yorba.shotwell.publishing.flickr"; + return "org.gnome.shotwell.publishing.flickr"; } public unowned string get_pluggable_name() { return "Flickr"; } - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); info.authors = "Lucas Beeler"; info.copyright = _("Copyright 2016 Software Freedom Conservancy Inc."); - info.translators = Resources.TRANSLATORS; - info.version = _VERSION; - info.website_name = Resources.WEBSITE_NAME; - info.website_url = Resources.WEBSITE_URL; - info.is_license_wordwrapped = false; - info.license = Resources.LICENSE; - info.icons = icon_pixbuf_set; + + return info; } public void activation(bool enabled) { @@ -80,10 +69,12 @@ internal class VisibilitySpecification { // not a struct because we want reference semantics internal class PublishingParameters { public UserKind user_kind; - public int64 quota_free_bytes; + public int64 max_images_count; + public uint64 uploaded_images_count; public int photo_major_axis_size; public string username; public VisibilitySpecification visibility_specification; + public bool strip_metadata; public PublishingParameters() { } @@ -154,30 +145,7 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { parameters.username = session.get_username(); - do_fetch_account_info(); - } - - private void on_account_fetch_txn_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_account_fetch_txn_completed); - txn.network_error.disconnect(on_account_fetch_txn_error); - - if (!is_running()) - return; - - debug("EVENT: account fetch transaction response received over the network"); - do_parse_account_info_from_xml(txn.get_response()); - } - - private void on_account_fetch_txn_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_account_fetch_txn_completed); - txn.network_error.disconnect(on_account_fetch_txn_error); - - if (!is_running()) - return; - - debug("EVENT: account fetch transaction caused a network error"); - host.post_error(err); + do_fetch_account_info.begin(); } private void on_account_info_available() { @@ -196,7 +164,7 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { return; debug("EVENT: user clicked the 'Publish' button in the publishing options pane"); - do_publish(strip_metadata); + do_publish.begin(strip_metadata); } private void on_publishing_options_pane_logout() { @@ -222,45 +190,19 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { progress_reporter(file_number, completed_fraction); } - private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, - int num_published) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload complete; %d items published.", num_published); - - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - do_show_success_pane(); - } - - private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, - Spit.Publishing.PublishingError err) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload error = '%s'.", err.message); - - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - host.post_error(err); - } - - private void do_fetch_account_info() { + private async void do_fetch_account_info() { debug("ACTION: running network transaction to fetch account information"); host.set_service_locked(true); host.install_account_fetch_wait_pane(); AccountInfoFetchTransaction txn = new AccountInfoFetchTransaction(session); - txn.completed.connect(on_account_fetch_txn_completed); - txn.network_error.connect(on_account_fetch_txn_error); - try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield txn.execute_async(); + debug("EVENT: account fetch transaction response received over the network"); + do_parse_account_info_from_xml(txn.get_response()); + } catch (Error err) { + debug("EVENT: account fetch transaction caused a network error"); host.post_error(err); } } @@ -275,9 +217,8 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { string is_pro_str = response_doc.get_property_value(user_node, "ispro"); - Xml.Node* bandwidth_node = response_doc.get_named_child(user_node, "bandwidth"); - - string remaining_kb_str = response_doc.get_property_value(bandwidth_node, "remainingkb"); + string max_images_str = response_doc.get_property_value(user_node, "upload_limit"); + string uploaded_images_str = response_doc.get_property_value(user_node, "upload_count"); UserKind user_kind; if (is_pro_str == "0") @@ -287,10 +228,9 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { else throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( "Unable to determine if user has free or pro account"); - - var quota_bytes_left = int64.parse(remaining_kb_str) * 1024; - parameters.quota_free_bytes = quota_bytes_left; + parameters.max_images_count = int64.parse(max_images_str); + parameters.uploaded_images_count = int64.parse(uploaded_images_str); parameters.user_kind = user_kind; } catch (Spit.Publishing.PublishingError err) { @@ -353,7 +293,7 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { return a.get_exposure_date_time().compare(b.get_exposure_date_time()); } - private void do_publish(bool strip_metadata) { + private async void do_publish(bool strip_metadata) { set_persistent_strip_metadata(strip_metadata); debug("ACTION: uploading media items to remote server."); @@ -377,9 +317,14 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { sorted_list.sort(flickr_date_time_compare_func); Uploader uploader = new Uploader(session, sorted_list.to_array(), parameters, strip_metadata); - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); - uploader.upload(on_upload_status_updated); + try { + var num_published = yield uploader.upload_async(on_upload_status_updated); + debug("EVENT: uploader reports upload complete; %d items published.", num_published); + do_show_success_pane(); + } catch (Error err) { + debug("EVENT: uploader reports upload error = '%s'.", err.message); + host.post_error(err); + } } private void do_show_success_pane() { @@ -506,7 +451,7 @@ private class UploadTransaction : Publishing.RESTSupport.OAuth1.UploadTransactio public UploadTransaction(Publishing.RESTSupport.OAuth1.Session session, PublishingParameters parameters, Spit.Publishing.Publishable publishable) { - base(session, publishable, "https://api.flickr.com/services/upload"); + base(session, publishable, "https://up.flickr.com/services/upload"); this.parameters = parameters; @@ -514,6 +459,18 @@ private class UploadTransaction : Publishing.RESTSupport.OAuth1.UploadTransactio add_argument("is_friend", ("%d".printf(parameters.visibility_specification.friends_level))); add_argument("is_family", ("%d".printf(parameters.visibility_specification.family_level))); + if (!parameters.strip_metadata) { + var title = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_TITLE); + if (title != null && title != "") { + add_argument("title", title); + } + + var comment = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_COMMENT); + if (comment != null && comment != "") { + add_argument("description", comment); + } + } + GLib.HashTable<string, string> disposition_table = new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal); string? filename = publishable.get_publishing_name(); @@ -522,7 +479,7 @@ private class UploadTransaction : Publishing.RESTSupport.OAuth1.UploadTransactio /// TODO: This may need to be revisited to send the title separately; please see /// http://www.flickr.com/services/api/upload.api.html for more details. - disposition_table.insert("filename", Soup.URI.encode( + disposition_table.insert("filename", GLib.Uri.escape_string( publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME), null)); disposition_table.insert("name", "photo"); @@ -530,9 +487,9 @@ private class UploadTransaction : Publishing.RESTSupport.OAuth1.UploadTransactio set_binary_disposition_table(disposition_table); } - public override void execute() throws Spit.Publishing.PublishingError { + public override async void execute_async() throws Spit.Publishing.PublishingError { this.authorize(); - base.execute(); + yield base.execute_async(); } } @@ -606,19 +563,19 @@ internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { string upload_label_text = _("You are logged into Flickr as %s.\n\n").printf(parameters.username); if (parameters.user_kind == UserKind.FREE) { - upload_label_text += _("Your free Flickr account limits how much data you can upload per month.\nThis month you have %s remaining in your upload quota.").printf(GLib.format_size(parameters.quota_free_bytes, FormatSizeFlags.LONG_FORMAT | FormatSizeFlags.IEC_UNITS)); + upload_label_text += _("Your free Flickr account limits how many photos you can upload to the service.\nYou have uploaded %llu out of your %lld file limit.").printf(parameters.uploaded_images_count, parameters.max_images_count); } else { - upload_label_text += _("Your Flickr Pro account entitles you to unlimited uploads."); + upload_label_text += ngettext("Your Flickr Pro account entitles you to unlimited uploads. You have currently uploaded a file", "Your Flickr Pro account entitles you to unlimited uploads. You have currently uploaded %d files", (int) parameters.uploaded_images_count).printf((int) parameters.uploaded_images_count); } upload_info_label.set_label(upload_label_text); - string visibility_label_text = _("Photos _visible to:"); + string visibility_label_text = _("Photos _visible to"); if ((media_type == Spit.Publishing.Publisher.MediaType.VIDEO)) { - visibility_label_text = _("Videos _visible to:"); + visibility_label_text = _("Videos _visible to"); } else if ((media_type == (Spit.Publishing.Publisher.MediaType.PHOTO | Spit.Publishing.Publisher.MediaType.VIDEO))) { - visibility_label_text = _("Photos and videos _visible to:"); + visibility_label_text = _("Photos and videos _visible to"); } visibility_label.set_label(visibility_label_text); @@ -646,6 +603,7 @@ internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { } private void on_publish_clicked() { + parameters.strip_metadata = strip_metadata_check.get_active(); parameters.visibility_specification = visibilities[visibility_combo.get_active()].specification; @@ -765,52 +723,62 @@ internal class Uploader : Publishing.RESTSupport.BatchUploader { if (!publishable_metadata.has_iptc()) return; - if (publishable_metadata.has_tag("Iptc.Application2.Caption")) - publishable_metadata.set_tag_string("Iptc.Application2.Caption", - Publishing.RESTSupport.asciify_string(publishable_metadata.get_tag_string( - "Iptc.Application2.Caption"))); - - if (publishable_metadata.has_tag("Iptc.Application2.Headline")) - publishable_metadata.set_tag_string("Iptc.Application2.Headline", - Publishing.RESTSupport.asciify_string(publishable_metadata.get_tag_string( - "Iptc.Application2.Headline"))); - - if (publishable_metadata.has_tag("Iptc.Application2.Keywords")) { - Gee.Set<string> keyword_set = new Gee.HashSet<string>(); - string[] iptc_keywords = publishable_metadata.get_tag_multiple("Iptc.Application2.Keywords"); - if (iptc_keywords != null) - foreach (string keyword in iptc_keywords) - keyword_set.add(keyword); - - string[] xmp_keywords = publishable_metadata.get_tag_multiple("Xmp.dc.subject"); - if (xmp_keywords != null) - foreach (string keyword in xmp_keywords) - keyword_set.add(keyword); - - string[] all_keywords = keyword_set.to_array(); - // append a null pointer to the end of all_keywords -- this is a necessary workaround - // for http://trac.yorba.org/ticket/3264. See also http://trac.yorba.org/ticket/3257, - // which describes the user-visible behavior seen in the Flickr Connector as a result - // of the former bug. - all_keywords += null; - - string[] no_keywords = new string[1]; - // append a null pointer to the end of no_keywords -- this is a necessary workaround - // for http://trac.yorba.org/ticket/3264. See also http://trac.yorba.org/ticket/3257, - // which describes the user-visible behavior seen in the Flickr Connector as a result - // of the former bug. - no_keywords[0] = null; - - publishable_metadata.set_tag_multiple("Xmp.dc.subject", all_keywords); - publishable_metadata.set_tag_multiple("Iptc.Application2.Keywords", no_keywords); - - try { - publishable_metadata.save_file(publishable.get_serialized_file().get_path()); - } catch (GLib.Error err) { - warning("couldn't write metadata to file '%s' for upload preprocessing.", - publishable.get_serialized_file().get_path()); + try { + if (publishable_metadata.try_has_tag("Iptc.Application2.Caption")) + publishable_metadata.try_set_tag_string("Iptc.Application2.Caption", + Publishing.RESTSupport.asciify_string(publishable_metadata.try_get_tag_string( + "Iptc.Application2.Caption"))); + } catch (Error err) {} + + try { + if (publishable_metadata.try_has_tag("Iptc.Application2.Headline")) + publishable_metadata.try_set_tag_string("Iptc.Application2.Headline", + Publishing.RESTSupport.asciify_string(publishable_metadata.try_get_tag_string( + "Iptc.Application2.Headline"))); + } catch (Error error) {} + + try { + if (publishable_metadata.try_has_tag("Iptc.Application2.Keywords")) { + Gee.Set<string> keyword_set = new Gee.HashSet<string>(); + string[] iptc_keywords = publishable_metadata.try_get_tag_multiple("Iptc.Application2.Keywords"); + if (iptc_keywords != null) + foreach (string keyword in iptc_keywords) + keyword_set.add(keyword); + + string[] xmp_keywords = publishable_metadata.try_get_tag_multiple("Xmp.dc.subject"); + if (xmp_keywords != null) + foreach (string keyword in xmp_keywords) + keyword_set.add(keyword); + + string[] all_keywords = keyword_set.to_array(); + // append a null pointer to the end of all_keywords -- this is a necessary workaround + // https://bugzilla.gnome.org/show_bug.cgi?id=712479. See also + // https://bugzilla.gnome.org/show_bug.cgi?id=717438 which describes the user-visible + // behavior seen in the Flickr Connector as a result of the former bug. + all_keywords += null; + + string[] no_keywords = new string[1]; + // append a null pointer to the end of no_keywords -- this is a necessary workaround + // for similar reasons as above. + no_keywords[0] = null; + + try { + publishable_metadata.try_set_tag_multiple("Xmp.dc.subject", all_keywords); + } catch (Error error) { + } + try { + publishable_metadata.try_set_tag_multiple("Iptc.Application2.Keywords", no_keywords); + } catch (Error error) { + } + + try { + publishable_metadata.save_file(publishable.get_serialized_file().get_path()); + } catch (GLib.Error err) { + warning("couldn't write metadata to file '%s' for upload preprocessing.", + publishable.get_serialized_file().get_path()); + } } - } + } catch (Error error) {} } protected override Publishing.RESTSupport.Transaction create_transaction( diff --git a/plugins/shotwell-publishing/PhotosPublisher.vala b/plugins/shotwell-publishing/PhotosPublisher.vala index 879f5fc..b592317 100644 --- a/plugins/shotwell-publishing/PhotosPublisher.vala +++ b/plugins/shotwell-publishing/PhotosPublisher.vala @@ -127,7 +127,7 @@ private class MediaCreationTransaction : Publishing.RESTSupport.GooglePublisher. this.titles = titles; } - public override void execute () throws Spit.Publishing.PublishingError { + public override async void execute_async () throws Spit.Publishing.PublishingError { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("albumId"); @@ -149,7 +149,7 @@ private class MediaCreationTransaction : Publishing.RESTSupport.GooglePublisher. builder.end_object(); set_custom_payload(Json.to_string (builder.get_root (), false), "application/json"); - base.execute(); + yield base.execute_async(); } } @@ -163,7 +163,7 @@ private class AlbumCreationTransaction : Publishing.RESTSupport.GooglePublisher. this.title = title; } - public override void execute () throws Spit.Publishing.PublishingError { + public override async void execute_async() throws Spit.Publishing.PublishingError { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("album"); @@ -174,58 +174,18 @@ private class AlbumCreationTransaction : Publishing.RESTSupport.GooglePublisher. builder.end_object(); set_custom_payload(Json.to_string (builder.get_root (), false), "application/json"); - base.execute(); + yield base.execute_async(); } } private class AlbumDirectoryTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/albums"; - private Album[] albums = new Album[0]; - public AlbumDirectoryTransaction(Publishing.RESTSupport.GoogleSession session) { + public AlbumDirectoryTransaction(Publishing.RESTSupport.GoogleSession session, string? token) { base(session, ENDPOINT_URL, Publishing.RESTSupport.HttpMethod.GET); - this.completed.connect(on_internal_continue_pagination); - } - - public Album[] get_albums() { - return this.albums; - } - - private void on_internal_continue_pagination() { - try { - debug(this.get_response()); - var json = Json.from_string (this.get_response()); - var object = json.get_object (); - if (!object.has_member ("albums")) { - return; - } - var pagination_token_node = object.get_member ("nextPageToken"); - var response_albums = object.get_member ("albums").get_array(); - response_albums.foreach_element( (a, b, element) => { - var album = element.get_object(); - var title = album.get_member("title"); - var is_writable = album.get_member("isWriteable"); - if (title != null && is_writable != null && is_writable.get_boolean()) - albums += new Album(title.get_string(), album.get_string_member("id")); - }); - - if (pagination_token_node != null) { - this.set_argument ("pageToken", pagination_token_node.get_string ()); - Signal.stop_emission_by_name (this, "completed"); - Idle.add(() => { - try { - this.execute(); - } catch (Spit.Publishing.PublishingError error) { - this.network_error(error); - } - - return false; - }); - } - } catch (Error error) { - critical ("Got error %s while trying to parse response, delegating", error.message); - this.network_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message)); + if (token != null) { + add_argument("pageToken", token); } } } @@ -264,61 +224,70 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { } protected override void on_login_flow_complete() { + do_publishing_process.begin(); + } + + private async void do_publishing_process() { debug("EVENT: OAuth login flow complete."); this.publishing_parameters.set_user_name (this.authenticator.get_authentication_parameter()["UserName"].get_string()); get_host().install_account_fetch_wait_pane(); get_host().set_service_locked(true); + var albums = new Album[0]; - AlbumDirectoryTransaction txn = new AlbumDirectoryTransaction(get_session()); - txn.completed.connect(on_initial_album_fetch_complete); - txn.network_error.connect(on_initial_album_fetch_error); - + AlbumDirectoryTransaction txn = new AlbumDirectoryTransaction(get_session(), null); try { - txn.execute(); - } catch (Spit.Publishing.PublishingError error) { - on_initial_album_fetch_error(txn, error); - } - } - - private void on_initial_album_fetch_complete(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_initial_album_fetch_complete); - txn.network_error.disconnect(on_initial_album_fetch_error); - - if (!is_running()) - return; - - debug("EVENT: finished fetching album information."); - - display_account_information((AlbumDirectoryTransaction)txn); - } - - private void on_initial_album_fetch_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError error) { - txn.completed.disconnect(on_initial_album_fetch_complete); - txn.network_error.disconnect(on_initial_album_fetch_error); - - if (!is_running()) - return; - - debug("EVENT: fetching album information failed; response = '%s'.", - txn.get_response()); - - if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { - do_logout(); - } else { - // If we get any other kind of error, we can't recover, so just post it to the user - get_host().post_error(error); + string? pagination_token = null; + do { + yield txn.execute_async(); + + if (!is_running()) + return; + + var json = Json.from_string (txn.get_response()); + var object = json.get_object (); + if (!object.has_member ("albums")) { + throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE("Album fetch did not contain expected data"); + } + + if (object.has_member("nextPageToken")) { + pagination_token = object.get_member ("nextPageToken").get_string(); + } else { + pagination_token = null; + } + + var response_albums = object.get_member ("albums").get_array(); + response_albums.foreach_element( (a, b, element) => { + var album = element.get_object(); + var title = album.get_member("title"); + var is_writable = album.get_member("isWriteable"); + if (title != null && is_writable != null && is_writable.get_boolean()) + albums += new Album(title.get_string(), album.get_string_member("id")); + }); + + if (pagination_token != null) { + debug("Not finished fetching all albums, getting more... %s", pagination_token); + txn = new AlbumDirectoryTransaction(get_session(), pagination_token); + } + } while (pagination_token != null); + + debug("EVENT: finished fetching album information."); + this.publishing_parameters.set_albums(albums); + + show_publishing_options_pane(); + } catch (Error err) { + debug("EVENT: fetching album information failed; response = '%s'.", + txn.get_response()); + + if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { + do_logout(); + } else { + // If we get any other kind of error, we can't recover, so just post it to the user + get_host().post_error(err); + } } } - private void display_account_information(AlbumDirectoryTransaction txn) { - debug("ACTION: parsing album information"); - this.publishing_parameters.set_albums(txn.get_albums()); - - show_publishing_options_pane(); - } - private void show_publishing_options_pane() { debug("ACTION: showing publishing options pane."); @@ -348,65 +317,43 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { save_parameters_to_configuration_system(publishing_parameters); if (publishing_parameters.get_target_album_entry_id () != null) { - do_upload(); + do_upload.begin(); } else { - do_create_album(); + do_create_album.begin(); } } - private void do_create_album() { + private async void do_create_album() { debug("ACTION: Creating album"); assert(publishing_parameters.get_target_album_entry_id () == null); get_host().set_service_locked(true); var txn = new AlbumCreationTransaction(get_session(), publishing_parameters.get_target_album_name()); - txn.completed.connect(on_album_create_complete); - txn.network_error.connect(on_album_create_error); try { - txn.execute(); - } catch (Spit.Publishing.PublishingError error) { - on_album_create_error(txn, error); - } - } - - private void on_album_create_complete(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_album_create_complete); - txn.network_error.disconnect(on_album_create_error); + yield txn.execute_async(); - if (!is_running()) - return; + if (!is_running()) + return; - debug("EVENT: finished creating album information: %s", txn.get_response()); + debug("EVENT: finished creating album information: %s", txn.get_response()); - try { var node = Json.from_string(txn.get_response()); var object = node.get_object(); publishing_parameters.set_target_album_entry_id (object.get_string_member ("id")); - do_upload(); - } catch (Error error) { - on_album_create_error(txn, new Spit.Publishing.PublishingError.MALFORMED_RESPONSE (error.message)); - } - } - - private void on_album_create_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError error) { - txn.completed.disconnect(on_initial_album_fetch_complete); - txn.network_error.disconnect(on_initial_album_fetch_error); + yield do_upload(); + } catch (Error err) { + debug("EVENT: creating album failed; response = '%s'.", + txn.get_response()); - if (!is_running()) - return; - - debug("EVENT: creating album failed; response = '%s'.", - txn.get_response()); - - if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { - do_logout(); - } else { - // If we get any other kind of error, we can't recover, so just post it to the user - get_host().post_error(error); + if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { + do_logout(); + } else { + // If we get any other kind of error, we can't recover, so just post it to the user + get_host().post_error(err); + } } } @@ -420,7 +367,7 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { } } - private void do_upload() { + private async void do_upload() { debug("ACTION: uploading media items to remote server."); get_host().set_service_locked(true); @@ -439,10 +386,17 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { Spit.Publishing.Publishable[] publishables = get_host().get_publishables(); Uploader uploader = new Uploader(get_session(), publishables, publishing_parameters); - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); + try { + yield uploader.upload_async(on_upload_status_updated); + yield do_media_creation_batch(uploader); + } catch (Error err) { + if (!is_running()) + return; + + debug("EVENT: uploader reports upload error = '%s'.", err.message); - uploader.upload(on_upload_status_updated); + get_host().post_error(err); + } } private void on_upload_status_updated(int file_number, double completed_fraction) { @@ -456,85 +410,39 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { progress_reporter(file_number, completed_fraction); } - private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, - int num_published) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload complete; %d items published.", num_published); - - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - do_media_creation_batch(uploader); - } - - private void do_media_creation_batch(Publishing.RESTSupport.BatchUploader uploader) { + private async void do_media_creation_batch(Publishing.RESTSupport.BatchUploader uploader) { var u = (Uploader) uploader; - if (creation_offset >= u.upload_tokens.length) { - on_media_creation_complete(); - return; - } + while (creation_offset < u.upload_tokens.length) { + var end = creation_offset + MAX_BATCH_SIZE < u.upload_tokens.length ? + creation_offset + MAX_BATCH_SIZE : u.upload_tokens.length; + + var txn = new MediaCreationTransaction(get_session(), + u.upload_tokens[creation_offset:end], + u.titles[creation_offset:end], + publishing_parameters.get_target_album_entry_id()); - var end = creation_offset + MAX_BATCH_SIZE < u.upload_tokens.length ? - creation_offset + MAX_BATCH_SIZE : u.upload_tokens.length; - - var txn = new MediaCreationTransaction(get_session(), - u.upload_tokens[creation_offset:end], - u.titles[creation_offset:end], - publishing_parameters.get_target_album_entry_id()); - - txn.completed.connect(() => { - do_media_creation_batch(uploader); - }); - - txn.network_error.connect(on_media_creation_error); - - try { creation_offset = end; - txn.execute(); - } catch (Spit.Publishing.PublishingError error) { - on_media_creation_error(txn, error); + try { + yield txn.execute_async(); + if (!is_running()) + return; + + debug("EVENT: Media creation reports success."); + + get_host().set_service_locked(false); + get_host().install_success_pane(); + } catch (Spit.Publishing.PublishingError err) { + if (!is_running()) + return; + + debug("EVENT: Media creation reports error: %s", err.message); + + get_host().post_error(err); + } } } - private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, - Spit.Publishing.PublishingError err) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload error = '%s'.", err.message); - - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - get_host().post_error(err); - } - - private void on_media_creation_complete() { - if (!is_running()) - return; - - debug("EVENT: Media creation reports success."); - - get_host().set_service_locked(false); - get_host().install_success_pane(); - } - - private void on_media_creation_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_media_creation_complete); - txn.network_error.disconnect(on_media_creation_error); - - if (!is_running()) - return; - - debug("EVENT: Media creation reports error: %s", err.message); - - get_host().post_error(err); - } - public override bool is_running() { return running; } diff --git a/plugins/shotwell-publishing/PhotosPublishingPane.vala b/plugins/shotwell-publishing/PhotosPublishingPane.vala index d1b00d6..fe2dfbd 100644 --- a/plugins/shotwell-publishing/PhotosPublishingPane.vala +++ b/plugins/shotwell-publishing/PhotosPublishingPane.vala @@ -22,25 +22,25 @@ internal class PublishingOptionsPane : Gtk.Box, Spit.Publishing.DialogPane { }; [GtkChild] - private Gtk.Button logout_button; + private unowned Gtk.Button logout_button; [GtkChild] - private Gtk.Button publish_button; + private unowned Gtk.Button publish_button; [GtkChild] - private Gtk.RadioButton existing_album_radio; + private unowned Gtk.RadioButton existing_album_radio; [GtkChild] - private Gtk.ComboBoxText existing_albums_combo; + private unowned Gtk.ComboBoxText existing_albums_combo; [GtkChild] - private Gtk.ComboBoxText size_combo; + private unowned Gtk.ComboBoxText size_combo; [GtkChild] - private Gtk.Label publish_to_label; + private unowned Gtk.Label publish_to_label; [GtkChild] - private Gtk.Label login_identity_label; + private unowned Gtk.Label login_identity_label; [GtkChild] - private Gtk.CheckButton strip_metadata_check; + private unowned Gtk.CheckButton strip_metadata_check; [GtkChild] - private Gtk.RadioButton new_album_radio; + private unowned Gtk.RadioButton new_album_radio; [GtkChild] - private Gtk.Entry new_album_entry; + private unowned Gtk.Entry new_album_entry; public signal void publish(); public signal void logout(); diff --git a/plugins/shotwell-publishing/PhotosService.vala b/plugins/shotwell-publishing/PhotosService.vala index 8e328f4..c68510b 100644 --- a/plugins/shotwell-publishing/PhotosService.vala +++ b/plugins/shotwell-publishing/PhotosService.vala @@ -8,15 +8,7 @@ namespace Publishing.GooglePhotos { public class Service : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "google-photos.svg"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - static construct { - icon_pixbuf_set = Resources.load_from_resource(Resources.RESOURCE_PATH + "/" + ICON_FILENAME); - } - - public Service(GLib.File resource_directory) {} + public Service() {} public int get_pluggable_interface(int min_host_interface, int max_host_interface) { return Spit.negotiate_interfaces(min_host_interface, max_host_interface, @@ -31,16 +23,14 @@ public class Service : Object, Spit.Pluggable, Spit.Publishing.Service { return "Google Photos"; } - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); + info.authors = "Jens Georg"; info.copyright = _("Copyright 2019 Jens Georg <mail@jensge.org>"); - info.translators = Resources.TRANSLATORS; - info.version = _VERSION; - info.website_name = Resources.WEBSITE_NAME; - info.website_url = Resources.WEBSITE_URL; - info.is_license_wordwrapped = false; - info.license = Resources.LICENSE; - info.icons = icon_pixbuf_set; + info.icon_name = "google-photos"; + + return info; } public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { diff --git a/plugins/shotwell-publishing/PhotosUploader.vala b/plugins/shotwell-publishing/PhotosUploader.vala index 83137ee..68b8e41 100644 --- a/plugins/shotwell-publishing/PhotosUploader.vala +++ b/plugins/shotwell-publishing/PhotosUploader.vala @@ -11,7 +11,7 @@ internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.Authen private PublishingParameters parameters; private Publishing.RESTSupport.GoogleSession session; private Spit.Publishing.Publishable publishable; - private MappedFile mapped_file; + private InputStream mapped_file; public UploadTransaction(Publishing.RESTSupport.GoogleSession session, PublishingParameters parameters, Spit.Publishing.Publishable publishable) { @@ -28,13 +28,16 @@ internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.Authen return this.publishable; } - public override void execute() throws Spit.Publishing.PublishingError { + public override async void execute_async() throws Spit.Publishing.PublishingError { var basename = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); + int64 mapped_file_size = -1; // attempt to map the binary image data from disk into memory try { - mapped_file = new MappedFile(publishable.get_serialized_file().get_path(), false); - } catch (FileError e) { + mapped_file = publishable.get_serialized_file().read(null); + var info = ((FileInputStream)mapped_file).query_info("standard::size", null); + mapped_file_size = info.get_size(); + } catch (Error e) { string msg = "Google Photos: couldn't read data from %s: %s".printf( publishable.get_serialized_file().get_path(), e.message); warning("%s", msg); @@ -42,14 +45,6 @@ internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.Authen throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(msg); } - unowned uint8[] photo_data = (uint8[]) mapped_file.get_contents(); - photo_data.length = (int) mapped_file.get_length(); - - // bind the binary image data read from disk into a Soup.Buffer object so that we - // can attach it to the multipart request, then actaully append the buffer - // to the multipart request. Then, set the MIME type for this part. - Soup.Buffer bindable_data = new Soup.Buffer(Soup.MemoryUse.TEMPORARY, photo_data); - // create a message that can be sent over the wire whose payload is the multipart container // that we've been building up var outbound_message = new Soup.Message ("POST", get_endpoint_url()); @@ -58,12 +53,12 @@ internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.Authen outbound_message.request_headers.append("X-Goog-Upload-File-Name", basename); outbound_message.request_headers.append("X-Goog-Upload-Protocol", "raw"); outbound_message.request_headers.set_content_type("application/octet-stream", null); - outbound_message.request_body.append_buffer (bindable_data); - set_message(outbound_message); + outbound_message.set_request_body(null, mapped_file, (ssize_t)mapped_file_size); + set_message(outbound_message, (ulong)mapped_file_size); // send the message and get its response set_is_executed(true); - send(); + yield send_async(); } } diff --git a/plugins/shotwell-publishing/PiwigoPublishing.vala b/plugins/shotwell-publishing/PiwigoPublishing.vala index d311ac5..9bf0013 100644 --- a/plugins/shotwell-publishing/PiwigoPublishing.vala +++ b/plugins/shotwell-publishing/PiwigoPublishing.vala @@ -1,18 +1,28 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2016 Software Freedom Conservancy Inc. -public class PiwigoService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "piwigo.svg"; +internal class Publishing.Piwigo.Account : Spit.Publishing.Account, Object { + public string server_uri; + public string user; - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public PiwigoService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = Resources.load_from_resource - (Resources.RESOURCE_PATH + "/" + ICON_FILENAME); + public Account(string server_uri, string user) { + this.server_uri = server_uri; + this.user = user; + } + + public string display_name() { + try { + var uri = Uri.parse(server_uri, UriFlags.NONE); + return user + "@" + uri.get_host(); + } catch (Error err) { + debug("Failed to parse uri in Piwigo account. %s", err.message); + return user + "@" + server_uri; + } + } +} + +public class PiwigoService : Object, Spit.Pluggable, Spit.Publishing.Service { + public PiwigoService() { } public int get_pluggable_interface(int min_host_interface, int max_host_interface) { @@ -21,35 +31,68 @@ public class PiwigoService : Object, Spit.Pluggable, Spit.Publishing.Service { } public unowned string get_id() { - return "org.yorba.shotwell.publishing.piwigo"; + return "org.gnome.shotwell.publishing.piwigo"; } public unowned string get_pluggable_name() { return "Piwigo"; } - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); + info.authors = "Bruno Girin"; info.copyright = _("Copyright 2016 Software Freedom Conservancy Inc."); info.translators = Resources.TRANSLATORS; info.version = _VERSION; - info.website_name = Resources.WEBSITE_NAME; - info.website_url = Resources.WEBSITE_URL; - info.is_license_wordwrapped = false; - info.license = Resources.LICENSE; - info.icons = icon_pixbuf_set; + info.icon_name = "piwigo"; + + return info; } public void activation(bool enabled) { } public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { - return new Publishing.Piwigo.PiwigoPublisher(this, host); + return new Publishing.Piwigo.PiwigoPublisher(this, host, null); + } + + public Spit.Publishing.Publisher create_publisher_with_account(Spit.Publishing.PluginHost host, + Spit.Publishing.Account? account) { + return new Publishing.Piwigo.PiwigoPublisher(this, host, account); } public Spit.Publishing.Publisher.MediaType get_supported_media() { return (Spit.Publishing.Publisher.MediaType.PHOTO); } + + public Gee.List<Spit.Publishing.Account>? get_accounts(string profile_id) { + var list = new Gee.ArrayList<Spit.Publishing.Account>(); + + // Always add the empty default account to allow new logins + list.add(new Spit.Publishing.DefaultAccount()); + + // Collect information from saved logins + var schema = new Secret.Schema (Publishing.Piwigo.PiwigoPublisher.PASSWORD_SCHEME, Secret.SchemaFlags.NONE, + Publishing.Piwigo.PiwigoPublisher.SCHEMA_KEY_PROFILE_ID, Secret.SchemaAttributeType.STRING, + "url", Secret.SchemaAttributeType.STRING, + "user", Secret.SchemaAttributeType.STRING); + + var attributes = new HashTable<string, string>(str_hash, str_equal); + attributes[Publishing.Piwigo.PiwigoPublisher.SCHEMA_KEY_PROFILE_ID] = profile_id; + try { + var entries = Secret.password_searchv_sync(schema, attributes, Secret.SearchFlags.ALL, null); + + foreach (var entry in entries) { + var found_attributes = entry.get_attributes(); + list.add(new Publishing.Piwigo.Account(found_attributes["url"], found_attributes["user"])); + } + } catch (Error err) { + warning("Failed to look up accounts for Piwigo: %s", err.message); + } + + return list; + } } namespace Publishing.Piwigo { @@ -117,12 +160,16 @@ internal class PublishingParameters { public SizeEntry photo_size = null; public bool title_as_comment = false; public bool no_upload_tags = false; + public bool no_upload_ratings = false; public PublishingParameters() { } } public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { + internal const string PASSWORD_SCHEME = "org.gnome.Shotwell.Piwigo"; + internal const string SCHEMA_KEY_PROFILE_ID = "shotwell-profile-id"; + private Spit.Publishing.Service service; private Spit.Publishing.PluginHost host; private bool running = false; @@ -131,13 +178,26 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { private Category[] categories = null; private PublishingParameters parameters = null; private Spit.Publishing.ProgressCallback progress_reporter = null; + private Secret.Schema? schema = null; + private Publishing.Piwigo.Account? account = null; public PiwigoPublisher(Spit.Publishing.Service service, - Spit.Publishing.PluginHost host) { + Spit.Publishing.PluginHost host, + Spit.Publishing.Account? account) { debug("PiwigoPublisher instantiated."); this.service = service; this.host = host; session = new Session(); + + // This should only ever be the default account which we don't care about + if (account is Publishing.Piwigo.Account) { + this.account = (Publishing.Piwigo.Account)account; + } + + this.schema = new Secret.Schema (PASSWORD_SCHEME, Secret.SchemaFlags.NONE, + SCHEMA_KEY_PROFILE_ID, Secret.SchemaAttributeType.STRING, + "url", Secret.SchemaAttributeType.STRING, + "user", Secret.SchemaAttributeType.STRING); } // Publisher interface implementation @@ -164,14 +224,16 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { if (session.is_authenticated()) { debug("PiwigoPublisher: session is authenticated."); - do_fetch_categories(); + do_fetch_categories.begin(); } else { debug("PiwigoPublisher: session is not authenticated."); string? persistent_url = get_persistent_url(); string? persistent_username = get_persistent_username(); - string? persistent_password = get_persistent_password(); + string? persistent_password = get_persistent_password(persistent_url, persistent_username); + + // This will only be null if either of the other two was null or the password did not exist if (persistent_url != null && persistent_username != null && persistent_password != null) - do_network_login(persistent_url, persistent_username, + do_network_login.begin(persistent_url, persistent_username, persistent_password, get_remember_password()); else do_show_authentication_pane(); @@ -185,27 +247,65 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { // Session and persistent data public string? get_persistent_url() { - return host.get_config_string("url", null); + if (account != null) { + return account.server_uri; + } + + return null; } private void set_persistent_url(string url) { - host.set_config_string("url", url); + // Do nothing } public string? get_persistent_username() { - return host.get_config_string("username", null); + if (account != null) { + return account.user; + } + + return null; } private void set_persistent_username(string username) { - host.set_config_string("username", username); + // Do nothing } - public string? get_persistent_password() { - return host.get_config_string("password", null); + public string? get_persistent_password(string? url, string? user) { + if (url != null && user != null) { + try { + var pw = Secret.password_lookup_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + "url", url, "user", user); + + return pw; + } catch (Error err) { + critical("Failed to lookup the password for url %s and user %s: %s", url, user, err.message); + + return null; + } + } + + return null; } - private void set_persistent_password(string? password) { - host.set_config_string("password", password); + private void set_persistent_password(string? url, string? user, string? password) { + try { + if (password == null) { + // remove + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + "url", url, "user", user); + } else { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (Piwigo account %s@%s)".printf(user, url), + password, + null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + "url", url, "user", user); + } + } catch (Error err) { + critical("Failed to store password for %s@%s: %s", user, url, err.message); + } } public bool get_remember_password() { @@ -256,6 +356,14 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { host.set_config_bool("last-no-upload-tags", no_upload_tags); } + private bool get_last_no_upload_ratings() { + return host.get_config_bool("last-no-upload-ratings", false); + } + + private void set_last_no_upload_ratings(bool no_upload_ratings) { + host.set_config_bool("last-no-upload-ratings", no_upload_ratings); + } + private bool get_metadata_removal_choice() { return host.get_config_bool("strip_metadata", false); } @@ -290,9 +398,14 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { private void do_show_ssl_downgrade_pane (SessionLoginTransaction trans, string url) { - var uri = new Soup.URI (url); + string host_name = ""; + try { + host_name = GLib.Uri.parse (url, GLib.UriFlags.NONE).get_host(); + } catch (Error err) { + debug("Failed to parse URL: %s", err.message); + } host.set_service_locked (false); - var ssl_pane = new SSLErrorPane (trans, uri.get_host ()); + var ssl_pane = new SSLErrorPane (trans, host_name); ssl_pane.proceed.connect (() => { debug ("SSL: User wants us to retry with broken certificate"); this.session = new Session (); @@ -300,9 +413,9 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { string? persistent_url = get_persistent_url(); string? persistent_username = get_persistent_username(); - string? persistent_password = get_persistent_password(); + string? persistent_password = get_persistent_password(persistent_url, persistent_username); if (persistent_url != null && persistent_username != null && persistent_password != null) - do_network_login(persistent_url, persistent_username, + do_network_login.begin(persistent_url, persistent_username, persistent_password, get_remember_password()); else do_show_authentication_pane(); @@ -330,7 +443,7 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { if (!running) return; - do_network_login(url, username, password, remember_password); + do_network_login.begin(url, username, password, remember_password); } /** @@ -344,24 +457,24 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { * @param username the name of the Piwigo user used to login * @param password the password of the Piwigo user used to login */ - private void do_network_login(string url, string username, string password, bool remember_password) { + private async void do_network_login(string url, string username, string password, bool remember_password) { debug("ACTION: logging in"); host.set_service_locked(true); host.install_login_wait_pane(); set_remember_password(remember_password); - if (remember_password) - set_persistent_password(password); - else - set_persistent_password(null); + if (remember_password) { + set_persistent_password(url, username, password); + } else { + set_persistent_password(url, username, null); + } SessionLoginTransaction login_trans = new SessionLoginTransaction( session, normalise_url(url), username, password); - login_trans.network_error.connect(on_login_network_error); - login_trans.completed.connect(on_login_network_complete); try { - login_trans.execute(); + yield login_trans.execute_async(); + on_login_network_complete(login_trans); } catch (Spit.Publishing.PublishingError err) { if (err is Spit.Publishing.PublishingError.SSL_FAILED) { debug ("ERROR: SSL connection problems"); @@ -405,8 +518,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { */ private void on_login_network_complete(Publishing.RESTSupport.Transaction txn) { debug("EVENT: on_login_network_complete"); - txn.completed.disconnect(on_login_network_complete); - txn.network_error.disconnect(on_login_network_error); try { Publishing.RESTSupport.XmlDocument.parse_string( @@ -435,34 +546,9 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { debug("Setting session pwg_id to %s", pwg_id); session.set_pwg_id(pwg_id); - do_fetch_session_status(endpoint_url, pwg_id); - } - - /** - * Event triggered when a network login action fails due to a network error. - * - * This event triggered as a result of a network error during the login - * transaction. As a result, it assumes that the service URL entered in the - * authentication dialog is incorrect and re-presents the authentication - * dialog with FAILED_RETRY_URL mode. - * - * @param bad_txn the received REST transaction - * @param err the received error - */ - private void on_login_network_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_login_network_error"); - bad_txn.completed.disconnect(on_login_network_complete); - bad_txn.network_error.disconnect(on_login_network_error); - - if (session.is_authenticated()) // ignore these events if the session is already auth'd - return; - - do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_URL); + do_fetch_session_status.begin(endpoint_url, pwg_id); } - + /** * Action to fetch the session status for a known Piwigo user. * @@ -473,29 +559,27 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { * persisted. In this case, it will log the user in and confirm the * identity. */ - private void do_fetch_session_status(string url = "", string pwg_id = "") { + private async void do_fetch_session_status(string url = "", string pwg_id = "") { debug("ACTION: fetching session status"); host.set_service_locked(true); host.install_account_fetch_wait_pane(); if (!session.is_authenticated()) { SessionGetStatusTransaction status_txn = new SessionGetStatusTransaction.unauthenticated(session, url, pwg_id); - status_txn.network_error.connect(on_session_get_status_error); - status_txn.completed.connect(on_session_get_status_complete); try { - status_txn.execute(); + yield status_txn.execute_async(); + on_session_get_status_complete(status_txn); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: do_fetch_session_status, not authenticated"); do_show_error(err); } } else { SessionGetStatusTransaction status_txn = new SessionGetStatusTransaction(session); - status_txn.network_error.connect(on_session_get_status_error); - status_txn.completed.connect(on_session_get_status_complete); try { - status_txn.execute(); + yield status_txn.execute_async(); + on_session_get_status_complete(status_txn); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: do_fetch_session_status, authenticated"); do_show_error(err); @@ -512,8 +596,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { */ private void on_session_get_status_complete(Publishing.RESTSupport.Transaction txn) { debug("EVENT: on_session_get_status_complete"); - txn.completed.disconnect(on_session_get_status_complete); - txn.network_error.disconnect(on_session_get_status_error); if (!session.is_authenticated()) { string endpoint_url = txn.get_endpoint_url(); @@ -533,7 +615,7 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { session.authenticate(endpoint_url, username, pwg_id); set_persistent_url(session.get_pwg_url()); set_persistent_username(session.get_username()); - do_fetch_categories(); + do_fetch_categories.begin(); } catch (Spit.Publishing.PublishingError err2) { debug("ERROR: on_session_get_status_complete, inner"); do_show_error(err2); @@ -548,43 +630,30 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { // This should never happen as the session should not be // authenticated at that point so this call is a safeguard // against the interaction not happening properly. - do_fetch_categories(); + do_fetch_categories.begin(); } } /** - * Event triggered when the get session status fails due to a network error. - */ - private void on_session_get_status_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_session_get_status_error"); - bad_txn.completed.disconnect(on_session_get_status_complete); - bad_txn.network_error.disconnect(on_session_get_status_error); - on_network_error(bad_txn, err); - } - - /** * Action that fetches all available categories from the Piwigo service. * * This action fetches all categories from the Piwigo service in order * to populate the publishing pane presented to the user. */ - private void do_fetch_categories() { + private async void do_fetch_categories() { debug("ACTION: fetching categories"); host.set_service_locked(true); host.install_account_fetch_wait_pane(); CategoriesGetListTransaction cat_trans = new CategoriesGetListTransaction(session); - cat_trans.network_error.connect(on_category_fetch_error); - cat_trans.completed.connect(on_category_fetch_complete); try { - cat_trans.execute(); + yield cat_trans.execute_async(); + on_category_fetch_complete(cat_trans); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: do_fetch_categories"); do_show_error(err); + return; } } @@ -597,8 +666,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { */ private void on_category_fetch_complete(Publishing.RESTSupport.Transaction txn) { debug("EVENT: on_category_fetch_complete"); - txn.completed.disconnect(on_category_fetch_complete); - txn.network_error.disconnect(on_category_fetch_error); debug("PiwigoConnector: list of categories: %s", txn.get_response()); // Empty the categories if (categories != null) { @@ -654,20 +721,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { } /** - * Event triggered when the fetch categories transaction fails due to a - * network error. - */ - private void on_category_fetch_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_category_fetch_error"); - bad_txn.completed.disconnect(on_category_fetch_complete); - bad_txn.network_error.disconnect(on_category_fetch_error); - on_network_error(bad_txn, err); - } - - /** * Action that shows the publishing options pane. * * This action method shows the publishing options pane. @@ -678,8 +731,8 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { host.set_service_locked(false); PublishingOptionsPane opts_pane = new PublishingOptionsPane( this, categories, get_last_category(), get_last_permission_level(), get_last_photo_size(), - get_last_title_as_comment(), get_last_no_upload_tags(), get_metadata_removal_choice()); - opts_pane.logout.connect(on_publishing_options_pane_logout_clicked); + get_last_title_as_comment(), get_last_no_upload_tags(), get_last_no_upload_ratings(), get_metadata_removal_choice()); + opts_pane.logout.connect(() => { on_publishing_options_pane_logout_clicked.begin(); }); opts_pane.publish.connect(on_publishing_options_pane_publish_clicked); host.install_dialog_pane(opts_pane, Spit.Publishing.PluginHost.ButtonMode.CLOSE); host.set_dialog_default_widget(opts_pane.get_default_widget()); @@ -688,14 +741,12 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { /** * Event triggered when the user clicks logout in the publishing options pane. */ - private void on_publishing_options_pane_logout_clicked() { + private async void on_publishing_options_pane_logout_clicked() { debug("EVENT: on_publishing_options_pane_logout_clicked"); - SessionLogoutTransaction logout_trans = new SessionLogoutTransaction(session); - logout_trans.network_error.connect(on_logout_network_error); - logout_trans.completed.connect(on_logout_network_complete); try { - logout_trans.execute(); + yield new SessionLogoutTransaction(session).execute_async(); + on_logout_network_complete(); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: on_publishing_options_pane_logout_clicked"); do_show_error(err); @@ -708,29 +759,14 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { * This event de-authenticates the session and shows the authentication * pane again. */ - private void on_logout_network_complete(Publishing.RESTSupport.Transaction txn) { + private void on_logout_network_complete() { debug("EVENT: on_logout_network_complete"); - txn.completed.disconnect(on_logout_network_complete); - txn.network_error.disconnect(on_logout_network_error); session.deauthenticate(); do_show_authentication_pane(AuthenticationPane.Mode.INTRO); } - - /** - * Event triggered when the logout action fails due to a network error. - */ - private void on_logout_network_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_logout_network_error"); - bad_txn.completed.disconnect(on_logout_network_complete); - bad_txn.network_error.disconnect(on_logout_network_error); - on_network_error(bad_txn, err); - } - + /** * Event triggered when the user clicks publish in the publishing options pane. * @@ -748,9 +784,9 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { this.strip_metadata = strip_metadata; if (parameters.category.is_local()) { - do_create_category(parameters.category); + do_create_category.begin(parameters.category); } else { - do_upload(this.strip_metadata); + do_upload.begin(this.strip_metadata); } } @@ -760,12 +796,12 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { * This actions runs a REST transaction to create a new category in the * Piwigo library. It displays a wait pane with an information message * while the transaction is running. This action should only be called with - * a local cateogory, i.e. one that does not exist on the server and does + * a local category, i.e. one that does not exist on the server and does * not yet have an ID. * * @param category the new category to create on the server */ - private void do_create_category(Category category) { + private async void do_create_category(Category category) { debug("ACTION: creating a new category: %s".printf(category.name)); assert(category.is_local()); @@ -774,11 +810,10 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { CategoriesAddTransaction creation_trans = new CategoriesAddTransaction( session, category.name.strip(), int.parse(category.uppercats), category.comment); - creation_trans.network_error.connect(on_category_add_error); - creation_trans.completed.connect(on_category_add_complete); try { - creation_trans.execute(); + yield creation_trans.execute_async(); + on_category_add_complete(creation_trans); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: do_create_category"); do_show_error(err); @@ -794,8 +829,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { */ private void on_category_add_complete(Publishing.RESTSupport.Transaction txn) { debug("EVENT: on_category_add_complete"); - txn.completed.disconnect(on_category_add_complete); - txn.network_error.disconnect(on_category_add_error); // Parse the response try { @@ -808,30 +841,17 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { string id_string = id_node->get_content(); int id = int.parse(id_string); parameters.category.id = id; - do_upload(strip_metadata); + do_upload.begin(strip_metadata); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: on_category_add_complete"); do_show_error(err); } } - - /** - * Event triggered when the add category action fails due to a network error. - */ - private void on_category_add_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_category_add_error"); - bad_txn.completed.disconnect(on_category_add_complete); - bad_txn.network_error.disconnect(on_category_add_error); - on_network_error(bad_txn, err); - } - + /** * Upload action: the big one, the one we've been waiting for! */ - private void do_upload(bool strip_metadata) { + private async void do_upload(bool strip_metadata) { this.strip_metadata = strip_metadata; debug("ACTION: uploading pictures"); @@ -842,25 +862,27 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { set_last_photo_size(parameters.photo_size.id); set_last_title_as_comment(parameters.title_as_comment); set_last_no_upload_tags(parameters.no_upload_tags); + set_last_no_upload_ratings(parameters.no_upload_ratings); set_metadata_removal_choice(strip_metadata); progress_reporter = host.serialize_publishables(parameters.photo_size.id, this.strip_metadata); Spit.Publishing.Publishable[] publishables = host.get_publishables(); Uploader uploader = new Uploader(session, publishables, parameters); - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); - uploader.upload(on_upload_status_updated); + try { + var num_published = yield uploader.upload_async(on_upload_status_updated); + on_upload_complete(num_published); + } catch (Spit.Publishing.PublishingError err) { + do_show_error(err); + } } /** * Event triggered when the batch uploader reports that at least one of the * network transactions encapsulating uploads has completed successfully */ - private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, int num_published) { + private void on_upload_complete(int num_published) { debug("EVENT: on_upload_complete"); - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); // TODO: should a message be displayed to the user if num_published is zero? @@ -869,22 +891,7 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { do_show_success_pane(); } - - /** - * Event triggered when the batch uploader reports that at least one of the - * network transactions encapsulating uploads has caused a network error - */ - private void on_upload_error( - Publishing.RESTSupport.BatchUploader uploader, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_upload_error"); - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - do_show_error(err); - } - + /** * Event triggered when upload progresses and the status needs to be updated. */ @@ -910,17 +917,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { } /** - * Helper event to handle network errors. - */ - private void on_network_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_network_error"); - do_show_error(err); - } - - /** * Action to display an error to the user. */ private void do_show_error(Spit.Publishing.PublishingError e) { @@ -1029,8 +1025,9 @@ internal class SSLErrorPane : Shotwell.Plugins.Common.BuilderPane { base.constructed (); var label = this.get_builder ().get_object ("main_text") as Gtk.Label; + var bold_host = "<b>%s</b>".printf(host); // %s is the host name that we tried to connect to - label.set_text (_("This does not look like the real <b>%s</b>. Attackers might be trying to steal or alter information going to or from this site (for example, private messages, credit card information, or passwords).").printf (host)); + label.set_text (_("This does not look like the real %s. Attackers might be trying to steal or alter information going to or from this site (for example, private messages, credit card information, or passwords).").printf(bold_host)); label.use_markup = true; label = this.get_builder ().get_object ("ssl_errors") as Gtk.Label; @@ -1136,7 +1133,7 @@ internal class AuthenticationPane : Shotwell.Plugins.Common.BuilderPane { username_entry.set_text(persistent_username); } password_entry = builder.get_object ("password_entry") as Gtk.Entry; - string? persistent_password = publisher.get_persistent_password(); + string? persistent_password = publisher.get_persistent_password(persistent_url, persistent_username); if (persistent_password != null) { password_entry.set_text(persistent_password); } @@ -1205,6 +1202,7 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { private Gtk.CheckButton strip_metadata_check = null; private Gtk.CheckButton title_as_comment_check = null; private Gtk.CheckButton no_upload_tags_check = null; + private Gtk.CheckButton no_upload_ratings_check = null; private Gtk.Button logout_button; private Gtk.Button publish_button; private Gtk.TextView album_comment; @@ -1218,6 +1216,7 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { public int last_photo_size { private get; construct; } public bool last_title_as_comment { private get; construct; } public bool last_no_upload_tags { private get; construct; } + public bool last_no_upload_ratings { private get; construct; } public bool strip_metadata_enabled { private get; construct; } public Gee.List<Category> existing_categories { private get; construct; } public string default_comment { private get; construct; } @@ -1232,6 +1231,7 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { int last_photo_size, bool last_title_as_comment, bool last_no_upload_tags, + bool last_no_upload_ratings, bool strip_metadata_enabled) { Object (resource_path : Resources.RESOURCE_PATH + "/piwigo_publishing_options_pane.ui", @@ -1242,6 +1242,7 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { last_photo_size : last_photo_size, last_title_as_comment : last_title_as_comment, last_no_upload_tags : last_no_upload_tags, + last_no_upload_ratings : last_no_upload_ratings, strip_metadata_enabled : strip_metadata_enabled, existing_categories : new Gee.ArrayList<Category>.wrap (categories, Category.equal), @@ -1275,6 +1276,9 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { no_upload_tags_check = builder.get_object("no_upload_tags_check") as Gtk.CheckButton; no_upload_tags_check.set_active(last_no_upload_tags); + no_upload_ratings_check = builder.get_object("no_upload_ratings_check") as Gtk.CheckButton; + no_upload_ratings_check.set_active(last_no_upload_ratings); + logout_button = builder.get_object("logout_button") as Gtk.Button; logout_button.clicked.connect(on_logout_button_clicked); @@ -1325,6 +1329,7 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { params.photo_size = photo_sizes[size_combo.get_active()]; params.title_as_comment = title_as_comment_check.get_active(); params.no_upload_tags = no_upload_tags_check.get_active(); + params.no_upload_ratings = no_upload_ratings_check.get_active(); if (create_new_radio.get_active()) { string uploadcomment = album_comment.buffer.text.strip(); int a = within_existing_combo.get_active(); @@ -1720,12 +1725,14 @@ private class CategoriesAddTransaction : Transaction { private class ImagesAddTransaction : Publishing.RESTSupport.UploadTransaction { private PublishingParameters parameters = null; + private Session session = null; public ImagesAddTransaction(Session session, PublishingParameters parameters, Spit.Publishing.Publishable publishable) { base.with_endpoint_url(session, publishable, session.get_pwg_url()); if (session.is_authenticated()) { add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); } + this.session = session; this.parameters = parameters; string[] keywords = publishable.get_publishing_keywords(); @@ -1785,12 +1792,40 @@ private class ImagesAddTransaction : Publishing.RESTSupport.UploadTransaction { !basename.down().has_suffix(".jpg")) { basename += ".jpg"; } - disposition_table.insert("filename", Soup.URI.encode(basename, null)); + disposition_table.insert("filename", GLib.Uri.escape_string(basename, null)); disposition_table.insert("name", "image"); set_binary_disposition_table(disposition_table); + base.completed.connect(on_completed); + } + + private void on_completed() { + try{ + Publishing.RESTSupport.XmlDocument resp_doc = Publishing.RESTSupport.XmlDocument.parse_string( + base.get_response(), Transaction.validate_xml); + Xml.Node* image_node = resp_doc.get_named_child(resp_doc.get_root_node(), "image_id"); + string image_id = image_node->get_content(); + + if (!parameters.no_upload_ratings) + new ImagesAddRating(session, publishable, image_id); + } catch(Spit.Publishing.PublishingError err) { + debug("Response parse error"); + } } } -} // namespace +private class ImagesAddRating : Publishing.RESTSupport.UploadTransaction { + public ImagesAddRating(Session session, Spit.Publishing.Publishable publishable, string image_id) { + base.with_endpoint_url(session, publishable, session.get_pwg_url()); + if (session.is_authenticated()) { + add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); + } + add_argument("method", "pwg.images.rate"); + add_argument("image_id", image_id); + add_argument("rate", publishable.get_rating().to_string()); + base.execute_async.begin(); + } +} + +} // namespace diff --git a/plugins/shotwell-publishing/TumblrPublishing.vala b/plugins/shotwell-publishing/TumblrPublishing.vala index 7061d6d..c9fed3f 100644 --- a/plugins/shotwell-publishing/TumblrPublishing.vala +++ b/plugins/shotwell-publishing/TumblrPublishing.vala @@ -6,16 +6,7 @@ */ public class TumblrService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "tumblr.png"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public TumblrService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = - Resources.load_from_resource(Resources.RESOURCE_PATH + "/" + - ICON_FILENAME); - } + public TumblrService() {} public int get_pluggable_interface(int min_host_interface, int max_host_interface) { return Spit.negotiate_interfaces(min_host_interface, max_host_interface, @@ -23,23 +14,21 @@ public class TumblrService : Object, Spit.Pluggable, Spit.Publishing.Service { } public unowned string get_id() { - return "org.yorba.shotwell.publishing.tumblr"; + return "org.gnome.shotwell.publishing.tumblr"; } public unowned string get_pluggable_name() { return "Tumblr"; } - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); + info.authors = "Jeroen Arnoldus"; info.copyright = _("Copyright 2012 BJA Electronics"); - info.translators = Resources.TRANSLATORS; - info.version = _VERSION; - info.website_name = Resources.WEBSITE_NAME; - info.website_url = Resources.WEBSITE_URL; - info.is_license_wordwrapped = false; - info.license = Resources.LICENSE; - info.icons = icon_pixbuf_set; + info.icon_name = "tumblr"; + + return info; } public void activation(bool enabled) { @@ -189,72 +178,47 @@ namespace Publishing.Tumblr { auth_token_secret.get_string(), ""); - do_get_blogs(); + do_get_blogs.begin(); } - private void do_get_blogs() { + private async void do_get_blogs() { debug("ACTION: obtain all blogs of the tumblr user"); UserInfoFetchTransaction txn = new UserInfoFetchTransaction(session); - txn.completed.connect(on_info_request_txn_completed); - txn.network_error.connect(on_info_request_txn_error); try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield txn.execute_async(); + if (!is_running()) + return; + + debug("EVENT: user info request transaction completed; response = '%s'", + txn.get_response()); + do_parse_token_info_from_user_request(txn.get_response()); + do_show_publishing_options_pane(); + } catch (Error err) { + session.deauthenticate(); + //invalidate_persistent_session(); + debug("EVENT: user info request transaction caused a network error"); host.post_error(err); } - - } - - private void on_info_request_txn_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_info_request_txn_completed); - txn.network_error.disconnect(on_info_request_txn_error); - - if (!is_running()) - return; - - debug("EVENT: user info request transaction completed; response = '%s'", - txn.get_response()); - do_parse_token_info_from_user_request(txn.get_response()); - do_show_publishing_options_pane(); - } - - - private void do_parse_token_info_from_user_request(string response) { + private void do_parse_token_info_from_user_request(string response) throws Error { debug("ACTION: parsing info request response '%s' into list of available blogs", response); - try { - var parser = new Json.Parser(); - parser.load_from_data (response, -1); - var root_object = parser.get_root().get_object(); - this.username = root_object.get_object_member("response").get_object_member("user").get_string_member ("name"); - debug("Got user name: %s",username); - foreach (var blognode in root_object.get_object_member("response").get_object_member("user").get_array_member("blogs").get_elements ()) { - var blog = blognode.get_object (); - string name = blog.get_string_member ("name"); - string url = blog.get_string_member ("url").replace("http://","").replace("https://", "").replace("/",""); - debug("Got blog name: %s and url: %s", name, url); - this.blogs += new BlogEntry(name,url); - } - } catch (Error err) { - host.post_error(err); + + var parser = new Json.Parser(); + parser.load_from_data (response, -1); + var root_object = parser.get_root().get_object(); + this.username = root_object.get_object_member("response").get_object_member("user").get_string_member ("name"); + debug("Got user name: %s",username); + foreach (var blognode in root_object.get_object_member("response").get_object_member("user").get_array_member("blogs").get_elements ()) { + var blog = blognode.get_object (); + string name = blog.get_string_member ("name"); + string url = blog.get_string_member ("url").replace("http://","").replace("https://", "").replace("/",""); + debug("Got blog name: %s and url: %s", name, url); + this.blogs += new BlogEntry(name,url); } } - private void on_info_request_txn_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_info_request_txn_completed); - txn.network_error.disconnect(on_info_request_txn_error); - - if (!is_running()) - return; - - session.deauthenticate(); - //invalidate_persistent_session(); - debug("EVENT: user info request transaction caused a network error"); - host.post_error(err); - } private void do_show_publishing_options_pane() { debug("ACTION: displaying publishing options pane"); @@ -328,7 +292,7 @@ namespace Publishing.Tumblr { Uploader uploader = new Uploader(session, sorted_list.to_array(),blog_url); uploader.upload_complete.connect(on_upload_complete); uploader.upload_error.connect(on_upload_error); - uploader.upload(on_upload_status_updated); + uploader.upload_async.begin(on_upload_status_updated); } private void do_show_success_pane() { @@ -575,22 +539,6 @@ namespace Publishing.Tumblr { internal class UploadTransaction : Publishing.RESTSupport.OAuth1.UploadTransaction { //Workaround for Soup.URI.encode() to support binary data (i.e. string with \0) - private string encode( uint8[] data ){ - var s = new StringBuilder(); - char[] bytes = new char[2]; - bytes[1] = 0; - foreach( var byte in data ) - { - if(byte == 0) { - s.append( "%00" ); - } else { - bytes[0] = (char)byte; - s.append( Soup.URI.encode((string) bytes, ENCODE_RFC_3986_EXTRA) ); - } - } - return s.str; - } - public UploadTransaction(Publishing.RESTSupport.OAuth1.Session session,Spit.Publishing.Publishable publishable, string blog_url) { debug("Init upload transaction"); @@ -598,25 +546,21 @@ namespace Publishing.Tumblr { } - public override void execute() throws Spit.Publishing.PublishingError { + public override async void execute_async() throws Spit.Publishing.PublishingError { string payload; size_t payload_length; try { FileUtils.get_contents(base.publishable.get_serialized_file().get_path(), out payload, out payload_length); - string reqdata = this.encode(payload.data[0:payload_length]); - - - - add_argument("data[0]", reqdata); + add_argument("data64", Base64.encode(payload.data[0:payload_length])); add_argument("type", "photo"); string[] keywords = base.publishable.get_publishing_keywords(); string tags = ""; if (keywords != null) { tags = string.joinv (",", keywords); } - add_argument("tags", Soup.URI.encode(tags, ENCODE_RFC_3986_EXTRA)); + add_argument("tags", tags); } catch (FileError e) { throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( @@ -626,13 +570,15 @@ namespace Publishing.Tumblr { this.authorize(); - Publishing.RESTSupport.Argument[] request_arguments = get_arguments(); - assert(request_arguments.length > 0); - - var request_data = Publishing.RESTSupport.Argument.serialize_list(request_arguments); + var form = new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal); + foreach (var arg in get_arguments()) { + form.insert(arg.key, arg.value); + } + assert(form.size() > 0); - Soup.Message outbound_message = new Soup.Message( "POST", get_endpoint_url()); - outbound_message.set_request("application/x-www-form-urlencoded", Soup.MemoryUse.COPY, request_data.data); + var outbound_message = new Soup.Message ("POST", get_endpoint_url()); + var body = new Bytes(Soup.Form.encode_hash(form).data); + outbound_message.set_request_body_from_bytes(Soup.FORM_MIME_TYPE_URLENCODED, body); // TODO: there must be a better way to iterate over a map Gee.MapIterator<string, string> i = base.message_headers.map_iterator(); @@ -641,11 +587,11 @@ namespace Publishing.Tumblr { outbound_message.request_headers.append(i.get_key(), i.get_value()); cont = i.next(); } - set_message(outbound_message); + set_message(outbound_message, body.length); set_is_executed(true); - send(); + yield send_async(); } } diff --git a/plugins/shotwell-publishing/YouTubePublishing.vala b/plugins/shotwell-publishing/YouTubePublishing.vala index e50d17a..f0a9866 100644 --- a/plugins/shotwell-publishing/YouTubePublishing.vala +++ b/plugins/shotwell-publishing/YouTubePublishing.vala @@ -5,14 +5,7 @@ */ public class YouTubeService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "youtube.png"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public YouTubeService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = Resources.load_from_resource - (Resources.RESOURCE_PATH + "/" + ICON_FILENAME); + public YouTubeService() { } public int get_pluggable_interface(int min_host_interface, int max_host_interface) { @@ -21,23 +14,20 @@ public class YouTubeService : Object, Spit.Pluggable, Spit.Publishing.Service { } public unowned string get_id() { - return "org.yorba.shotwell.publishing.youtube"; + return "org.gnome.shotwell.publishing.youtube"; } public unowned string get_pluggable_name() { return "YouTube"; } - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); info.authors = "Jani Monoses, Lucas Beeler"; info.copyright = _("Copyright 2016 Software Freedom Conservancy Inc."); - info.translators = Resources.TRANSLATORS; - info.version = _VERSION; - info.website_name = Resources.WEBSITE_NAME; - info.website_url = Resources.WEBSITE_URL; - info.is_license_wordwrapped = false; - info.license = Resources.LICENSE; - info.icons = icon_pixbuf_set; + info.icon_name = "youtube"; + + return info; } public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { @@ -60,10 +50,23 @@ private const string DEVELOPER_KEY = private enum PrivacySetting { PUBLIC, UNLISTED, - PRIVATE + PRIVATE; + + public string to_string() { + switch (this) { + case PUBLIC: + return "public"; + case UNLISTED: + return "unlisted"; + case PRIVATE: + return "private"; + default: + assert_not_reached(); + } + } } -private class PublishingParameters { +internal class PublishingParameters { private PrivacySetting privacy; private string? user_name; @@ -89,44 +92,14 @@ private class PublishingParameters { } } -internal class YouTubeAuthorizer : GData.Authorizer, Object { - private RESTSupport.GoogleSession session; - private Spit.Publishing.Authenticator authenticator; - - public YouTubeAuthorizer(RESTSupport.GoogleSession session, Spit.Publishing.Authenticator authenticator) { - this.session = session; - this.authenticator = authenticator; - } - - public bool is_authorized_for_domain(GData.AuthorizationDomain domain) { - return domain.scope.has_suffix ("auth/youtube"); - } - - public void process_request(GData.AuthorizationDomain? domain, - Soup.Message message) { - if (domain == null) { - return; - } - - var header = "Bearer %s".printf(session.get_access_token()); - message.request_headers.replace("Authorization", header); - } - - public bool refresh_authorization (GLib.Cancellable? cancellable = null) throws GLib.Error { - this.authenticator.refresh(); - return true; - } -} - public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { private bool running; private PublishingParameters publishing_parameters; private Spit.Publishing.ProgressCallback? progress_reporter; private Spit.Publishing.Authenticator authenticator; - private GData.YouTubeService youtube_service; public YouTubePublisher(Spit.Publishing.Service service, Spit.Publishing.PluginHost host) { - base(service, host, "https://gdata.youtube.com/"); + base(service, host, "https://www.googleapis.com/upload/youtube/v3/videos"); this.running = false; this.publishing_parameters = new PublishingParameters(); @@ -161,8 +134,6 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { publishing_parameters.set_user_name(get_session().get_user_name()); - this.youtube_service = new GData.YouTubeService(DEVELOPER_KEY, - new YouTubeAuthorizer(get_session(), this.authenticator)); do_show_publishing_options_pane(); } @@ -181,7 +152,7 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { if (!is_running()) return; - do_upload(); + do_upload.begin(); } private void on_upload_status_updated(int file_number, double completed_fraction) { @@ -195,32 +166,6 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { progress_reporter(file_number, completed_fraction); } - private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, - int num_published) { - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - debug("EVENT: uploader reports upload complete; %d items published.", num_published); - - if (!is_running()) - return; - - do_show_success_pane(); - } - - private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, - Spit.Publishing.PublishingError err) { - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - if (!is_running()) - return; - - debug("EVENT: uploader reports upload error = '%s'.", err.message); - - get_host().post_error(err); - } - private void do_show_publishing_options_pane() { debug("ACTION: showing publishing options pane."); @@ -245,7 +190,7 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { get_host().set_service_locked(false); } - private void do_upload() { + private async void do_upload() { debug("ACTION: uploading media items to remote server."); get_host().set_service_locked(true); @@ -261,12 +206,24 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { return; Spit.Publishing.Publishable[] publishables = get_host().get_publishables(); - Uploader uploader = new Uploader(this.youtube_service, get_session(), publishables, publishing_parameters); + Uploader uploader = new Uploader(get_session(), publishables, publishing_parameters); + + try { + var num_published = yield uploader.upload_async(on_upload_status_updated); + debug("EVENT: uploader reports upload complete; %d items published.", num_published); + + if (!is_running()) + return; + + do_show_success_pane(); + } catch (Error err) { + if (!is_running()) + return; - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); + debug("EVENT: uploader reports upload error = '%s'.", err.message); - uploader.upload(on_upload_status_updated); + get_host().post_error(err); + } } private void do_show_success_pane() { @@ -397,104 +354,20 @@ internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { } } -internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { - private const string ENDPOINT_URL = "https://uploads.gdata.youtube.com/feeds/api/users/default/uploads"; - private PublishingParameters parameters; - private Publishing.RESTSupport.GoogleSession session; - private Spit.Publishing.Publishable publishable; - private GData.YouTubeService youtube_service; - - public UploadTransaction(GData.YouTubeService youtube_service, Publishing.RESTSupport.GoogleSession session, - PublishingParameters parameters, Spit.Publishing.Publishable publishable) { - base(session, ENDPOINT_URL, Publishing.RESTSupport.HttpMethod.POST); - assert(session.is_authenticated()); - this.session = session; - this.parameters = parameters; - this.publishable = publishable; - this.youtube_service = youtube_service; - } - - public override void execute() throws Spit.Publishing.PublishingError { - var video = new GData.YouTubeVideo(null); - - var slug = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); - // Set title to publishing name, but if that's empty default to filename. - string title = publishable.get_publishing_name(); - if (title == "") { - title = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); - } - video.title = title; - - video.is_private = (parameters.get_privacy() == PrivacySetting.PRIVATE); - - if (parameters.get_privacy() == PrivacySetting.UNLISTED) { - video.set_access_control("list", GData.YouTubePermission.DENIED); - } else if (!video.is_private) { - video.set_access_control("list", GData.YouTubePermission.ALLOWED); - } - - var file = publishable.get_serialized_file(); - - try { - var info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE + "," + - FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE); - var upload_stream = this.youtube_service.upload_video(video, slug, - info.get_content_type()); - var input_stream = file.read(); - - // Yuck... - var loop = new MainLoop(null, false); - this.splice_with_progress.begin(info, input_stream, upload_stream, (obj, res) => { - try { - this.splice_with_progress.end(res); - } catch (Error error) { - critical("Failed to upload: %s", error.message); - } - loop.quit(); - }); - loop.run(); - video = this.youtube_service.finish_video_upload(upload_stream); - } catch (Error error) { - critical("Upload failed: %s", error.message); - } - } - - private async void splice_with_progress(GLib.FileInfo info, GLib.InputStream input, GLib.OutputStream output) throws Error { - var total_bytes = info.get_size(); - var bytes_to_write = total_bytes; - uint8 buffer[8192]; - - while (bytes_to_write > 0) { - var bytes_read = yield input.read_async(buffer); - if (bytes_read == 0) - break; - - var bytes_written = yield output.write_async(buffer[0:bytes_read]); - bytes_to_write -= bytes_written; - chunk_transmitted((int)(total_bytes - bytes_to_write), (int) total_bytes); - } - - yield output.close_async(); - yield input.close_async(); - } -} - internal class Uploader : Publishing.RESTSupport.BatchUploader { private PublishingParameters parameters; - private GData.YouTubeService youtube_service; - public Uploader(GData.YouTubeService youtube_service, Publishing.RESTSupport.GoogleSession session, + public Uploader(Publishing.RESTSupport.GoogleSession session, Spit.Publishing.Publishable[] publishables, PublishingParameters parameters) { base(session, publishables); this.parameters = parameters; - this.youtube_service = youtube_service; } protected override Publishing.RESTSupport.Transaction create_transaction( Spit.Publishing.Publishable publishable) { - return new UploadTransaction(this.youtube_service, (Publishing.RESTSupport.GoogleSession) get_session(), - parameters, get_current_publishable()); + return new UploadTransaction((Publishing.RESTSupport.GoogleSession) get_session(), + parameters, get_current_publishable()); } } diff --git a/plugins/shotwell-publishing/YoutubeUploader.vala b/plugins/shotwell-publishing/YoutubeUploader.vala new file mode 100644 index 0000000..47c6051 --- /dev/null +++ b/plugins/shotwell-publishing/YoutubeUploader.vala @@ -0,0 +1,76 @@ +using Spit; + +internal class Publishing.YouTube.UploadTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + private PublishingParameters parameters; + private Publishing.RESTSupport.GoogleSession session; + private Spit.Publishing.Publishable publishable; + + public UploadTransaction(Publishing.RESTSupport.GoogleSession session, + PublishingParameters parameters, Spit.Publishing.Publishable publishable) { + base(session, "https://www.googleapis.com/upload/youtube/v3/videos", + Publishing.RESTSupport.HttpMethod.POST); + assert(session.is_authenticated()); + + this.session = session; + this.parameters = parameters; + this.publishable = publishable; + } + public override async void execute_async() throws Spit.Publishing.PublishingError { + // Collect parameters + + var slug = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); + // Set title to publishing name, but if that's empty default to filename. + string title = publishable.get_publishing_name(); + if (title == "") { + title = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); + } + + var builder = new Json.Builder(); + builder.begin_object(); + builder.set_member_name("snippet"); + builder.begin_object(); + builder.set_member_name("description"); + builder.add_string_value(slug); + builder.set_member_name("title"); + builder.add_string_value(title); + builder.end_object(); + builder.set_member_name("status"); + builder.begin_object(); + builder.set_member_name("privacyStatus"); + builder.add_string_value(parameters.get_privacy().to_string()); + builder.end_object(); + builder.end_object(); + + var meta_data = Json.to_string (builder.get_root(), false); + debug ("Parameters: %s", meta_data); + var message_parts = new Soup.Multipart("multipart/related"); + var headers = new Soup.MessageHeaders(Soup.MessageHeadersType.MULTIPART); + var encoding = new GLib.HashTable<string, string>(str_hash, str_equal); + encoding.insert("encoding", "UTF-8"); + headers.set_content_type ("application/json", encoding); + + message_parts.append_part (headers, new Bytes (meta_data.data)); + headers = new Soup.MessageHeaders(Soup.MessageHeadersType.MULTIPART); + headers.set_content_type ("application/octet-stream", null); + headers.append("Content-Transfer-Encoding", "binary"); + + MappedFile? mapped_file = null; + try { + mapped_file = new MappedFile(publishable.get_serialized_file().get_path(), false); + } catch (Error e) { + throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( + _("A temporary file needed for publishing is unavailable")); + } + + + message_parts.append_part (headers, mapped_file.get_bytes()); + + var outbound_message = new Soup.Message.from_multipart (get_endpoint_url() + "?part=" + GLib.Uri.escape_string ("snippet,status"), message_parts); + outbound_message.get_request_headers().append("Authorization", "Bearer " + + session.get_access_token()); + + set_message(outbound_message, mapped_file.get_length() + meta_data.length); + set_is_executed(true); + yield send_async(); + } +} diff --git a/plugins/shotwell-publishing/facebook.png b/plugins/shotwell-publishing/facebook.png Binary files differdeleted file mode 100644 index 384609f..0000000 --- a/plugins/shotwell-publishing/facebook.png +++ /dev/null diff --git a/plugins/shotwell-publishing/facebook_publishing_options_pane.ui b/plugins/shotwell-publishing/facebook_publishing_options_pane.ui deleted file mode 100644 index 4d6b021..0000000 --- a/plugins/shotwell-publishing/facebook_publishing_options_pane.ui +++ /dev/null @@ -1,223 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> -<interface domain="shotwell"> - <requires lib="gtk+" version="3.14"/> - <object class="GtkBox" id="facebook_pane_box"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="homogeneous">True</property> - <child> - <object class="GtkBox" id="facebook_pane_inner_box"> - <property name="width_request">1</property> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="orientation">vertical</property> - <property name="spacing">16</property> - <child> - <object class="GtkLabel" id="how_to_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">16</property> - <property name="margin_right">16</property> - <property name="margin_top">16</property> - <property name="margin_bottom">16</property> - <property name="label"> (text depends on fb username and is modified in the app - -anything put into this field won't display)</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkGrid" id="grid1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="row_spacing">8</property> - <property name="column_spacing">32</property> - <property name="column_homogeneous">True</property> - <child> - <object class="GtkRadioButton" id="use_existing_radio"> - <property name="label" translatable="yes">Publish to an e_xisting album:</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="use_underline">True</property> - <property name="draw_indicator">True</property> - <property name="group">create_new_radio</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkRadioButton" id="create_new_radio"> - <property name="label" translatable="yes">Create a _new album named:</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="margin_bottom">8</property> - <property name="use_underline">True</property> - <property name="active">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="new_album_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="margin_bottom">8</property> - <property name="invisible_char">●</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkComboBoxText" id="existing_albums_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="size_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="label" translatable="yes">Upload _size:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">resolution_combo</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">3</property> - </packing> - </child> - <child> - <object class="GtkComboBoxText" id="resolution_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">3</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="visibility_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="label" translatable="yes">Videos and new photo albums _visible to:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">visibility_combo</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">2</property> - </packing> - </child> - <child> - <object class="GtkComboBoxText" id="visibility_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">2</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="padding">4</property> - <property name="position">1</property> - </packing> - </child> - <child> - <object class="GtkCheckButton" id="strip_metadata_check"> - <property name="label" translatable="yes">_Remove location, camera, and other identifying information before uploading</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">False</property> - <property name="halign">start</property> - <property name="use_underline">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">2</property> - </packing> - </child> - <child> - <object class="GtkBox" id="box2"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="spacing">32</property> - <property name="homogeneous">True</property> - <child> - <object class="GtkButton" id="logout_button"> - <property name="label" translatable="yes">_Logout</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="padding">80</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="publish_button"> - <property name="label" translatable="yes">_Publish</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="padding">80</property> - <property name="position">1</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="padding">2</property> - <property name="position">3</property> - </packing> - </child> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="padding">8</property> - <property name="pack_type">end</property> - <property name="position">0</property> - </packing> - </child> - </object> -</interface> diff --git a/plugins/shotwell-publishing/flickr_publishing_options_pane.ui b/plugins/shotwell-publishing/flickr_publishing_options_pane.ui index 77d32cf..e5e61aa 100644 --- a/plugins/shotwell-publishing/flickr_publishing_options_pane.ui +++ b/plugins/shotwell-publishing/flickr_publishing_options_pane.ui @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> +<!-- Generated with glade 3.22.0 --> <interface domain="shotwell"> <requires lib="gtk+" version="3.14"/> <object class="GtkBox" id="flickr_pane"> @@ -36,8 +36,8 @@ so changes made here will not display)</property> <property name="halign">center</property> <property name="valign">center</property> <property name="margin_bottom">8</property> - <property name="row_spacing">8</property> - <property name="column_spacing">24</property> + <property name="row_spacing">6</property> + <property name="column_spacing">12</property> <child> <object class="GtkLabel" id="visibility_label"> <property name="visible">True</property> @@ -58,7 +58,7 @@ so changes made here will not display)</property> <property name="visible">True</property> <property name="can_focus">False</property> <property name="halign">end</property> - <property name="label" translatable="yes">Photo _size:</property> + <property name="label" translatable="yes">Photo _size</property> <property name="use_underline">True</property> <property name="mnemonic_widget">size_combo</property> </object> diff --git a/plugins/shotwell-publishing/meson.build b/plugins/shotwell-publishing/meson.build index eeac177..a93726b 100644 --- a/plugins/shotwell-publishing/meson.build +++ b/plugins/shotwell-publishing/meson.build @@ -1,9 +1,9 @@ shotwell_publishing_sources = [ 'shotwell-publishing.vala', - 'FacebookPublishing.vala', 'FlickrPublishing.vala', 'TumblrPublishing.vala', 'YouTubePublishing.vala', + 'YoutubeUploader.vala', 'PiwigoPublishing.vala', 'PhotosPublisher.vala', 'PhotosService.vala', @@ -13,13 +13,13 @@ shotwell_publishing_sources = [ shotwell_publishing_resources = gnome.compile_resources('publishing-resource', 'org.gnome.Shotwell.Publishing.gresource.xml', - source_dir : meson.source_root()) + source_dir : meson.project_source_root()) shared_module('shotwell-publishing', shotwell_publishing_sources + shotwell_publishing_resources, dependencies : [gtk, soup, gexiv2, gee, sw_plugin, json_glib, - webkit, sw_plugin_common_dep, xml, gdata, gcr, - gcr_ui, authenticator_dep], + webkit, sw_plugin_common_dep, xml, gcr, + gcr_ui, authenticator_dep, secret], c_args : ['-DPLUGIN_RESOURCE_PATH="/org/gnome/Shotwell/Publishing"', '-DGCR_API_SUBJECT_TO_CHANGE'], install: true, diff --git a/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml b/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml index e04ab02..5272df6 100644 --- a/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml +++ b/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml @@ -1,12 +1,12 @@ <?xml version="1.0" encoding="UTF-8"?> <gresources> <gresource prefix="/org/gnome/Shotwell/Publishing"> - <file>facebook.png</file> - <file>piwigo.svg</file> - <file>youtube.png</file> - <file>tumblr.png</file> - <file>google-photos.svg</file> - <file>facebook_publishing_options_pane.ui</file> + <!-- icons, with additional aliases into the icon namespace --> + <file alias="icons/hicolor/scalable/apps/piwigo.svg">piwigo.svg</file> + <file alias="icons/hicolor/24x24/apps/youtube.png">youtube.png</file> + <file alias="icons/hicolor/scalable/apps/google-photos.svg">google-photos.svg</file> + <file alias="icons/hicolor/scalable/apps/tumblr.svg">tumblr.svg</file> + <file>flickr_publishing_options_pane.ui</file> <file>google_photos_publishing_options_pane.ui</file> <file>piwigo_authentication_pane.ui</file> diff --git a/plugins/shotwell-publishing/piwigo_publishing_options_pane.ui b/plugins/shotwell-publishing/piwigo_publishing_options_pane.ui index 397d2ec..c17d616 100644 --- a/plugins/shotwell-publishing/piwigo_publishing_options_pane.ui +++ b/plugins/shotwell-publishing/piwigo_publishing_options_pane.ui @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> +<!-- Generated with glade 3.22.0 --> <interface domain="shotwell"> <requires lib="gtk+" version="3.14"/> <object class="GtkWindow" id="publishing_options_pane"> @@ -8,8 +8,8 @@ <object class="GtkBox" id="content"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="margin_left">10</property> - <property name="margin_right">10</property> + <property name="margin_start">10</property> + <property name="margin_end">10</property> <property name="orientation">vertical</property> <property name="spacing">12</property> <child> @@ -247,6 +247,22 @@ </packing> </child> <child> + <object class="GtkCheckButton" id="no_upload_ratings_check"> + <property name="label" translatable="yes">_Do not upload ratings</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + <child> <object class="GtkButtonBox" id="hbuttonbox1"> <property name="visible">True</property> <property name="can_focus">False</property> @@ -280,10 +296,13 @@ <packing> <property name="expand">True</property> <property name="fill">True</property> - <property name="position">5</property> + <property name="position">6</property> </packing> </child> </object> </child> + <child type="titlebar"> + <placeholder/> + </child> </object> </interface> diff --git a/plugins/shotwell-publishing/shotwell-publishing.vala b/plugins/shotwell-publishing/shotwell-publishing.vala index 52c82ec..f5251f4 100644 --- a/plugins/shotwell-publishing/shotwell-publishing.vala +++ b/plugins/shotwell-publishing/shotwell-publishing.vala @@ -13,45 +13,37 @@ private class ShotwellPublishingCoreServices : Object, Spit.Module { // we need to get a module file handle because our pluggables have to load resources from the // module file directory public ShotwellPublishingCoreServices(GLib.File module_file) { - GLib.File resource_directory = module_file.get_parent(); var factory = Publishing.Authenticator.Factory.get_instance(); var authenicators = factory.get_available_authenticators(); // Prevent vala complaining when all authenticators from this plugin // are disabled - debug("Looking for resources in %s", resource_directory.get_path()); debug("Found %d authenicators", authenicators.size); -#if HAVE_FACEBOOK - if (authenicators.contains("facebook")) { - pluggables += new FacebookService(resource_directory); - } -#endif - #if HAVE_GOOGLEPHOTOS if (authenicators.contains("google-photos")) { - pluggables += new Publishing.GooglePhotos.Service(resource_directory); + pluggables += new Publishing.GooglePhotos.Service(); } #endif #if HAVE_FLICKR if (authenicators.contains("flickr")) { - pluggables += new FlickrService(resource_directory); + pluggables += new FlickrService(); } #endif #if HAVE_YOUTUBE if (authenicators.contains("youtube")) { - pluggables += new YouTubeService(resource_directory); + pluggables += new YouTubeService(); } #endif #if HAVE_PIWIGO - pluggables += new PiwigoService(resource_directory); + pluggables += new PiwigoService(); #endif #if HAVE_TUMBLR - pluggables += new TumblrService(module_file.get_parent()); + pluggables += new TumblrService(); #endif } @@ -64,7 +56,7 @@ private class ShotwellPublishingCoreServices : Object, Spit.Module { } public unowned string get_id() { - return "org.yorba.shotwell.publishing.core_services"; + return "org.gnome.shotwell.publishing.core_services"; } public unowned Spit.Pluggable[]? get_pluggables() { diff --git a/plugins/shotwell-publishing/tumblr.svg b/plugins/shotwell-publishing/tumblr.svg new file mode 100644 index 0000000..d245dd1 --- /dev/null +++ b/plugins/shotwell-publishing/tumblr.svg @@ -0,0 +1,245 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> + +<svg + version="1.1" + id="Layer_1" + x="0px" + y="0px" + viewBox="0 0 159.30001 159.3" + xml:space="preserve" + sodipodi:docname="tumblr.svg" + width="159.3" + height="159.3" + inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"><defs + id="defs341" /><sodipodi:namedview + id="namedview339" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="4.9089767" + inkscape:cx="12.120652" + inkscape:cy="79.650002" + inkscape:window-width="1920" + inkscape:window-height="1011" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="Layer_1" /> +<style + type="text/css" + id="style248"> + .st0{fill:#FFFFFF;} + .st1{fill:#001935;} + .st2{fill:#231F20;} +</style> +<text + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="text250" + x="-696.69513" + y="-356.28101">Black #000000</text> +<text + id="text260" + x="-1587.3539" + y="-356.28101"><tspan + x="-1587.3539" + y="-356.28101" + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="tspan252">Notes:</tspan><tspan + x="-1587.3539" + y="-246.28101" + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="tspan254">Always use the full wordmark</tspan><tspan + x="-1587.3539" + y="-191.38101" + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="tspan256">unless you’re explicitly speaking to </tspan><tspan + x="-1587.3539" + y="-136.38101" + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="tspan258">or marketing the iOS or Android app </tspan></text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text262" + x="-691.97113" + y="189.9763">iOS icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text264" + x="-696.69513" + y="-111.0251">Wordmark (always use full word)</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text266" + x="-369.3826" + y="189.9763">Android Icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text268" + x="-0.00025388421" + y="189.9763">t icon</text> +<text + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="text270" + x="-696.69513" + y="363.68301">White #000000</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text272" + x="-691.97113" + y="909.94043">iOS icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text274" + x="-696.69513" + y="608.9389">Wordmark (always use full word)</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text276" + x="-369.3826" + y="909.94043">Android Icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text278" + x="-0.00025388421" + y="909.94043">t icon</text> +<text + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="text280" + x="239.25819" + y="-356.28101">Blue #001935</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text282" + x="243.98219" + y="189.9763">iOS icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text284" + x="239.25819" + y="-111.0251">Wordmark (always use full word)</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text286" + x="566.57068" + y="189.9763">Android Icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text288" + x="935.953" + y="189.9763">t icon</text> +<path + class="st1" + d="m 302.9,-147.5 c -24,0 -41.8,-12.3 -41.8,-41.8 v -47.2 h -21.8 v -25.6 c 24,-6.2 34,-26.8 35.1,-44.7 h 24.9 v 40.6 h 29 v 29.7 h -29 v 41.1 c 0,12.3 6.2,16.6 16.1,16.6 h 14.1 v 31.3 z m 89,1.4 c 13.6,0 29.3,-5.9 37.8,-16.4 v 15 h 44.8 V -173 h -11.4 v -93.3 H 409 v 25.9 h 15.4 v 44.9 c 0,3.6 -3,18.7 -23.3,18.7 -13,0 -15.4,-7.8 -15.4,-17.7 v -71.8 h -39.9 v 73.4 c 0,18.3 8.3,46.8 46.1,46.8 z m 96.9,-1.4 h 65.6 V -173 H 541 v -44.4 c 0,-6.5 5.5,-19.7 22.4,-19.7 13.6,0 16.4,8.3 16.4,18.1 v 46 h -13.2 v 25.5 h 65.1 V -173 H 618 v -44.4 c 0,-6.5 5,-19.7 21.9,-19.7 13.7,0 17.1,8.3 17.1,18.1 v 46 h -13 v 25.5 h 64.8 V -173 h -13.3 v -41.5 c 0,-24.4 -9.2,-53.2 -45.6,-53.2 -18,0 -32.7,8.5 -38.9,19 -7.8,-11.9 -19.7,-19 -37.4,-19 -14.1,0 -29.5,6 -37.8,17.4 v -16 h -49.2 v 25.9 h 15.9 V -173 H 489 v 25.5 z m 303,1.4 c 33.2,0 51,-24.9 51,-62.2 0,-35.8 -16.6,-59.3 -50.3,-59.3 -11.9,0 -23.7,4.5 -30.9,10 v -49.2 h -54.8 v 25.7 h 15.9 v 133.6 h 36.5 V -158 c 9.4,7.8 20.5,11.9 32.6,11.9 z m -8.3,-29 c -11.6,0 -23,-7.4 -23,-30.6 0,-28 13.5,-33 22.8,-33 10.8,0 20.4,8.8 20.4,30.5 0,31.4 -14.1,32.4 -20.2,33.1 z m 66.5,27.6 h 66 V -173 h -14 v -133.9 h -54.4 v 25.7 h 15.9 V -173 H 850 Z m 80.4,0 h 64.9 V -173 H 982 v -33.2 c 0,-22.6 15.9,-27.5 28,-27.5 h 16.1 v -34 h -13 c -14.7,0 -28.3,7.1 -34.4,18.5 v -17.2 h -49.4 v 25.9 h 14.3 v 67.4 h -13.1 v 25.6 z" + id="path290" /> +<path + class="st1" + d="m 999.6,159.3 c -24,0 -41.8,-12.3 -41.8,-41.8 V 70.3 H 936 V 44.7 C 960,38.5 970,17.9 971.1,0 H 996 v 40.6 h 29 v 29.7 h -29 v 41.1 c 0,12.3 6.2,16.6 16.1,16.6 h 14.1 v 31.3 z" + id="path292" /> +<g + id="g298"> + <path + class="st1" + d="m 645.1,1.3 c -43.4,0 -78.6,35.1 -78.6,78.3 0,43.2 35.1,78.5 78.6,78.5 43.2,0 78.3,-35.3 78.3,-78.5 C 723.5,36.2 688.3,1.3 645.1,1.3 Z" + id="path294" /> + <path + class="st0" + d="m 651.2,97.3 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 v 18 h -15.3 c -13.7,0 -24,-7.1 -24,-24 V 73.7 H 616.7 V 59 c 13.7,-3.6 19.5,-15.4 20.2,-25.6 h 14.3 v 23.3 h 16.6 v 17 h -16.6 v 23.6" + id="path296" /> +</g> +<g + id="g304"> + <path + class="st1" + d="m 359.9,7.3 h -87 C 257,7.3 244,20.3 244,36.2 v 87 c 0,15.9 13,28.9 28.9,28.9 h 87 c 15.9,0 28.9,-13 28.9,-28.9 v -87 c 0,-15.9 -13,-28.9 -28.9,-28.9 z" + id="path300" /> + <path + class="st0" + d="M 341.3,124.8 H 326 c -13.7,0 -24,-7.1 -24,-24 V 73.7 H 289.5 V 59 C 303.2,55.4 309,43.6 309.7,33.4 H 324 v 23.3 h 16.6 v 17 H 324 v 23.6 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 z" + id="path302" /> +</g> +<path + class="st0" + d="m -633.1,573.9 c -24,0 -41.8,-12.3 -41.8,-41.8 v -47.2 h -21.8 v -25.6 c 24,-6.2 34,-26.8 35.1,-44.7 h 24.9 v 40.6 h 29 v 29.7 h -29 V 526 c 0,12.3 6.2,16.6 16.1,16.6 h 14.1 v 31.3 z m 89.1,1.4 c 13.6,0 29.3,-5.9 37.8,-16.4 v 15 h 44.8 v -25.5 h -11.4 V 455.1 H -527 V 481 h 15.4 v 45 c 0,3.6 -3,18.7 -23.3,18.7 -13,0 -15.4,-7.8 -15.4,-17.7 v -71.8 h -39.9 v 73.4 c 0.1,18.3 8.4,46.7 46.2,46.7 z m 96.9,-1.4 h 65.6 v -25.5 h -13.4 V 504 c 0,-6.5 5.5,-19.7 22.4,-19.7 13.6,0 16.4,8.3 16.4,18.1 v 46 h -13.2 v 25.5 h 65.1 V 548.4 H -318 V 504 c 0,-6.5 5,-19.7 21.9,-19.7 13.7,0 17.1,8.3 17.1,18.1 v 46 h -13 v 25.5 h 64.8 v -25.5 h -13.3 V 507 c 0,-24.4 -9.2,-53.2 -45.6,-53.2 -18,0 -32.7,8.5 -38.9,19 -7.8,-11.9 -19.7,-19 -37.4,-19 -14.1,0 -29.5,6 -37.8,17.4 v -16 h -49.2 v 25.9 h 15.9 v 67.4 H -447 v 25.4 z m 303,1.4 c 33.2,0 51,-24.9 51,-62.2 0,-35.8 -16.6,-59.3 -50.3,-59.3 -11.9,0 -23.7,4.5 -30.9,10 v -49.2 h -54.8 v 25.7 h 15.9 v 133.6 h 36.5 v -10.5 c 9.3,7.9 20.5,11.9 32.6,11.9 z m -8.3,-28.9 c -11.6,0 -23,-7.4 -23,-30.6 0,-28 13.5,-33 22.8,-33 10.8,0 20.4,8.8 20.4,30.5 -0.1,31.4 -14.1,32.4 -20.2,33.1 z m 66.5,27.5 h 66 V 548.4 H -34 V 414.6 h -54.4 v 25.7 h 15.9 V 548.5 H -86 v 25.4 z m 80.4,0 H 59.4 V 548.4 H 46.1 v -33.2 c 0,-22.6 15.9,-27.5 28,-27.5 h 16.1 v -34 h -13 c -14.7,0 -28.3,7.1 -34.4,18.5 V 455 H -6.7 v 25.9 H 7.6 v 67.4 H -5.5 Z" + id="path306" /> +<path + class="st0" + d="m 63.6,880.8 c -24,0 -41.8,-12.3 -41.8,-41.8 V 791.8 H 0 V 766.2 C 24,760 34,739.4 35.1,721.5 H 60 V 762 h 29 v 29.7 H 60 v 41.1 c 0,12.3 6.2,16.6 16.1,16.6 h 14.1 v 31.3 H 63.6 Z" + id="path308" /> +<g + id="g314"> + <path + class="st0" + d="m -290.8,722.7 c -43.4,0 -78.6,35.1 -78.6,78.3 0,43.2 35.1,78.5 78.6,78.5 43.2,0 78.3,-35.3 78.3,-78.5 0,-43.4 -35.1,-78.3 -78.3,-78.3 z" + id="path310" /> + <path + class="st2" + d="m -284.7,818.8 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 v 18 h -15.3 c -13.7,0 -24,-7.1 -24,-24 v -27.1 h -12.5 v -14.7 c 13.7,-3.6 19.5,-15.4 20.2,-25.6 h 14.3 v 23.3 h 16.6 v 17 h -16.6 v 23.6" + id="path312" /> +</g> +<g + id="g320"> + <path + class="st0" + d="m -576.1,728.7 h -87 c -15.9,0 -28.9,13 -28.9,28.9 v 87 c 0,15.9 13,28.9 28.9,28.9 h 87 c 15.9,0 28.9,-13 28.9,-28.9 v -87 c 0.1,-15.9 -13,-28.9 -28.9,-28.9 z" + id="path316" /> + <path + class="st2" + d="m -594.6,846.2 h -15.3 c -13.7,0 -24,-7.1 -24,-24 v -27.1 h -12.5 v -14.7 c 13.7,-3.6 19.5,-15.4 20.2,-25.6 h 14.3 v 23.3 h 16.6 v 17 h -16.6 v 23.6 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 z" + id="path318" /> +</g> +<path + d="m -633.1,-147.5 c -24,0 -41.8,-12.3 -41.8,-41.8 v -47.2 h -21.8 v -25.6 c 24,-6.2 34,-26.8 35.1,-44.7 h 24.9 v 40.6 h 29 v 29.7 h -29 v 41.1 c 0,12.3 6.2,16.6 16.1,16.6 h 14.1 v 31.3 z m 89.1,1.4 c 13.6,0 29.3,-5.9 37.8,-16.4 v 15 h 44.8 V -173 h -11.4 v -93.3 H -527 v 25.9 h 15.4 v 44.9 c 0,3.6 -3,18.7 -23.3,18.7 -13,0 -15.4,-7.8 -15.4,-17.7 v -71.8 h -39.9 v 73.4 c 0.1,18.3 8.4,46.8 46.2,46.8 z m 96.9,-1.4 h 65.6 V -173 h -13.4 v -44.4 c 0,-6.5 5.5,-19.7 22.4,-19.7 13.6,0 16.4,8.3 16.4,18.1 v 46 h -13.2 v 25.5 h 65.1 V -173 H -318 v -44.4 c 0,-6.5 5,-19.7 21.9,-19.7 13.7,0 17.1,8.3 17.1,18.1 v 46 h -13 v 25.5 h 64.8 V -173 h -13.3 v -41.5 c 0,-24.4 -9.2,-53.2 -45.6,-53.2 -18,0 -32.7,8.5 -38.9,19 -7.8,-11.9 -19.7,-19 -37.4,-19 -14.1,0 -29.5,6 -37.8,17.4 v -16 h -49.2 v 25.9 h 15.9 V -173 H -447 v 25.5 z m 303,1.4 c 33.2,0 51,-24.9 51,-62.2 0,-35.8 -16.6,-59.3 -50.3,-59.3 -11.9,0 -23.7,4.5 -30.9,10 v -49.2 h -54.8 v 25.7 h 15.9 v 133.6 h 36.5 V -158 c 9.3,7.8 20.5,11.9 32.6,11.9 z m -8.3,-29 c -11.6,0 -23,-7.4 -23,-30.6 0,-28 13.5,-33 22.8,-33 10.8,0 20.4,8.8 20.4,30.5 -0.1,31.4 -14.1,32.4 -20.2,33.1 z m 66.5,27.6 h 66 V -173 H -34 v -133.9 h -54.4 v 25.7 h 15.9 V -173 H -86 v 25.5 z m 80.4,0 H 59.4 V -173 H 46.1 v -33.2 c 0,-22.6 15.9,-27.5 28,-27.5 h 16.1 v -34 h -13 c -14.7,0 -28.3,7.1 -34.4,18.5 v -17.2 H -6.7 v 25.9 H 7.6 v 67.4 H -5.5 Z" + id="path322" /> +<path + d="m 98.150002,159.3 c -24,0 -41.8,-12.3 -41.8,-41.8 V 70.3 h -21.8 V 44.7 c 24,-6.2 34,-26.8 35.1,-44.7 h 24.9 V 40.6 H 123.55 V 70.3 H 94.550002 v 41.1 c 0,12.3 6.199998,16.6 16.099998,16.6 h 14.1 v 31.3 z" + id="path324" /> +<g + id="g330"> + <path + d="m -290.8,1.3 c -43.4,0 -78.6,35.1 -78.6,78.3 0,43.2 35.1,78.5 78.6,78.5 43.2,0 78.3,-35.3 78.3,-78.5 0,-43.4 -35.1,-78.3 -78.3,-78.3 z" + id="path326" /> + <path + class="st0" + d="m -284.7,97.3 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 v 18 h -15.3 c -13.7,0 -24,-7.1 -24,-24 V 73.7 h -12.5 V 59 c 13.7,-3.6 19.5,-15.4 20.2,-25.6 h 14.3 v 23.3 h 16.6 v 17 h -16.6 v 23.6" + id="path328" /> +</g> +<g + id="g336"> + <path + d="m -576.1,7.3 h -87 c -15.9,0 -28.9,13 -28.9,28.9 v 87 c 0,15.9 13,28.9 28.9,28.9 h 87 c 15.9,0 28.9,-13 28.9,-28.9 v -87 c 0.1,-15.9 -13,-28.9 -28.9,-28.9 z" + id="path332" /> + <path + class="st0" + d="m -594.6,124.8 h -15.3 c -13.7,0 -24,-7.1 -24,-24 V 73.7 h -12.5 V 59 c 13.7,-3.6 19.5,-15.4 20.2,-25.6 h 14.3 v 23.3 h 16.6 v 17 h -16.6 v 23.6 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 z" + id="path334" /> +</g> +</svg> diff --git a/plugins/shotwell-publishing/tumblr_publishing_options_pane.ui b/plugins/shotwell-publishing/tumblr_publishing_options_pane.ui index acc5459..982dc26 100644 --- a/plugins/shotwell-publishing/tumblr_publishing_options_pane.ui +++ b/plugins/shotwell-publishing/tumblr_publishing_options_pane.ui @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> +<!-- Generated with glade 3.22.0 --> <interface domain="shotwell"> <requires lib="gtk+" version="3.14"/> <object class="GtkBox" id="tumblr_pane"> @@ -35,15 +35,16 @@ so changes made here will not display)</property> <property name="can_focus">False</property> <property name="halign">center</property> <property name="valign">center</property> - <property name="row_spacing">8</property> - <property name="column_spacing">24</property> + <property name="row_spacing">6</property> + <property name="column_spacing">12</property> <child> <object class="GtkLabel" id="blog_label"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="label" translatable="yes">Blogs:</property> + <property name="halign">end</property> + <property name="label" translatable="yes">Blogs</property> <property name="use_underline">True</property> - <property name="justify">right</property> + <property name="justify">center</property> <property name="mnemonic_widget">blog_combo</property> </object> <packing> @@ -56,7 +57,7 @@ so changes made here will not display)</property> <property name="visible">True</property> <property name="can_focus">False</property> <property name="halign">end</property> - <property name="label" translatable="yes">Photo _size:</property> + <property name="label" translatable="yes">Photo _size</property> <property name="use_underline">True</property> <property name="mnemonic_widget">size_combo</property> </object> diff --git a/plugins/shotwell-publishing/youtube_publishing_options_pane.ui b/plugins/shotwell-publishing/youtube_publishing_options_pane.ui index 47f75a1..777632e 100644 --- a/plugins/shotwell-publishing/youtube_publishing_options_pane.ui +++ b/plugins/shotwell-publishing/youtube_publishing_options_pane.ui @@ -27,18 +27,17 @@ <object class="GtkBox" id="box1"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="margin_left">40</property> - <property name="margin_right">40</property> + <property name="margin_start">40</property> + <property name="margin_end">40</property> <property name="margin_top">16</property> + <property name="spacing">6</property> <property name="homogeneous">True</property> <child> <object class="GtkLabel" id="privacy_label"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="margin_left">16</property> - <property name="margin_right">16</property> - <property name="xpad">10</property> - <property name="label" translatable="yes">Video privacy _setting:</property> + <property name="halign">end</property> + <property name="label" translatable="yes">Video privacy _setting</property> <property name="use_underline">True</property> </object> <packing> @@ -51,10 +50,6 @@ <object class="GtkComboBoxText" id="privacy_combo"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="margin_left">16</property> - <property name="margin_right">32</property> - <property name="entry_text_column">0</property> - <property name="id_column">1</property> </object> <packing> <property name="expand">False</property> @@ -74,8 +69,8 @@ <object class="GtkBox" id="button_area_box"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="margin_left">112</property> - <property name="margin_right">112</property> + <property name="margin_start">112</property> + <property name="margin_end">112</property> <property name="margin_top">48</property> <property name="margin_bottom">24</property> <property name="spacing">128</property> @@ -86,7 +81,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> - <property name="use_action_appearance">False</property> <property name="use_underline">True</property> </object> <packing> @@ -101,7 +95,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> - <property name="use_action_appearance">False</property> <property name="use_underline">True</property> </object> <packing> diff --git a/plugins/shotwell-transitions/BlindsEffect.vala b/plugins/shotwell-transitions/BlindsEffect.vala index 19af7b3..f1dceb6 100644 --- a/plugins/shotwell-transitions/BlindsEffect.vala +++ b/plugins/shotwell-transitions/BlindsEffect.vala @@ -8,12 +8,12 @@ using Spit; private class BlindsEffectDescriptor : ShotwellTransitionDescriptor { - public BlindsEffectDescriptor(GLib.File resource_directory) { - base(resource_directory); + public BlindsEffectDescriptor() { + base(); } public override unowned string get_id() { - return "org.yorba.shotwell.transitions.blinds"; + return "org.gnome.shotwell.transitions.blinds"; } public override unowned string get_pluggable_name() { diff --git a/plugins/shotwell-transitions/ChessEffect.vala b/plugins/shotwell-transitions/ChessEffect.vala index 9daa670..449f5d3 100644 --- a/plugins/shotwell-transitions/ChessEffect.vala +++ b/plugins/shotwell-transitions/ChessEffect.vala @@ -8,12 +8,12 @@ using Spit; private class ChessEffectDescriptor : ShotwellTransitionDescriptor { - public ChessEffectDescriptor(GLib.File resource_directory) { - base(resource_directory); + public ChessEffectDescriptor() { + base(); } public override unowned string get_id() { - return "org.yorba.shotwell.transitions.chess"; + return "org.gnome.shotwell.transitions.chess"; } public override unowned string get_pluggable_name() { diff --git a/plugins/shotwell-transitions/CircleEffect.vala b/plugins/shotwell-transitions/CircleEffect.vala index b03d8fb..5115c23 100644 --- a/plugins/shotwell-transitions/CircleEffect.vala +++ b/plugins/shotwell-transitions/CircleEffect.vala @@ -8,12 +8,12 @@ using Spit; private class CircleEffectDescriptor : ShotwellTransitionDescriptor { - public CircleEffectDescriptor(GLib.File resource_directory) { - base(resource_directory); + public CircleEffectDescriptor() { + base(); } public override unowned string get_id() { - return "org.yorba.shotwell.transitions.circle"; + return "org.gnome.shotwell.transitions.circle"; } public override unowned string get_pluggable_name() { diff --git a/plugins/shotwell-transitions/CirclesEffect.vala b/plugins/shotwell-transitions/CirclesEffect.vala index 259d801..2e97bfb 100644 --- a/plugins/shotwell-transitions/CirclesEffect.vala +++ b/plugins/shotwell-transitions/CirclesEffect.vala @@ -8,12 +8,12 @@ using Spit; private class CirclesEffectDescriptor : ShotwellTransitionDescriptor { - public CirclesEffectDescriptor(GLib.File resource_directory) { - base(resource_directory); + public CirclesEffectDescriptor() { + base(); } public override unowned string get_id() { - return "org.yorba.shotwell.transitions.circles"; + return "org.gnome.shotwell.transitions.circles"; } public override unowned string get_pluggable_name() { diff --git a/plugins/shotwell-transitions/ClockEffect.vala b/plugins/shotwell-transitions/ClockEffect.vala index 2b7a56a..0dfd353 100644 --- a/plugins/shotwell-transitions/ClockEffect.vala +++ b/plugins/shotwell-transitions/ClockEffect.vala @@ -8,12 +8,12 @@ using Spit; private class ClockEffectDescriptor : ShotwellTransitionDescriptor { - public ClockEffectDescriptor(GLib.File resource_directory) { - base(resource_directory); + public ClockEffectDescriptor() { + base(); } public override unowned string get_id() { - return "org.yorba.shotwell.transitions.clock"; + return "org.gnome.shotwell.transitions.clock"; } public override unowned string get_pluggable_name() { diff --git a/plugins/shotwell-transitions/CrumbleEffect.vala b/plugins/shotwell-transitions/CrumbleEffect.vala index f707bab..ba7235e 100644 --- a/plugins/shotwell-transitions/CrumbleEffect.vala +++ b/plugins/shotwell-transitions/CrumbleEffect.vala @@ -8,12 +8,12 @@ using Spit; private class CrumbleEffectDescriptor : ShotwellTransitionDescriptor { - public CrumbleEffectDescriptor(GLib.File resource_directory) { - base(resource_directory); + public CrumbleEffectDescriptor() { + base(); } public override unowned string get_id() { - return "org.yorba.shotwell.transitions.crumble"; + return "org.gnome.shotwell.transitions.crumble"; } public override unowned string get_pluggable_name() { diff --git a/plugins/shotwell-transitions/FadeEffect.vala b/plugins/shotwell-transitions/FadeEffect.vala index 6330001..d28edb2 100644 --- a/plugins/shotwell-transitions/FadeEffect.vala +++ b/plugins/shotwell-transitions/FadeEffect.vala @@ -8,12 +8,12 @@ using Spit; private class FadeEffectDescriptor : ShotwellTransitionDescriptor { - public FadeEffectDescriptor(GLib.File resource_directory) { - base(resource_directory); + public FadeEffectDescriptor() { + base(); } public override unowned string get_id() { - return "org.yorba.shotwell.transitions.fade"; + return "org.gnome.shotwell.transitions.fade"; } public override unowned string get_pluggable_name() { diff --git a/plugins/shotwell-transitions/SlideEffect.vala b/plugins/shotwell-transitions/SlideEffect.vala index 2b2aca0..e4d6412 100644 --- a/plugins/shotwell-transitions/SlideEffect.vala +++ b/plugins/shotwell-transitions/SlideEffect.vala @@ -8,12 +8,12 @@ using Spit; private class SlideEffectDescriptor : ShotwellTransitionDescriptor { - public SlideEffectDescriptor(GLib.File resource_directory) { - base(resource_directory); + public SlideEffectDescriptor() { + base(); } public override unowned string get_id() { - return "org.yorba.shotwell.transitions.slide"; + return "org.gnome.shotwell.transitions.slide"; } public override unowned string get_pluggable_name() { diff --git a/plugins/shotwell-transitions/SquaresEffect.vala b/plugins/shotwell-transitions/SquaresEffect.vala index b397419..fcb7690 100644 --- a/plugins/shotwell-transitions/SquaresEffect.vala +++ b/plugins/shotwell-transitions/SquaresEffect.vala @@ -8,12 +8,12 @@ using Spit; private class SquaresEffectDescriptor : ShotwellTransitionDescriptor { - public SquaresEffectDescriptor(GLib.File resource_directory) { - base(resource_directory); + public SquaresEffectDescriptor() { + base(); } public override unowned string get_id() { - return "org.yorba.shotwell.transitions.squares"; + return "org.gnome.shotwell.transitions.squares"; } public override unowned string get_pluggable_name() { diff --git a/plugins/shotwell-transitions/StripesEffect.vala b/plugins/shotwell-transitions/StripesEffect.vala index 55d8bd8..4d0282e 100644 --- a/plugins/shotwell-transitions/StripesEffect.vala +++ b/plugins/shotwell-transitions/StripesEffect.vala @@ -8,12 +8,12 @@ using Spit; private class StripesEffectDescriptor : ShotwellTransitionDescriptor { - public StripesEffectDescriptor(GLib.File resource_directory) { - base(resource_directory); + public StripesEffectDescriptor() { + base(); } public override unowned string get_id() { - return "org.yorba.shotwell.transitions.stripes"; + return "org.gnome.shotwell.transitions.stripes"; } public override unowned string get_pluggable_name() { diff --git a/plugins/shotwell-transitions/org.gnome.Shotwell.Transitions.gresource.xml b/plugins/shotwell-transitions/org.gnome.Shotwell.Transitions.gresource.xml index 7545683..fedc742 100644 --- a/plugins/shotwell-transitions/org.gnome.Shotwell.Transitions.gresource.xml +++ b/plugins/shotwell-transitions/org.gnome.Shotwell.Transitions.gresource.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <gresources> <gresource prefix="/org/gnome/Shotwell/Transitions"> - <file>slideshow-plugin.png</file> + <file alias="icons/hicolor/24x24/mimetypes/slideshow-plugin.png">slideshow-plugin.png</file> </gresource> </gresources> diff --git a/plugins/shotwell-transitions/shotwell-transitions.vala b/plugins/shotwell-transitions/shotwell-transitions.vala index 1d2344f..a4288bb 100644 --- a/plugins/shotwell-transitions/shotwell-transitions.vala +++ b/plugins/shotwell-transitions/shotwell-transitions.vala @@ -9,19 +9,17 @@ extern const string _VERSION; private class ShotwellTransitions : Object, Spit.Module { private Spit.Pluggable[] pluggables = new Spit.Pluggable[0]; - public ShotwellTransitions(GLib.File module_file) { - GLib.File resource_directory = module_file.get_parent(); - - pluggables += new FadeEffectDescriptor(resource_directory); - pluggables += new SlideEffectDescriptor(resource_directory); - pluggables += new CrumbleEffectDescriptor(resource_directory); - pluggables += new BlindsEffectDescriptor(resource_directory); - pluggables += new CircleEffectDescriptor(resource_directory); - pluggables += new CirclesEffectDescriptor(resource_directory); - pluggables += new ClockEffectDescriptor(resource_directory); - pluggables += new SquaresEffectDescriptor(resource_directory); - pluggables += new ChessEffectDescriptor(resource_directory); - pluggables += new StripesEffectDescriptor(resource_directory); + public ShotwellTransitions() { + pluggables += new FadeEffectDescriptor(); + pluggables += new SlideEffectDescriptor(); + pluggables += new CrumbleEffectDescriptor(); + pluggables += new BlindsEffectDescriptor(); + pluggables += new CircleEffectDescriptor(); + pluggables += new CirclesEffectDescriptor(); + pluggables += new ClockEffectDescriptor(); + pluggables += new SquaresEffectDescriptor(); + pluggables += new ChessEffectDescriptor(); + pluggables += new StripesEffectDescriptor(); } public unowned string get_module_name() { @@ -33,7 +31,7 @@ private class ShotwellTransitions : Object, Spit.Module { } public unowned string get_id() { - return "org.yorba.shotwell.transitions"; + return "org.gnome.shotwell.transitions"; } public unowned Spit.Pluggable[]? get_pluggables() { @@ -47,20 +45,12 @@ public Spit.Module? spit_entry_point(Spit.EntryPointParams *params) { params->host_max_spit_interface, Spit.CURRENT_INTERFACE); return (params->module_spit_interface != Spit.UNSUPPORTED_INTERFACE) - ? new ShotwellTransitions(params->module_file) : null; + ? new ShotwellTransitions() : null; } // Base class for all transition descriptors in this module public abstract class ShotwellTransitionDescriptor : Object, Spit.Pluggable, Spit.Transitions.Descriptor { - private const string ICON_FILENAME = "slideshow-plugin.png"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - protected ShotwellTransitionDescriptor(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = - Resources.load_from_resource(Resources.RESOURCE_PATH + "/" + ICON_FILENAME); - } + protected ShotwellTransitionDescriptor() {} public int get_pluggable_interface(int min_host_interface, int max_host_interface) { return Spit.negotiate_interfaces(min_host_interface, max_host_interface, @@ -71,16 +61,13 @@ public abstract class ShotwellTransitionDescriptor : Object, Spit.Pluggable, Spi public abstract unowned string get_pluggable_name(); - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); info.authors = "Maxim Kartashev"; info.copyright = _("Copyright 2010 Maxim Kartashev, Copyright 2016 Software Freedom Conservancy Inc."); - info.translators = Resources.TRANSLATORS; - info.version = _VERSION; - info.website_name = Resources.WEBSITE_NAME; - info.website_url = Resources.WEBSITE_URL; - info.is_license_wordwrapped = false; - info.license = Resources.LICENSE; - info.icons = icon_pixbuf_set; + info.icon_name = "slideshow-plugin"; + + return info; } public void activation(bool enabled) { |