summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff.email>2025-06-09 10:50:03 +0200
committerJörg Frings-Fürst <debian@jff.email>2025-06-09 10:50:03 +0200
commit62ae476eab4e600d6b7d662735910db0db2c4aa1 (patch)
treecb3f8e53587ee51cd0201765e6140dcc423ba4b0
parente10377c3781fe84f10b3758b35bf403f91e6603a (diff)
parent361eb97e74a85fd3cbbb67a7a17281c49e2585f4 (diff)
Merge branch 'feature/upstream' into develop
-rw-r--r--.gitlab-ci.yml16
-rw-r--r--NEWS31
-rw-r--r--data/org.gnome.Shotwell.appdata.xml.in32
-rw-r--r--meson.build2
-rw-r--r--plugins/authenticator/shotwell/GoogleAuthenticator.vala35
-rw-r--r--plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala4
-rw-r--r--plugins/shotwell-publishing/PhotosPublisher.vala22
-rw-r--r--plugins/shotwell-publishing/YouTubePublishing.vala3
-rw-r--r--po/id.po92
-rw-r--r--src/Commands.vala2
-rw-r--r--src/PhotoPage.vala8
-rw-r--r--src/PixbufCache.vala6
-rw-r--r--src/camera/ImportPage.vala2
-rw-r--r--src/db/DatabaseTable.vala51
-rw-r--r--src/db/Db.vala4
-rw-r--r--src/db/PhotoTable.vala7
-rw-r--r--src/db/VideoTable.vala7
-rw-r--r--src/photos/PhotoMetadata.vala8
l---------test/DatabaseTable.vala1
-rw-r--r--test/RegexpReplace.vala55
-rw-r--r--test/meson.build4
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']
diff --git a/NEWS b/NEWS
index dd7a612..158ccdb 100644
--- a/NEWS
+++ b/NEWS
@@ -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,
diff --git a/po/id.po b/po/id.po
index 0c9f23d..12cbc06 100644
--- a/po/id.po
+++ b/po/id.po
@@ -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