diff options
author | Jörg Frings-Fürst <debian@jff.email> | 2025-06-09 10:50:03 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff.email> | 2025-06-09 10:50:03 +0200 |
commit | 62ae476eab4e600d6b7d662735910db0db2c4aa1 (patch) | |
tree | cb3f8e53587ee51cd0201765e6140dcc423ba4b0 | |
parent | e10377c3781fe84f10b3758b35bf403f91e6603a (diff) | |
parent | 361eb97e74a85fd3cbbb67a7a17281c49e2585f4 (diff) |
Merge branch 'feature/upstream' into develop
-rw-r--r-- | .gitlab-ci.yml | 16 | ||||
-rw-r--r-- | NEWS | 31 | ||||
-rw-r--r-- | data/org.gnome.Shotwell.appdata.xml.in | 32 | ||||
-rw-r--r-- | meson.build | 2 | ||||
-rw-r--r-- | plugins/authenticator/shotwell/GoogleAuthenticator.vala | 35 | ||||
-rw-r--r-- | plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala | 4 | ||||
-rw-r--r-- | plugins/shotwell-publishing/PhotosPublisher.vala | 22 | ||||
-rw-r--r-- | plugins/shotwell-publishing/YouTubePublishing.vala | 3 | ||||
-rw-r--r-- | po/id.po | 92 | ||||
-rw-r--r-- | src/Commands.vala | 2 | ||||
-rw-r--r-- | src/PhotoPage.vala | 8 | ||||
-rw-r--r-- | src/PixbufCache.vala | 6 | ||||
-rw-r--r-- | src/camera/ImportPage.vala | 2 | ||||
-rw-r--r-- | src/db/DatabaseTable.vala | 51 | ||||
-rw-r--r-- | src/db/Db.vala | 4 | ||||
-rw-r--r-- | src/db/PhotoTable.vala | 7 | ||||
-rw-r--r-- | src/db/VideoTable.vala | 7 | ||||
-rw-r--r-- | src/photos/PhotoMetadata.vala | 8 | ||||
l--------- | test/DatabaseTable.vala | 1 | ||||
-rw-r--r-- | test/RegexpReplace.vala | 55 | ||||
-rw-r--r-- | test/meson.build | 4 |
21 files changed, 316 insertions, 76 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6ee2899..083e1f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,13 +1,23 @@ -include: 'https://gitlab.gnome.org/GNOME/citemplates/raw/master/flatpak/flatpak_ci_initiative.yml' +include: + - project: 'gnome/citemplates' + file: 'flatpak/flatpak_ci_initiative.yml' + - component: 'gitlab.gnome.org/GNOME/citemplates/release-service@master' + inputs: + dist-job-name: 'flatpak@x86_64' + tarball-artifact-path: "${TARBALL_ARTIFACT_PATH}" + +variables: + FLATPAK_MODULE: "shotwell" + TARBALL_ARTIFACT_PATH: ".flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-dist/${CI_COMMIT_TAG}.tar.xz" + GIT_SUBMODULE_STRATEGY: recursive .vars-devel: variables: BUNDLE: "org.gnome.Shotwell.Devel.flatpak" - GIT_SUBMODULE_STRATEGY: recursive MANIFEST_PATH: "flatpak/org.gnome.Shotwell.json" RUNTIME_REPO: "https://flathub.org/repo/flathub.flatpakrepo" - FLATPAK_MODULE: "shotwell" APP_ID: "org.gnome.Shotwell" + TARBALL_ARTIFACT_PATH: "" flatpak@x86_64: extends: ['.flatpak@x86_64', '.vars-devel'] @@ -1,3 +1,34 @@ +Shotwell 0.32.13 (stable) - 8 Jun 2025 + * Fix broken meson file + +Shotwell 0.32.12 (stable) - 8 Jun 2025 + * Fix broken appdata + +Shotwell 0.32.11 (stable) - 8 Jun 2025 + * Fix wrong "Error -53" dialog when plugging in phone + * Fix issue when Google Photos returns empty response + * Fix assertion when deleting an image in fullscreen view + * Add a work-around comments with "charset=" prefix + * Fix publishing Google Photos + +Bugs fixed in this release: + - https://gitlab.gnome.org/GNOME/shotwell/issues/339 + - https://gitlab.gnome.org/GNOME/shotwell/issues/5095 + - https://gitlab.gnome.org/GNOME/shotwell/issues/5150 + - https://gitlab.gnome.org/GNOME/shotwell/issues/80 + +Merge requests included in this release: + - https://gitlab.gnome.org/GNOME/shotwell/merge_requests/92 + - https://gitlab.gnome.org/GNOME/shotwell/merge_requests/94 + - https://gitlab.gnome.org/GNOME/shotwell/merge_requests/93 + +All contributors to this release: + - Jens Georg <mail@jensge.org> + - Andika Triwidada <atriwidada@gnome.org> + +Added/updated translations: + - id.po, courtesy of Andika Triwidada + Shotwell 0.32.10 (stable) - 27 Oct 2024 * Add information to log and about about running environment * Fix video description upload for YouTube diff --git a/data/org.gnome.Shotwell.appdata.xml.in b/data/org.gnome.Shotwell.appdata.xml.in index 797fce5..122118e 100644 --- a/data/org.gnome.Shotwell.appdata.xml.in +++ b/data/org.gnome.Shotwell.appdata.xml.in @@ -24,7 +24,7 @@ Shotwell supports JPEG, PNG, TIFF, and a variety of RAW file formats. </p> </description> - <url type="homepage">https://wiki.gnome.org/Apps/Shotwell</url> + <url type="homepage">https://shotwell-project.org</url> <content_rating type="oars-1.0" /> <screenshots> <screenshot type="default"> @@ -56,6 +56,36 @@ <url type="bugtracker">https://gitlab.gnome.org/GNOME/shotwell/issues</url> <releases> + <release version="0.32.13" date="2025-06-08" urgency="low" type="stable"> + <description> + <ul> + <li>Fixes an issue with the meson file in 0.32.12</li> + </ul> + </description> + </release> + <release version="0.32.12" date="2025-06-08" urgency="low" type="stable"> + <description> + <ul> + <li>Fixes an issue with appdata in 0.32.11</li> + </ul> + </description> + </release> + <release version="0.32.12" date="2025-06-08" urgency="high" type="stable"> + <description> + <ul> + <li>Fixes publishing to Google Photos</li> + <li>Fixes charset= prefixes in photo comments</li> + <li>Fixes a crash when deleting a photo in fullscreen view</li> + </ul> + </description> + </release> + <release version="0.32.10" date="2024-10-18" urgency="high" type="stable"> + <description> + <ul> + <li>Fixes a crash when editing the photo</li> + </ul> + </description> + </release> <release version="0.32.9" date="2024-09-15" urgency="high" type="stable"> <description> <ul> diff --git a/meson.build b/meson.build index 9eb58cf..e745a29 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('shotwell', ['vala', 'c'], - version : '0.32.10', + version : '0.32.13', meson_version : '>= 0.59.0', default_options : ['buildtype=debugoptimized']) diff --git a/plugins/authenticator/shotwell/GoogleAuthenticator.vala b/plugins/authenticator/shotwell/GoogleAuthenticator.vala index 5a0d934..1fe2448 100644 --- a/plugins/authenticator/shotwell/GoogleAuthenticator.vala +++ b/plugins/authenticator/shotwell/GoogleAuthenticator.vala @@ -64,7 +64,7 @@ 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; + private string[] scopes = null; // Prepare for multiple user accounts private string accountname = "default"; @@ -74,12 +74,12 @@ namespace Publishing.Authenticator.Shotwell.Google { private string welcome_message = null; private Secret.Schema? schema = null; - public Google(string scope, + public Google(string[] scopes, string welcome_message, Spit.Publishing.PluginHost host) { this.host = host; this.params = new GLib.HashTable<string, Variant>(str_hash, str_equal); - this.scope = scope; + this.scopes = scopes; this.session = new Session(); this.welcome_message = welcome_message; this.schema = new Secret.Schema(PASSWORD_SCHEME, Secret.SchemaFlags.NONE, @@ -93,7 +93,7 @@ namespace Publishing.Authenticator.Shotwell.Google { 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); + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", get_scopes()); } catch (Error err) { critical("Failed to lookup refresh_token from password store: %s", err.message); } @@ -106,6 +106,10 @@ namespace Publishing.Authenticator.Shotwell.Google { this.do_show_service_welcome_pane(); } + public string get_scopes(string separator=",") { + return string.joinv(separator, this.scopes); + } + public bool can_logout() { return true; } @@ -119,9 +123,9 @@ namespace Publishing.Authenticator.Shotwell.Google { 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); + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", get_scopes()); } catch (Error err) { - critical("Failed to remove password for scope %s: %s", this.scope, err.message); + critical("Failed to remove password for scope %s: %s", get_scopes(), err.message); } } @@ -147,7 +151,7 @@ namespace Publishing.Authenticator.Shotwell.Google { "response_type=code&" + "client_id=" + OAUTH_CLIENT_ID + "&" + "redirect_uri=" + GLib.Uri.escape_string(OAUTH_CALLBACK_URI, null) + "&" + - "scope=" + GLib.Uri.escape_string(this.scope, null) + "+" + + "scope=" + GLib.Uri.escape_string(get_scopes(" "), null) + "+" + GLib.Uri.escape_string("https://www.googleapis.com/auth/userinfo.profile", null) + "&" + "state=connect&" + "access_type=offline&" + @@ -155,18 +159,25 @@ namespace Publishing.Authenticator.Shotwell.Google { var auth_callback = new AuthCallback(); string? web_auth_code = null; + auth_callback.auth.connect((prm) => { if ("code" in prm) { web_auth_code = prm["code"]; } + if ("scope" in prm) { + debug("Effective scopes as returned from login: %s", prm["scope"]); + } do_hosted_web_authentication.callback(); }); host.register_auth_callback(REVERSE_CLIENT_ID, auth_callback); try { + debug("Launching external authentication on URI %s", user_authorization_url); AppInfo.launch_default_for_uri(user_authorization_url, null); host.install_login_wait_pane(); yield; + // FIXME throw error missing scopes + yield do_get_access_tokens(web_auth_code); } catch (Error err) { host.post_error(err); @@ -315,12 +326,12 @@ namespace Publishing.Authenticator.Shotwell.Google { assert(session.is_authenticated()); try { Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, - "Shotwell publishing (Google account scope %s@%s)".printf(this.accountname, this.scope), + "Shotwell publishing (Google account scope %s@%s)".printf(this.accountname, get_scopes()), session.refresh_token, null, SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), - SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope); + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", get_scopes()); } catch (Error err) { - critical("Failed to look up password for scope %s: %s", this.scope, err.message); + critical("Failed to look up password for scope %s: %s", get_scopes(), err.message); } this.authenticated(); @@ -352,9 +363,9 @@ namespace Publishing.Authenticator.Shotwell.Google { 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); + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", get_scopes()); } catch (Error err) { - critical("Failed to remove password for accountname@scope %s@%s: %s", this.accountname, this.scope, err.message); + critical("Failed to remove password for accountname@scope %s@%s: %s", this.accountname, get_scopes(), err.message); } Idle.add (() => { this.authenticate(); return false; }); diff --git a/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala b/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala index 01fa3c3..c289006 100644 --- a/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala +++ b/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala @@ -27,11 +27,11 @@ namespace Publishing.Authenticator { case "flickr": return new Shotwell.Flickr.Flickr(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); + 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.appendonly", "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata"}, _("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/shotwell-publishing/PhotosPublisher.vala b/plugins/shotwell-publishing/PhotosPublisher.vala index b592317..67c3ecb 100644 --- a/plugins/shotwell-publishing/PhotosPublisher.vala +++ b/plugins/shotwell-publishing/PhotosPublisher.vala @@ -111,6 +111,7 @@ internal class PublishingParameters { } private class MediaCreationTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + // SCOPE: photoslibrary.appendonly private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate"; private string[] upload_tokens; private string[] titles; @@ -154,6 +155,7 @@ private class MediaCreationTransaction : Publishing.RESTSupport.GooglePublisher. } private class AlbumCreationTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + // SCOPE: photoslibrary.appendonly private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/albums"; private string title; @@ -179,6 +181,7 @@ private class AlbumCreationTransaction : Publishing.RESTSupport.GooglePublisher. } private class AlbumDirectoryTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + // SCOPE: photoslibrary.readonly.appcreateddata private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/albums"; public AlbumDirectoryTransaction(Publishing.RESTSupport.GoogleSession session, string? token) { @@ -244,8 +247,15 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { if (!is_running()) return; + var json = Json.from_string (txn.get_response()); var object = json.get_object (); + // Work-around for Google sometimes sending an empty JSON object '{}' instead of + // not setting the nextPageToken on the previous page + if (object.get_size() == 0) { + break; + } + if (!object.has_member ("albums")) { throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE("Album fetch did not contain expected data"); } @@ -279,7 +289,10 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { debug("EVENT: fetching album information failed; response = '%s'.", txn.get_response()); - if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { + if (txn.get_status_code() == 403) { + debug("Lacking permission to download album list, showing publishing options anyway"); + show_publishing_options_pane(); + } else if (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 @@ -345,10 +358,13 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { yield do_upload(); } catch (Error err) { - debug("EVENT: creating album failed; response = '%s'.", + debug("EVENT: creating album failed; status = '%u', response = '%s'.", txn.get_status_code(), txn.get_response()); - if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { + if (txn.get_status_code() == 403) { + get_host().install_static_message_pane(_("Could not create album, Shotwell is lacking permission to do so. Please re-authenticate and grant Shotwell the required permission to create new media and albums"), + Spit.Publishing.PluginHost.ButtonMode.CLOSE); + } else if (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 diff --git a/plugins/shotwell-publishing/YouTubePublishing.vala b/plugins/shotwell-publishing/YouTubePublishing.vala index 88218cc..13e4afd 100644 --- a/plugins/shotwell-publishing/YouTubePublishing.vala +++ b/plugins/shotwell-publishing/YouTubePublishing.vala @@ -44,9 +44,6 @@ public class YouTubeService : Object, Spit.Pluggable, Spit.Publishing.Service { namespace Publishing.YouTube { -private const string DEVELOPER_KEY = - "AIzaSyB6hLnm0n5j8Y6Bkvh9bz3i8ADM2bJdYeY"; - private enum PrivacySetting { PUBLIC, UNLISTED, @@ -18,8 +18,8 @@ msgid "" msgstr "" "Project-Id-Version: shotwell shotwell-0.32\n" "Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/shotwell/issues\n" -"POT-Creation-Date: 2024-09-05 13:47+0000\n" -"PO-Revision-Date: 2024-09-08 18:40+0700\n" +"POT-Creation-Date: 2024-10-27 17:51+0000\n" +"PO-Revision-Date: 2024-10-28 20:23+0700\n" "Last-Translator: Andika Triwidada <andika@gmail.com>\n" "Language-Team: Indonesian <gnome@i15n.org>\n" "Language: id\n" @@ -27,7 +27,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Poedit 3.2.2\n" +"X-Generator: Poedit 3.5\n" #: data/gsettings/org.gnome.shotwell-extras.gschema.xml:6 #: data/gsettings/org.gnome.shotwell.gschema.xml:438 @@ -2298,7 +2298,7 @@ msgstr "" msgid "Visit the Shotwell home page" msgstr "Kunjungi laman serambi Shotwell" -#: plugins/common/Resources.vala:31 src/AppWindow.vala:599 +#: plugins/common/Resources.vala:31 src/AppWindow.vala:614 #: src/plugins/SpitInterfaces.vala:177 msgid "translator-credits" msgstr "" @@ -2372,7 +2372,7 @@ msgstr "" #: plugins/common/RESTSupport.vala:574 #: plugins/shotwell-publishing/TumblrPublishing.vala:567 -#: plugins/shotwell-publishing/YoutubeUploader.vala:62 +#: plugins/shotwell-publishing/YoutubeUploader.vala:65 msgid "A temporary file needed for publishing is unavailable" msgstr "Berkas sementara yang diperlukan untuk penerbitan tidak tersedia" @@ -2400,7 +2400,7 @@ msgstr "_Keluar" #: plugins/shotwell-publishing/google_photos_publishing_options_pane.ui:195 #: plugins/shotwell-publishing/tumblr_publishing_options_pane.ui:122 #: plugins/shotwell-publishing/youtube_publishing_options_pane.ui:94 -#: src/CollectionPage.vala:82 src/PhotoPage.vala:2495 +#: src/CollectionPage.vala:82 src/PhotoPage.vala:2496 msgid "_Publish" msgstr "_Terbitkan" @@ -2774,17 +2774,17 @@ msgid "You are logged into YouTube as %s." msgstr "Anda masuk ke YouTube sebagai %s." #: plugins/shotwell-publishing/YouTubePublishing.vala:333 -msgid "Public listed" -msgstr "Publik terdaftar" +msgid "Public" +msgstr "Publik" #: plugins/shotwell-publishing/YouTubePublishing.vala:334 -msgid "Public unlisted" -msgstr "Publik tak terdaftar" - -#: plugins/shotwell-publishing/YouTubePublishing.vala:335 msgid "Private" msgstr "Privat" +#: plugins/shotwell-publishing/YouTubePublishing.vala:335 +msgid "unlisted" +msgstr "tidak terdaftar" + #: plugins/shotwell-transitions/BlindsEffect.vala:20 msgid "Blinds" msgstr "Tirai" @@ -2852,22 +2852,22 @@ msgstr "Gagal membuat direktori data %s: %s" msgid "Pictures" msgstr "Foto" -#: src/AppDirs.vala:211 +#: src/AppDirs.vala:238 #, c-format msgid "Unable to create temporary directory %s: %s" msgstr "Gagal membuat direktori temporer %s: %s" -#: src/AppDirs.vala:229 src/AppDirs.vala:260 +#: src/AppDirs.vala:256 src/AppDirs.vala:287 #, c-format msgid "Unable to create data subdirectory %s: %s" msgstr "Gagal membuat direktori data %s: %s" -#: src/AppDirs.vala:242 +#: src/AppDirs.vala:269 #, c-format msgid "Could not make directory %s writable" msgstr "Tidak bisa membuat direktori %s dapat ditulisi" -#: src/AppDirs.vala:245 +#: src/AppDirs.vala:272 #, c-format msgid "Could not make directory %s writable: %s" msgstr "Tidak bisa membuat direktori %s dapat ditulisi: %s" @@ -2897,21 +2897,21 @@ msgstr "" "\n" "%s" -#: src/AppWindow.vala:596 +#: src/AppWindow.vala:611 msgid "Visit the Shotwell web site" msgstr "Kunjungi situs web Shotwell" -#: src/AppWindow.vala:609 +#: src/AppWindow.vala:624 #, c-format msgid "Unable to display help: %s" msgstr "Pedoman penggunaan tidak dapat ditampilkan: %s" -#: src/AppWindow.vala:617 +#: src/AppWindow.vala:632 #, c-format msgid "Unable to navigate to bug database: %s" msgstr "Gagal menavigasi basis data bug: %s" -#: src/AppWindow.vala:625 +#: src/AppWindow.vala:640 #, c-format msgid "Unable to display FAQ: %s" msgstr "Gagal menampilkan FAQ: %s" @@ -3134,12 +3134,12 @@ msgid "No photos/videos found which match the current filter" msgstr "Tidak ditemukan foto/video yang cocok dengan penyaring saat ini" #: src/CollectionPage.vala:80 src/direct/DirectPhotoPage.vala:86 -#: src/PhotoPage.vala:2490 src/Resources.vala:137 +#: src/PhotoPage.vala:2491 src/Resources.vala:137 msgid "_Print" msgstr "_Cetak" #: src/CollectionPage.vala:84 src/direct/DirectPhotoPage.vala:91 -#: src/PhotoPage.vala:2500 src/Resources.vala:187 +#: src/PhotoPage.vala:2501 src/Resources.vala:187 msgid "Set as _Desktop Background" msgstr "Atur sebagai Latar _Destop" @@ -3692,7 +3692,7 @@ msgstr[0] "Ini akan menghapus wajah \"%s\" dari %d foto. Teruskan?" msgid "Export Video" msgstr "Ekspor Video" -#: src/Dialogs.vala:70 src/PhotoPage.vala:3025 +#: src/Dialogs.vala:70 src/PhotoPage.vala:3026 msgctxt "Dialog Title" msgid "Export Photo" msgstr "Ekspor Foto" @@ -4499,80 +4499,80 @@ msgstr "Tutup penghilang mata merah" msgid "Remove any red-eye effects in the selected region" msgstr "Hilangkan efek mata merah pada area yang diinginkan" -#: src/editing_tools/EditingTools.vala:2229 +#: src/editing_tools/EditingTools.vala:2228 #: src/editing_tools/StraightenTool.vala:102 msgid "_Reset" msgstr "_Reset" #. fit both on the top line, emit and move on -#: src/editing_tools/EditingTools.vala:2243 src/Properties.vala:448 +#: src/editing_tools/EditingTools.vala:2242 src/Properties.vala:448 #: src/Properties.vala:452 src/Properties.vala:459 msgid "Exposure:" msgstr "Bukaan:" -#: src/editing_tools/EditingTools.vala:2252 +#: src/editing_tools/EditingTools.vala:2251 msgid "Contrast:" msgstr "Kontras:" -#: src/editing_tools/EditingTools.vala:2261 +#: src/editing_tools/EditingTools.vala:2260 msgid "Saturation:" msgstr "Saturasi:" -#: src/editing_tools/EditingTools.vala:2270 +#: src/editing_tools/EditingTools.vala:2269 msgid "Tint:" msgstr "Tint:" -#: src/editing_tools/EditingTools.vala:2280 +#: src/editing_tools/EditingTools.vala:2279 msgid "Temperature:" msgstr "Suhu:" -#: src/editing_tools/EditingTools.vala:2289 +#: src/editing_tools/EditingTools.vala:2288 msgid "Shadows:" msgstr "Kegelapan:" #. FIXME: Hack to make the slider the same length as the other. Find out why it is aligned #. Differently (probably because it only has positive values) -#: src/editing_tools/EditingTools.vala:2300 +#: src/editing_tools/EditingTools.vala:2299 msgid "Highlights:" msgstr "Highlight:" -#: src/editing_tools/EditingTools.vala:2357 +#: src/editing_tools/EditingTools.vala:2356 msgid "Reset Colors" msgstr "Reset Warna" -#: src/editing_tools/EditingTools.vala:2357 +#: src/editing_tools/EditingTools.vala:2356 msgid "Reset all color adjustments to original" msgstr "Kembalikan warna ke nilai aslinya" -#: src/editing_tools/EditingTools.vala:2716 +#: src/editing_tools/EditingTools.vala:2715 msgid "Temperature" msgstr "Suhu" -#: src/editing_tools/EditingTools.vala:2728 +#: src/editing_tools/EditingTools.vala:2727 msgid "Tint" msgstr "Tint" -#: src/editing_tools/EditingTools.vala:2740 +#: src/editing_tools/EditingTools.vala:2739 msgid "Contrast" msgstr "Kontras" -#: src/editing_tools/EditingTools.vala:2754 +#: src/editing_tools/EditingTools.vala:2753 msgid "Saturation" msgstr "Saturasi" -#: src/editing_tools/EditingTools.vala:2767 +#: src/editing_tools/EditingTools.vala:2766 msgid "Exposure" msgstr "Bukaan" -#: src/editing_tools/EditingTools.vala:2780 +#: src/editing_tools/EditingTools.vala:2779 msgid "Shadows" msgstr "Kegelapan" -#: src/editing_tools/EditingTools.vala:2793 +#: src/editing_tools/EditingTools.vala:2792 msgid "Highlights" msgstr "Highlight" -#: src/editing_tools/EditingTools.vala:2803 +#: src/editing_tools/EditingTools.vala:2802 msgid "Contrast Expansion" msgstr "Naikkan Kontras" @@ -4967,22 +4967,22 @@ msgstr "Foto sebelumnya" msgid "Next photo" msgstr "Foto berikutnya" -#: src/PhotoPage.vala:1867 +#: src/PhotoPage.vala:1868 #, c-format msgid "Photo source file missing: %s" msgstr "Sumber foto asli hilang: %s" -#: src/PhotoPage.vala:2900 +#: src/PhotoPage.vala:2901 msgctxt "Dialog Title" msgid "Remove From Library" msgstr "Hapus Dari Album" -#: src/PhotoPage.vala:2901 +#: src/PhotoPage.vala:2902 msgctxt "Dialog Title" msgid "Removing Photo From Library" msgstr "Menghapus Foto Dari Album" -#: src/PhotoPage.vala:3045 +#: src/PhotoPage.vala:3046 #, c-format msgid "Unable to export %s: %s" msgstr "Proses ekspor %s gagal: %s" @@ -5044,7 +5044,7 @@ msgid "WebP" msgstr "WebP" #. TRANSLATORS: "modified" here is part of a file name that was changed with another image tool outside of Shotwell. Note that there are potential issues with UTF-8 characters -#: src/Photo.vala:3812 +#: src/Photo.vala:3810 msgid "modified" msgstr "berubah" diff --git a/src/Commands.vala b/src/Commands.vala index 76aecb4..25bdbc2 100644 --- a/src/Commands.vala +++ b/src/Commands.vala @@ -321,7 +321,7 @@ public abstract class MultipleDataSourceCommand : PageCommand { private void on_source_destroyed(DataSource source) { // as with SingleDataSourceCommand, too risky to selectively remove commands from the stack, // although this could be reconsidered in the future - if (source_list.contains(source)) + if (source_list.contains(source) && get_command_manager() != null) get_command_manager().reset(); } diff --git a/src/PhotoPage.vala b/src/PhotoPage.vala index a28ab44..3ab0f6b 100644 --- a/src/PhotoPage.vala +++ b/src/PhotoPage.vala @@ -835,7 +835,9 @@ public abstract class EditingHostPage : SinglePhotoPage { photo_changing(photo); DataView view = get_view().get_view_for_source(photo); - assert(view != null); + if (view == null) { + return; + } // Select photo. get_view().unselect_all(); @@ -1255,6 +1257,10 @@ public abstract class EditingHostPage : SinglePhotoPage { } private void quick_update_pixbuf() { + if (get_photo() == null) { + return; + } + Gdk.Pixbuf? pixbuf = cache.get_ready_pixbuf(get_photo()); if (pixbuf != null) { set_pixbuf(pixbuf, get_photo().get_dimensions()); diff --git a/src/PixbufCache.vala b/src/PixbufCache.vala index 6ff740e..76fdbd3 100644 --- a/src/PixbufCache.vala +++ b/src/PixbufCache.vala @@ -120,7 +120,11 @@ public class PixbufCache : Object { } // This call never blocks. Returns null if the pixbuf is not present. - public Gdk.Pixbuf? get_ready_pixbuf(Photo photo) { + public Gdk.Pixbuf? get_ready_pixbuf(Photo? photo) { + if (photo == null) { + return null; + } + return get_cached(photo); } diff --git a/src/camera/ImportPage.vala b/src/camera/ImportPage.vala index 463317b..20a6a58 100644 --- a/src/camera/ImportPage.vala +++ b/src/camera/ImportPage.vala @@ -1086,7 +1086,7 @@ public class ImportPage : CheckerboardPage { progress_bar.set_text(""); progress_bar.visible = false; - try_refreshing_camera(true); + Timeout.add_seconds(3, () => { try_refreshing_camera(true); return false; }); } private void clear_all_import_sources() { diff --git a/src/db/DatabaseTable.vala b/src/db/DatabaseTable.vala index dea797a..be45e5e 100644 --- a/src/db/DatabaseTable.vala +++ b/src/db/DatabaseTable.vala @@ -29,10 +29,61 @@ public abstract class DatabaseTable { public string table_name = null; + static Gee.HashMap<string, Regex> regex_map; + + private static void regexp_replace(Sqlite.Context context, Sqlite.Value[] args) { + var pattern = args[0].to_text(); + if (pattern == null) { + context.result_error("Missing regular expression", Sqlite.ERROR); + return; + } + + var text = args[1].to_text(); + if (text == null) { + return; + } + + var replacement = args[2].to_text(); + if (replacement == null) { + context.result_value(args[1]); + return; + } + + Regex re; + if (regex_map == null) { + regex_map = new Gee.HashMap<string, Regex>(); + } + if (regex_map.has_key(pattern)) { + re = regex_map[pattern]; + } else { + try { + re = new Regex(pattern, RegexCompileFlags.DEFAULT, RegexMatchFlags.DEFAULT); + regex_map[pattern] = re; + } catch (Error err) { + context.result_error("Invalid pattern: %s".printf(err.message), Sqlite.ERROR); + return; + } + } + + try { + var result = re.replace(text, -1, 0, replacement, RegexMatchFlags.DEFAULT); + context.result_text(result); + } catch (Error err) { + context.result_error("Replacement failed: %s".printf(err.message), Sqlite.ERROR); + } + } + + [CCode (cname="SQLITE_DETERMINISTIC", cheader_filename="sqlite3.h")] + extern static int SQLITE_DETERMINISTIC; + private static void prepare_db(string filename) { // Open DB. int res = Sqlite.Database.open_v2(filename, out db, Sqlite.OPEN_READWRITE | Sqlite.OPEN_CREATE, null); + + db.create_function("regexp_replace", 3, Sqlite.UTF8 | SQLITE_DETERMINISTIC, null, + DatabaseTable.regexp_replace, null, null); + if (res != Sqlite.OK) AppWindow.panic(_("Unable to open/create photo database %s: error code %d").printf(filename, res)); diff --git a/src/db/Db.vala b/src/db/Db.vala index 5072967..7f76f2d 100644 --- a/src/db/Db.vala +++ b/src/db/Db.vala @@ -55,6 +55,10 @@ public VerifyResult verify_database(out string app_version, out int schema_versi if (result != VerifyResult.OK) return result; } + + PhotoTable.clean_comments(); + VideoTable.clean_comments(); + return VerifyResult.OK; } diff --git a/src/db/PhotoTable.vala b/src/db/PhotoTable.vala index 420b209..d74cbd1 100644 --- a/src/db/PhotoTable.vala +++ b/src/db/PhotoTable.vala @@ -1123,6 +1123,13 @@ public class PhotoTable : DatabaseTable { throw_error("PhotoTable.upgrade_for_unset_timestamp", res); } } + + public static void clean_comments() throws DatabaseError { + var result = db.exec("UPDATE PhotoTable SET comment = regexp_replace('^charset=\\w+\\s*', comment, '') WHERE comment like 'charset=%'"); + if (result != Sqlite.OK) { + throw_error("Cleaning comments from charset", result); + } + } } diff --git a/src/db/VideoTable.vala b/src/db/VideoTable.vala index 8af1278..67c50ba 100644 --- a/src/db/VideoTable.vala +++ b/src/db/VideoTable.vala @@ -480,5 +480,12 @@ public class VideoTable : DatabaseTable { } } + public static void clean_comments() throws DatabaseError { + var result = db.exec("UPDATE VideoTable SET comment = regexp_replace('^charset=\\w+\\s*', comment, '') WHERE comment like 'charset=%'"); + if (result != Sqlite.OK) { + throw_error("Cleaning comments from charset", result); + } + } + } diff --git a/src/photos/PhotoMetadata.vala b/src/photos/PhotoMetadata.vala index 3bf77d6..0624b41 100644 --- a/src/photos/PhotoMetadata.vala +++ b/src/photos/PhotoMetadata.vala @@ -1043,7 +1043,13 @@ public class PhotoMetadata : MediaMetadata { }; public override string? get_comment() { - return get_first_string_interpreted (COMMENT_TAGS); + var comment = get_first_string_interpreted (COMMENT_TAGS); + try { + var re = new Regex("^charset=\\w+\\s*"); + return re.replace(comment, -1, 0, "", RegexMatchFlags.DEFAULT); + } catch (Error err) { + return comment; + } } public void set_comment(string? comment, diff --git a/test/DatabaseTable.vala b/test/DatabaseTable.vala new file mode 120000 index 0000000..e860d3b --- /dev/null +++ b/test/DatabaseTable.vala @@ -0,0 +1 @@ +../src/db/DatabaseTable.vala
\ No newline at end of file diff --git a/test/RegexpReplace.vala b/test/RegexpReplace.vala new file mode 100644 index 0000000..b9aaac7 --- /dev/null +++ b/test/RegexpReplace.vala @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2025 Jens Georg <mail@jensge.org> + +namespace Db { + public static unowned string IN_MEMORY_NAME = ":memory:"; +} + +class AppWindow { + public static void panic(string args) {} +} + +// Helper class to expose protected members +abstract class TestDb : DatabaseTable { + public static unowned Sqlite.Database get_db() { + DatabaseTable.init(Db.IN_MEMORY_NAME); + return DatabaseTable.db; + } +} + +void main(string[] args) { + GLib.Intl.setlocale(LocaleCategory.ALL, ""); + Test.init(ref args); + Test.add_func("/functional/regexp_replace", () => { + unowned Sqlite.Database db = TestDb.get_db(); + + { + Sqlite.Statement s; + assert(db.prepare_v2("SELECT regexp_replace('^charset=\\w+\\s*', 'charset=Unicode This is a comment, äöü, some encoding perhjaps', '')", -1, out s) == Sqlite.OK); + assert(s.step() == Sqlite.ROW); + assert(s.column_text(0) == "This is a comment, äöü, some encoding perhjaps"); + } + + { + Sqlite.Statement s; + assert(db.prepare_v2("SELECT regexp_replace('^charset=\\w+\\s*', 'test charset=Unicode This is a comment, äöü, some encoding perhjaps', '')", -1, out s) == Sqlite.OK); + assert(s.step() == Sqlite.ROW); + assert(s.column_text(0) == "test charset=Unicode This is a comment, äöü, some encoding perhjaps"); + } + }); + Test.add_func("/functional/catch_invalid_regexp", () => { + unowned Sqlite.Database db = TestDb.get_db(); + assert(db.exec("regexp_replace('charset=\\X*', '', '')") == Sqlite.ERROR); + assert(db.exec("regexp_replace(NULL, '', '')") == Sqlite.ERROR); + assert(db.exec("regexp_replace('pattern', NULL, '')") == Sqlite.ERROR); + + Sqlite.Statement s; + + // NULL replacement should return the original text, even if it matches + assert(db.prepare_v2("SELECT regexp_replace('test\\s+', 'test some pattern', NULL)", -1, out s) == Sqlite.OK); + assert(s.step() == Sqlite.ROW); + assert(s.column_text(0) == "test some pattern"); + + }); + Test.run(); +}
\ No newline at end of file diff --git a/test/meson.build b/test/meson.build index 5319cfc..54ed3dc 100644 --- a/test/meson.build +++ b/test/meson.build @@ -20,6 +20,10 @@ jfif_support_test = executable('jfif-support-test', c_args : ['-DTEST_DATA_DIR="@0@"'.format(meson.current_source_dir())] ) +regexp_replace_test = executable('regexp-replace-test', + ['RegexpReplace.vala', 'DatabaseTable.vala'], + dependencies: [gee, gio, sqlite]) test('natural-collate', natural_collate_test) test('jfif-support', jfif_support_test) +test('regexp-replace', regexp_replace_test)
\ No newline at end of file |