web: a film sim is colour and tone only

The ten PHOTO STYLE sims now carry nothing but their stock's own grade, and
each is named for the stock it stands for: PROVIA, VELVIA, CLASSIC CHROME,
CLASSIC VIVID (Velvia spliced with Classic Chrome at the blue row), CLASSIC
NEGATIVE, ASTIA, ETERNA, ACROS, LC STREETLIFE CLASSIC, LC STREETLIFE VIVID.
Grain, clarity, saturation and light moves were dropped from their
`adjustments`, so a sim is a clean starting point and the general knobs read
their defaults while the look still lands on the pixels.

LC STREETLIFE VIVID keeps the one brightness step its stock needs, but as
SIM_EXPOSURE_BIAS in colorUtils rather than as an adjustment: it is folded in
where the Exposure slider applies, so the picture gets the lift and the
parameter stays at 0.

Also in this checkpoint: the watermark/GPS boxes and their colour pickers, the
WATERMARK chip column, the real admin stats, and the fix that stopped presets
from doubling and a frame from refusing to come off when a photo was reopened
(/file is the finished render, /base the editable pixels).
This commit is contained in:
2026-09-22 08:32:28 +07:00
parent 52b672deec
commit 428e7fa682
18 changed files with 1831 additions and 148 deletions
+50 -21
View File
@@ -12,9 +12,13 @@ import { QrCode } from './ui/QrCode';
// pairs — the app dictionary holds UI strings, this is a page of copy, and the
// blueprint stays the source of truth for it. The page follows the same theme
// and language switches as the workspace (buttons in the nav, top-right).
// The store URLs are still unknown (WEB_PLAN.md W6), so the store badges raise
// the toast instead of navigating; fill STORE_LINKS in and drop the onClick.
const STORE_LINKS = { play: '#', appstore: '#' };
// The Google Play listing is the internal-test track, so the store badge is a
// real link from today. The App Store has no listing yet (WEB_PLAN.md W6), so
// that badge still raises the toast until its URL lands here.
const STORE_LINKS = {
play: 'https://play.google.com/apps/internaltest/4700963739341162920',
appstore: '#',
};
type Txt = { en: string; vi: string };
@@ -359,8 +363,7 @@ export function Landing() {
{ href: '#pricing', label: { en: 'Pricing', vi: 'Bảng giá' } },
];
const toastDownload: Txt = { en: 'Download links land with the public release — the web studio is live now.', vi: 'Link tải sẽ có khi phát hành chính thức — web studio đã chạy được ngay.' };
const toastStore: Txt = { en: 'Store listing arrives with the public release — use the web studio in the meantime.', vi: 'Cửa hàng ứng dụng sẽ mở khi phát hành chính thức — trong lúc chờ, hãy dùng web studio.' };
const toastStore: Txt = { en: 'The App Store listing arrives with the public release — use the web studio in the meantime.', vi: 'App Store sẽ mở khi phát hành chính thức — trong lúc chờ, hãy dùng web studio.' };
// Sharing. The URL is the page's own origin, so the links point at whatever
// host is serving this build (the crawler meta in index.html does the same
@@ -410,9 +413,15 @@ export function Landing() {
<a className="lp-btn lp-sm lp-primary" data-key="nav-studio" href="/app">
{c({ en: 'RecipesCam web studio', vi: 'Studio web RecipesCam' })}
</a>
<button type="button" className="lp-btn lp-sm lp-primary" data-key="nav-download" onClick={() => showToast(toastDownload)}>
<a
className="lp-btn lp-sm lp-primary"
data-key="download-play"
href={STORE_LINKS.play}
target="_blank"
rel="noopener noreferrer"
>
{c({ en: 'Download RecipesCam', vi: 'Tải RecipesCam' })}
</button>
</a>
{/* Theme + language, the same two switches the workspace TopBar has.
Theme opens a popover: light/dark plus the font group, which
retypes the whole page. */}
@@ -561,14 +570,16 @@ export function Landing() {
{t('nav.login')}
</button>
)}
<button
type="button"
<a
className="lp-btn lp-primary"
data-key="sheet-download"
onClick={() => { setMenu(false); showToast(toastDownload); }}
data-key="download-play"
href={STORE_LINKS.play}
target="_blank"
rel="noopener noreferrer"
onClick={() => setMenu(false)}
>
{c({ en: 'Download RecipesCam', vi: 'Tải RecipesCam' })}
</button>
</a>
</div>
) : null}
</nav>
@@ -868,9 +879,15 @@ export function Landing() {
</ul>
<div className="lp-plan-cta">
<a className="lp-btn" href="/app">{c({ en: 'Open the studio', vi: 'Mở studio' })}</a>
<button type="button" className="lp-btn" onClick={() => showToast(toastStore)}>
<a
className="lp-btn"
data-key="download-play"
href={STORE_LINKS.play}
target="_blank"
rel="noopener noreferrer"
>
{c({ en: 'Get it on Google Play', vi: 'Tải trên Google Play' })}
</button>
</a>
</div>
</article>
<article className="lp-plan vip">
@@ -885,10 +902,16 @@ export function Landing() {
<li>{c({ en: 'Unlimited QR generation', vi: 'Tạo QR không giới hạn' })}</li>
</ul>
<div className="lp-plan-cta">
<button type="button" className="lp-btn" onClick={() => showToast(toastStore)}>
<a
className="lp-btn"
data-key="download-play"
href={STORE_LINKS.play}
target="_blank"
rel="noopener noreferrer"
>
{c({ en: 'Get it on Google Play', vi: 'Tải trên Google Play' })}
</button>
<button type="button" className="lp-btn" onClick={() => showToast(toastStore)}>
</a>
<button type="button" className="lp-btn" data-key="download-appstore" onClick={() => showToast(toastStore)}>
{c({ en: 'Download on the App Store', vi: 'Tải trên App Store' })}
</button>
</div>
@@ -934,12 +957,18 @@ export function Landing() {
<h2>{c({ en: 'Load a recipe. Shoot the rest of the day.', vi: 'Nạp một công thức. Chụp cả ngày còn lại.' })}</h2>
<p>{c({ en: 'RecipesCam is free on both stores, and the web studio runs right here in your browser.', vi: 'RecipesCam miễn phí trên cả hai cửa hàng, và web studio chạy ngay trong trình duyệt của bạn.' })}</p>
<div className="lp-stores">
<button type="button" className="lp-btn" onClick={() => showToast(toastStore)}>
<button type="button" className="lp-btn" data-key="download-appstore" onClick={() => showToast(toastStore)}>
<span className="lp-store"><small>{c({ en: 'COMING SOON', vi: 'SẮP CÓ' })}</small><b>{c({ en: 'Download on the App Store', vi: 'Tải trên App Store' })}</b></span>
</button>
<button type="button" className="lp-btn" onClick={() => showToast(toastStore)}>
<span className="lp-store"><small>{c({ en: 'COMING SOON', vi: 'SẮP CÓ' })}</small><b>{c({ en: 'Get it on Google Play', vi: 'Tải trên Google Play' })}</b></span>
</button>
<a
className="lp-btn"
data-key="download-play"
href={STORE_LINKS.play}
target="_blank"
rel="noopener noreferrer"
>
<span className="lp-store"><small>{c({ en: 'AVAILABLE NOW', vi: 'ĐÃ CÓ' })}</small><b>{c({ en: 'Get it on Google Play', vi: 'Tải trên Google Play' })}</b></span>
</a>
<a className="lp-btn lp-primary" href="/app">{c({ en: 'Open the Web Studio', vi: 'Mở Web Studio' })}</a>
</div>
{/* Share targets: the four networks take the URL as a query param,