diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 0b4713971..084a540ed 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -7,8 +7,6 @@ run-name: ${{ github.event.release.tag_name || github.ref_name }} Web on: release: types: [published] - discussion: - types: [created, edited, deleted, transferred, pinned, unpinned, labeled, unlabeled, locked, unlocked, category_changed, answered, unanswered] workflow_dispatch: permissions: @@ -81,11 +79,25 @@ jobs: echo '[]' > dist/supporters.json; exit 0 fi curl -sf -H "Authorization: Bearer $OCS_PATREON_TOKEN" \ - "https://www.patreon.com/api/oauth2/v2/campaigns/$CID/members?fields%5Bmember%5D=full_name,patron_status,currently_entitled_amount_cents&page%5Bcount%5D=200" \ - | jq -c '[.data[] - | select(.attributes.patron_status=="active_patron") - | select(.attributes.currently_entitled_amount_cents>0) - | {name: .attributes.full_name, cents: .attributes.currently_entitled_amount_cents}] + "https://www.patreon.com/api/oauth2/v2/campaigns/$CID/members?include=pledge_history&fields%5Bmember%5D=full_name,patron_status,currently_entitled_amount_cents&fields%5Bpledge-event%5D=amount_cents,date,payment_status&page%5Bcount%5D=200" \ + | jq -c ' + (.included // [] + | map(select(.type == "pledge-event")) + | map({key: .id, value: .attributes}) + | from_entries) as $events + | [.data[] + | select(.attributes.patron_status == "active_patron") + | . as $member + | ([($member.relationships.pledge_history.data // [])[] + | $events[.id] + | select(.payment_status == "Paid") + | select(.amount_cents > 0)] + | sort_by(.date) + | last) as $last_paid + | {name: ((.attributes.full_name // "") | gsub("^\\s+|\\s+$"; "")), + cents: ($last_paid.amount_cents // .attributes.currently_entitled_amount_cents)} + | select(.name != "") + | select(.cents > 0)] | sort_by(-.cents)' \ > dist/supporters.json || echo '[]' > dist/supporters.json echo "supporters: $(jq 'length' dist/supporters.json)" diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index dea870c59..fa9d114e4 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -582,10 +582,10 @@ catalog-browser-storage-73cbc39086 = Tarayıcı depolaması catalog-building-scene-caches-3518c9d139 = Sahne önbellekleri oluşturuluyor… catalog-bump-map-301f0ab5fb = Kabartma Haritası catalog-by-annotation-scale-0aeb536ce3 = Açıklama ölçeğine göre -catalog-by-layer-3243a14fc9 = Katmana göre +catalog-by-layer-3243a14fc9 = Katmandan catalog-by-style-4697cfbbbf = Stile göre -catalog-byblock-a5106d0575 = Bloğa Göre -catalog-bylayer-c668f6e29f = Katmana Göre +catalog-byblock-a5106d0575 = Bloktan +catalog-bylayer-c668f6e29f = Katmandan catalog-c-class-dffdc6893e = C++ Sınıfı catalog-cad-application-for-architecture-engineering-79b5234416 = Mimarlık ve mühendislik için CAD uygulaması catalog-cal-expression-e-g-2-3-4-9d91f1992e = CAL İfade (ör. (2+3)*4): @@ -604,7 +604,7 @@ catalog-chamfer-enter-second-chamfer-distance-d2-2de574d864 = CHAMFER İkinci p catalog-chamfer-select-first-line-d1-d1-d2-d2-3c27011b33 = CHAMFER Birinci çizgiyi seçin [D1=__ocs_arg_d1__ D2=__ocs_arg_d2__]: catalog-chamfer-select-second-line-d1-d1-d2-d2-f2857f4ba8 = CHAMFER İkinci çizgiyi seçin [D1=__ocs_arg_d1__ D2=__ocs_arg_d2__]: catalog-chamfer-select-the-adjacent-polyline-segment-d-0380882c9b = CHAMFER Bitişik çoklu çizgi parçasını seçin [D1=__ocs_arg_d1__ D2=__ocs_arg_d2__]: -catalog-chprop-new-colour-name-1-255-bylayer-93afe25036 = CHPROP Yeni renk (ad / 1-255 / Katmana Göre): +catalog-chprop-new-colour-name-1-255-bylayer-93afe25036 = CHPROP Yeni renk (ad / 1-255 / Katmandan): catalog-chprop-new-layer-name-f0d35963a9 = CHPROP Yeni katman adı: catalog-chprop-new-linetype-name-45684c2bb8 = CHPROP Yeni çizgi tipi adı: catalog-chprop-new-linetype-scale-6c8dca5d8e = CHPROP Yeni çizgi tipi ölçeği: @@ -633,7 +633,7 @@ catalog-clayer-new-current-layer-name-f0f6991d49 = CLAYER Yeni geçerli katman catalog-clayer-cur-968bad6379 = CLAYER = "__ocs_fmt_0__" catalog-clayer-set-to-name-arg-c1045e6576 = CLAYER "__ocs_fmt_0__" olarak ayarlandı. catalog-clayer-layer-not-found-fb5d0a4e1f = CLAYER: '__ocs_fmt_0__' katmanı bulunamadı. -catalog-color-new-object-colour-bylayer-byblock-1-255-fb9a12e441 = COLOR Yeni nesne rengi [Katmana Göre / Bloğa Göre / 1-255 / kırmızı / sarı / yeşil / camgöbeği / mavi / macenta / beyaz]: +catalog-color-new-object-colour-bylayer-byblock-1-255-fb9a12e441 = COLOR Yeni nesne rengi [Katmandan / Bloktan / 1-255 / kırmızı / sarı / yeşil / camgöbeği / mavi / macenta / beyaz]: catalog-colorscheme-unknown-theme-type-colorscheme-lis-8bb80164f7 = COLORSCHEME: bilinmeyen tema '__ocs_fmt_0__'. Seçenekler için COLORSCHEME LIST yazın. catalog-convtosurface-converted-n-solid-s-to-surface-s-1f4c640ada = CONVTOSURFACE: __ocs_fmt_0__ katı yüzeye dönüştürüldü. catalog-convtosurface-select-a-solid-created-this-sess-8a3ac96200 = CONVTOSURFACE: bu oturumda oluşturulmuş bir katı seçin. @@ -1447,7 +1447,7 @@ catalog-line-specify-first-point-c01dd86573 = LINE İlk noktayı belirtin: catalog-line-specify-next-point-close-undo-2db6e2e095 = LINE Sonraki noktayı belirtin [Kapat/Geri Al]: catalog-line-specify-next-point-undo-ade411d556 = LINE Sonraki noktayı belirtin [Geri Al]: catalog-linetype-list-set-4a097128f4 = LINETYPE [Listele / Ayarla]: -catalog-linetype-set-linetype-name-bylayer-byblock-08f8a9af56 = LINETYPE SET Çizgi tipi adı (Katmana Göre / Bloğa Göre / …): +catalog-linetype-set-linetype-name-bylayer-byblock-08f8a9af56 = LINETYPE SET Çizgi tipi adı (Katmandan / Bloktan / …): catalog-linetype-name-is-not-loaded-use-linetype-list-6299789ccf = LINETYPE: "__ocs_fmt_0__" yüklü değil. Kullanılabilir çizgi tipleri için LINETYPE LIST komutunu kullanın. catalog-list-no-entities-selected-select-entities-firs-84fa189d6b = LIST: Nesne seçilmedi. Önce nesneleri seçin. catalog-loft-select-first-cross-section-53660582e4 = LOFT İlk kesiti seçin: @@ -2314,7 +2314,7 @@ catalog-section-solid-modelling-is-unavailable-in-this-da202cedf5 = SECTION: Bu catalog-section-the-plane-does-not-cross-the-solid-dbf07e9f4b = SECTION: Düzlem katıyı kesmiyor. catalog-selectall-object-s-selected-1f84d4cdcd = SELECTALL: __ocs_fmt_0__ nesne seçildi. catalog-selhandles-no-entities-selected-select-entitie-4a7c8d016c = SELHANDLES: Nesne seçilmedi. Önce nesneleri seçin. -catalog-setbylayer-reset-changed-entity-entities-to-by-a875faf5fc = SETBYLAYER: __ocs_fmt_0__ nesne Katmana Göre olarak sıfırlandı. +catalog-setbylayer-reset-changed-entity-entities-to-by-a875faf5fc = SETBYLAYER: __ocs_fmt_0__ nesne Katmandan özelliklerine sıfırlandı. catalog-setbylayer-select-entities-first-07cb922ea6 = SETBYLAYER: Önce nesneleri seçin. catalog-setvar-new-value-blank-to-read-99412ea9e4 = SETVAR Yeni değer (okumak için boş bırakın): catalog-setvar-variable-name-c09f71bfdc = SETVAR Değişken adı: @@ -3225,7 +3225,7 @@ dimstyle-when-text-is-moved = Metin taşındığında dimstyle-zero-suppression = Sıfırları gizleme dimstyle-add-a-leader = Kılavuz ekle dimstyle-automatic = Otomatik -dimstyle-by-block = Bloğa göre +dimstyle-by-block = Bloktan dimstyle-comma = Virgül dimstyle-degrees-minutes-seconds = Derece, dakika, saniye dimstyle-keep-dimension-line-with-text = Ölçü çizgisini metinle birlikte taşı diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 3a9e4e2a5..fdefd0e8f 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -2738,6 +2738,10 @@ fn start_page_content<'a>( Space::new().height(iced::Length::Fixed(12.0)), ] .spacing(6) + .padding(iced::Padding { + right: 12.0, + ..iced::Padding::ZERO + }) .width(Fill); for (name, cents) in patrons { // Cents → the campaign currency's main unit. Symbol assumed "$"; diff --git a/src/patreon.rs b/src/patreon.rs index e95103e8a..1fa926c7c 100644 --- a/src/patreon.rs +++ b/src/patreon.rs @@ -60,12 +60,18 @@ pub fn fetch_patrons() -> Result, String> { let mut patrons: Vec<(String, i64)> = Vec::new(); let mut url = format!( "https://www.patreon.com/api/oauth2/v2/campaigns/{campaign_id}/members\ - ?fields%5Bmember%5D=full_name,patron_status,currently_entitled_amount_cents\ + ?include=pledge_history\ + &fields%5Bmember%5D=full_name,patron_status,currently_entitled_amount_cents\ + &fields%5Bpledge-event%5D=amount_cents,date,payment_status\ &page%5Bcount%5D=200" ); // Bound the loop so a malformed `next` link can never spin forever. for _ in 0..50 { let page = get_json(&agent, token, &url)?; + let included = page["included"] + .as_array() + .map(Vec::as_slice) + .unwrap_or(&[]); if let Some(arr) = page["data"].as_array() { for m in arr { let attrs = &m["attributes"]; @@ -75,7 +81,15 @@ pub fn fetch_patrons() -> Result, String> { if attrs["patron_status"].as_str() != Some("active_patron") { continue; } - let cents = attrs["currently_entitled_amount_cents"].as_i64().unwrap_or(0); + // The entitlement can retain the previous tier amount after a + // pledge change. The latest successful pledge event records + // what was actually paid; retain the entitlement as a fallback + // for memberships whose history is unavailable. + let cents = latest_paid_amount(m, included).unwrap_or_else(|| { + attrs["currently_entitled_amount_cents"] + .as_i64() + .unwrap_or(0) + }); if cents <= 0 { continue; } @@ -96,6 +110,48 @@ pub fn fetch_patrons() -> Result, String> { Ok(patrons) } +#[cfg(not(target_arch = "wasm32"))] +fn latest_paid_amount( + member: &serde_json::Value, + included: &[serde_json::Value], +) -> Option { + let history = member["relationships"]["pledge_history"]["data"].as_array()?; + let mut latest: Option<(String, i64)> = None; + + for relationship in history { + let Some(id) = relationship["id"].as_str() else { + continue; + }; + let Some(resource_type) = relationship["type"].as_str() else { + continue; + }; + let Some(event) = included.iter().find(|entry| { + entry["id"].as_str() == Some(id) + && entry["type"].as_str() == Some(resource_type) + }) else { + continue; + }; + let attrs = &event["attributes"]; + if attrs["payment_status"].as_str() != Some("Paid") { + continue; + } + let cents = attrs["amount_cents"].as_i64().unwrap_or(0); + if cents <= 0 { + continue; + } + let date = attrs["date"].as_str().unwrap_or(""); + if latest + .as_ref() + .map(|(current, _)| date > current.as_str()) + .unwrap_or(true) + { + latest = Some((date.to_string(), cents)); + } + } + + latest.map(|(_, cents)| cents) +} + /// Web build: the browser can't call the Patreon API directly (CORS + the /// token would be exposed in the bundle), so it fetches a pre-generated /// `supporters.json` published next to the app on the same origin (produced by