diff --git a/README.md b/README.md index 7197c1b..3582904 100644 --- a/README.md +++ b/README.md @@ -89,9 +89,12 @@ MultiCam Planner is designed for quick, intuitive camera planning with essential next version instead of a second device. *Upload own devices automatically* (Settings, on by default) uploads on start and a few seconds after a change; *Sync now* uploads everything. Unchanged entries (by hash of - what was sent last) are not re-sent automatically. Under the selector every - own entry shows its last result — waiting for moderation, live, in sync, - blocked (with the reason, e.g. *datasheet link missing*) or failed — and + what was sent last) are not re-sent automatically — except those still + waiting for moderation: the server reports `moderation: pending | approved` + with every result (also `in-sync`), so the status turns *live* once a + moderator approves. Under the selector every own entry shows its last + result — waiting for moderation, live, blocked (with the reason, e.g. + *datasheet link missing*) or failed — and *Upload…*, which asks for the datasheet link, stores it on the entry (`manufacturerUrl`) and uploads at once. Not signed in, it leads to the sign-in. Changed community guidelines (`guidelines-outdated`) have to be diff --git a/src/__tests__/deviceLibrary.test.ts b/src/__tests__/deviceLibrary.test.ts index eb8a76e..af410bf 100644 --- a/src/__tests__/deviceLibrary.test.ts +++ b/src/__tests__/deviceLibrary.test.ts @@ -224,7 +224,7 @@ describe('store against a mocked server', () => { fetchMock .mockResolvedValueOnce(json({ user: { id: 'u', email: 'a@b.de', username: 'l' } }, { 'set-auth-token': 'T' })) .mockResolvedValueOnce(json({ planner: 'multicam', results: [ - { localId: eigeneKamera.id, state: 'edit-proposed', slug: 'sony-pxw-fx9' }, + { localId: eigeneKamera.id, state: 'edit-proposed', slug: 'sony-pxw-fx9', moderation: 'pending' }, { localId: 'custom-lens-1', state: 'blocked', findings: [{ kind: 'no-source', blocking: true }] }, ] })) .mockResolvedValueOnce(json(antwort(0, []))); @@ -247,19 +247,27 @@ describe('store against a mocked server', () => { expect(r['custom-lens-1']).toMatchObject({ state: 'blocked', findings: ['no-source'] }); expect(JSON.parse(speicher['multicam-device-library-uploads']).records[eigeneKamera.id].state).toBe('edit-proposed'); - // … unveraendert geht automatisch nichts erneut hoch, nur der Abgleich: + // … automatisch geht unveraendert nur noch mit, was in der Moderation + // wartet (die Kamera), nicht das Blockierte — und die Freigabe kommt an: + fetchMock + .mockResolvedValueOnce(json({ results: [{ localId: eigeneKamera.id, state: 'in-sync', moderation: 'approved' }] })) + .mockResolvedValueOnce(json(antwort(0, []))); + await useDeviceLibrary.getState().syncAll(); + expect(JSON.parse(String((fetchMock.mock.calls[3][1] as RequestInit).body)).items.map((i: { localId: string }) => i.localId)).toEqual([eigeneKamera.id]); + expect(useDeviceLibrary.getState().uploads.records[eigeneKamera.id]).toMatchObject({ state: 'in-sync', moderation: 'approved', slug: 'sony-pxw-fx9' }); + + // … danach, freigegeben und unveraendert, nur noch der Abgleich: fetchMock.mockResolvedValueOnce(json(antwort(0, []))); await useDeviceLibrary.getState().syncAll(); - expect(fetchMock.mock.calls[3][0]).toContain('/api/sync'); + expect(fetchMock.mock.calls[5][0]).toContain('/api/sync'); // … „Sync now" schickt alles, dann erst den Abgleich. fetchMock - .mockResolvedValueOnce(json({ results: [{ localId: eigeneKamera.id, state: 'in-sync' }, { localId: 'custom-lens-1', state: 'blocked' }] })) + .mockResolvedValueOnce(json({ results: [{ localId: eigeneKamera.id, state: 'in-sync', moderation: 'approved' }, { localId: 'custom-lens-1', state: 'blocked' }] })) .mockResolvedValueOnce(json(antwort(0, []))); await useDeviceLibrary.getState().syncAll({ manual: true }); - expect(fetchMock.mock.calls[4][0]).toContain('/api/upload'); - expect(fetchMock.mock.calls[5][0]).toContain('/api/sync'); - expect(useDeviceLibrary.getState().uploads.records[eigeneKamera.id]).toMatchObject({ state: 'in-sync', slug: 'sony-pxw-fx9' }); + expect(fetchMock.mock.calls[6][0]).toContain('/api/upload'); + expect(fetchMock.mock.calls[7][0]).toContain('/api/sync'); }); it('automatic upload can be switched off; manual sync still uploads', async () => { @@ -324,7 +332,10 @@ describe('upload ledger', () => { const { pendingUploads, applyUploadResults, emptyLedger, hashOf, toUploadItem } = await import('../library/upload'); const erst = pendingUploads([kamera], emptyLedger('https://s')); expect(erst).toHaveLength(1); - const ledger = applyUploadResults(emptyLedger('https://s'), erst, [{ localId: eigeneKamera.id, state: 'created', slug: 'x' }], '2026-09-27T00:00:00Z'); + const wartend = applyUploadResults(emptyLedger('https://s'), erst, [{ localId: eigeneKamera.id, state: 'created', slug: 'x', moderation: 'pending' }], '2026-09-27T00:00:00Z'); + // Wartet in der Moderation: geht erneut mit, damit die Freigabe ankommt. + expect(pendingUploads([kamera], wartend)).toHaveLength(1); + const ledger = applyUploadResults(wartend, erst, [{ localId: eigeneKamera.id, state: 'in-sync', moderation: 'approved' }], '2026-09-27T00:00:00Z'); expect(pendingUploads([kamera], ledger)).toHaveLength(0); expect(pendingUploads([kamera], ledger, true)).toHaveLength(1); const umsortiert = Object.fromEntries(Object.entries(eigeneKamera).reverse()) as unknown as Camera; @@ -336,6 +347,17 @@ describe('upload ledger', () => { expect(hashOf({ a: 1, b: 2 })).toBe(hashOf({ b: 2, a: 1 })); }); + it('display bucket follows moderation, older records fall back to the state', async () => { + const { uploadBucket } = await import('../library/upload'); + const r = (state: string, moderation?: 'pending' | 'approved') => ({ hash: 'h', state, at: 'z', ...(moderation ? { moderation } : {}) }) as never; + expect(uploadBucket(r('in-sync', 'pending'))).toBe('waiting'); + expect(uploadBucket(r('in-sync', 'approved'))).toBe('live'); + expect(uploadBucket(r('created'))).toBe('waiting'); + expect(uploadBucket(r('in-sync'))).toBe('live'); + expect(uploadBucket(r('blocked', 'pending'))).toBe('blocked'); + expect(uploadBucket(r('error'))).toBe('failed'); + }); + it('a ledger of another server is not reused; deleted entries fall out', async () => { const { readLedger, pruneLedger, emptyLedger } = await import('../library/upload'); const l = { server: 'https://a', records: { x: { hash: 'h', state: 'created' as const, at: 'z' } } }; diff --git a/src/components/Settings/DeviceLibrarySection.tsx b/src/components/Settings/DeviceLibrarySection.tsx index ced28ea..2597b5d 100644 --- a/src/components/Settings/DeviceLibrarySection.tsx +++ b/src/components/Settings/DeviceLibrarySection.tsx @@ -11,6 +11,7 @@ import { import { useDeviceLibrary } from '../../library/store'; import LibraryErrorLine from '../Library/LibraryErrorLine'; import { usesKeychain } from '../../library/tokenStore'; +import { uploadBucket } from '../../library/upload'; const feld = 'block w-full border border-bc-border bg-bc-dark text-xs text-bc-text-bright'; const knopf = 'border border-bc-border text-xs text-bc-text transition-colors hover:bg-bc-panel-raised disabled:opacity-50'; @@ -28,7 +29,7 @@ export default function DeviceLibrarySection() { const busy = lib.phase === 'signing-in' || lib.phase === 'checking' || lib.phase === 'syncing' || lib.phase === 'uploading'; const records = Object.values(lib.uploads.records); - const zaehle = (...states: string[]) => records.filter((r) => states.includes(r.state)).length; + const zaehle = (bucket: ReturnType) => records.filter((r) => uploadBucket(r) === bucket).length; const cameras = lib.cache.entries.filter((e) => e.kind === 'camera').length; const lenses = lib.cache.entries.length - cameras; @@ -224,12 +225,12 @@ export default function DeviceLibrarySection() { {records.length > 0 && (

{format( - t('library.upload.summary', 'Own entries: {live} live or in sync, {waiting} waiting for moderation, {blocked} blocked, {failed} failed.'), + t('library.upload.summary', 'Own entries: {live} live, {waiting} waiting for moderation, {blocked} blocked, {failed} failed.'), { - live: zaehle('approved', 'in-sync'), - waiting: zaehle('created', 'edit-proposed', 'pending-updated'), + live: zaehle('live'), + waiting: zaehle('waiting'), blocked: zaehle('blocked'), - failed: zaehle('error'), + failed: zaehle('failed'), }, )}

diff --git a/src/data/cameras.ts b/src/data/cameras.ts index 8ba0417..6a0c2ac 100644 --- a/src/data/cameras.ts +++ b/src/data/cameras.ts @@ -578,13 +578,13 @@ export const CAMERAS: Camera[] = [ { id: 'datavideo-ptc-305', manufacturerUrl: 'https://datavideo.com/product/PTC-305', manufacturer: 'Datavideo', model: 'PTC-305', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, { id: 'datavideo-ptc-145', manufacturerUrl: 'https://datavideo.com/product/PTC-145', manufacturer: 'Datavideo', model: 'PTC-145', sensor: { name: '1/2.8" (5.37×4.04)', widthMm: 5.37, heightMm: 4.04, cropFactor: 6.7 }, mount: 'integrated', resolutions: ['HD'], type: 'ptz' }, { id: 'avonic-cm93-ip', manufacturerUrl: 'https://avonic.com/cm93/', manufacturer: 'Avonic', model: 'CM93-IP', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, - { id: 'sony-srg-a30-b', manufacturer: 'Sony', model: 'SRG-XB25', sensor: { name: '1/2.5" (5.76×4.29)', widthMm: 5.76, heightMm: 4.29, cropFactor: 6.25 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, - { id: 'canon-cr-n300w', manufacturer: 'Canon', model: 'CR-N300 (weiß)', sensor: { name: '1/2.3" (6.17×4.55)', widthMm: 6.17, heightMm: 4.55, cropFactor: 5.64 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, + { id: 'sony-srg-a30-b', manufacturerUrl: 'https://pro.sony/ue_US/products/ptz-network-cameras/srg-xb25', manufacturer: 'Sony', model: 'SRG-XB25', sensor: { name: '1/2.5" (5.76×4.29)', widthMm: 5.76, heightMm: 4.29, cropFactor: 6.25 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, + { id: 'canon-cr-n300w', manufacturerUrl: 'https://www.canon-europe.com/ptz-cameras/cr-n300/specifications/', manufacturer: 'Canon', model: 'CR-N300 (weiß)', sensor: { name: '1/2.3" (6.17×4.55)', widthMm: 6.17, heightMm: 4.55, cropFactor: 5.64 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, { id: 'ptzoptics-move-4k-ndi', manufacturerUrl: 'https://docs.ptzoptics.com/docs/cameras/move-4k/technical-specs', manufacturer: 'PTZOptics', model: 'Move 4K SDI', sensor: { name: '1/2.5" (5.76×4.29)', widthMm: 5.76, heightMm: 4.29, cropFactor: 6.25 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, { id: 'lumens-vc-b11u', manufacturerUrl: 'https://www.mylumens.com/en/Products_detail/1047/VC-B11U-Video-Conference-Camera', manufacturer: 'Lumens', model: 'VC-B11U', sensor: { name: '1/2.8" (5.37×4.04)', widthMm: 5.37, heightMm: 4.04, cropFactor: 6.7 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, { id: 'marshall-cv730-ndi', manufacturerUrl: 'https://marshall-usa.com/cameras/CV730-NDI/', manufacturer: 'Marshall', model: 'CV730-NDI', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, - { id: 'aida-uhd-100a', manufacturer: 'AIDA', model: 'UHD-100A', sensor: { name: '1/2.5" (5.76×4.29)', widthMm: 5.76, heightMm: 4.29, cropFactor: 6.25 }, mount: 'C/CS', resolutions: ['4K','HD'], type: 'broadcast' }, - { id: 'aida-ptz4k-ndi', manufacturer: 'AIDA', model: 'PTZ4K-NDI-X30', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, + { id: 'aida-uhd-100a', manufacturerUrl: 'https://aidaimaging.com/uhd-100a/', manufacturer: 'AIDA', model: 'UHD-100A', sensor: { name: '1/2.5" (5.76×4.29)', widthMm: 5.76, heightMm: 4.29, cropFactor: 6.25 }, mount: 'C/CS', resolutions: ['4K','HD'], type: 'broadcast' }, + { id: 'aida-ptz4k-ndi', manufacturerUrl: 'https://aidaimaging.com/ptz4k-ndi-x30/', manufacturer: 'AIDA', model: 'PTZ4K-NDI-X30', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' }, // ── Marshall POV ── { id: 'marshall-cv568', manufacturerUrl: 'https://marshall-usa.com/cameras/CV568/', manufacturer: 'Marshall', model: 'CV568', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'M12', resolutions: ['HD'], type: 'broadcast', notes: 'POV camera, global shutter' }, diff --git a/src/data/lenses.ts b/src/data/lenses.ts index 2515881..18bbcfe 100644 --- a/src/data/lenses.ts +++ b/src/data/lenses.ts @@ -81,7 +81,7 @@ export const LENSES: Lens[] = [ { id: 'can-hj24ex7.5b', manufacturerUrl: 'https://asia.canon/en/business/hj24ex7-5b/specification', manufacturer: 'Canon', model: 'HJ24ex7.5B', focalLengthMin: 7.5, focalLengthMax: 180, maxApertureWide: 1.8, mount: 'B4', extenderFactors: [2], type: 'zoom', notes: 'HD handheld 24x' }, { id: 'can-hj22ex7.6b', manufacturerUrl: 'https://asia.canon/en/business/hj22ex7-6b-its-me/specification', manufacturer: 'Canon', model: 'HJ22ex7.6B', focalLengthMin: 7.6, focalLengthMax: 167, maxApertureWide: 1.8, mount: 'B4', extenderFactors: [2], type: 'zoom' }, { id: 'can-hj17ex7.6b', manufacturerUrl: 'https://asia.canon/en/business/hj17ex7-6b-its-me/specification', manufacturer: 'Canon', model: 'HJ17ex7.6B', focalLengthMin: 7.6, focalLengthMax: 129, maxApertureWide: 1.8, mount: 'B4', extenderFactors: [2], type: 'zoom', notes: 'HD standard 17x' }, - { id: 'can-hj17ex6.2b', manufacturer: 'Canon', model: 'HJ17ex6.2B', focalLengthMin: 6.2, focalLengthMax: 105, maxApertureWide: 1.8, mount: 'B4', extenderFactors: [2], type: 'zoom', notes: 'HD wide 17x' }, + { id: 'can-hj17ex6.2b', manufacturerUrl: 'https://www.canon-europe.com/broadcast/eng-efp-pro/hj17ex62b/', manufacturer: 'Canon', model: 'HJ17ex6.2B', focalLengthMin: 6.2, focalLengthMax: 106, maxApertureWide: 1.8, mount: 'B4', extenderFactors: [2], type: 'zoom', notes: 'HD wide 17x' }, { id: 'can-hj14ex4.3b', manufacturerUrl: 'https://asia.canon/en/business/hj14ex4-3b-its-me/specification', manufacturer: 'Canon', model: 'HJ14ex4.3B', focalLengthMin: 4.3, focalLengthMax: 60, maxApertureWide: 1.8, mount: 'B4', extenderFactors: [2], type: 'zoom', notes: 'HD wide 14x' }, { id: 'can-hj11ex4.7b', manufacturer: 'Canon', model: 'HJ11ex4.7B', focalLengthMin: 4.7, focalLengthMax: 52, maxApertureWide: 1.9, mount: 'B4', extenderFactors: [2], type: 'zoom', notes: 'HD ultra-wide 11x' }, @@ -236,22 +236,22 @@ export const LENSES: Lens[] = [ // ══════════════════════════════════════════════ // PTZ INTEGRATED LENSES (virtual entries) // ══════════════════════════════════════════════ - { id: 'ptz-sony-brc-x400', manufacturer: 'Sony', model: 'BRC-X400 integrated 20x', focalLengthMin: 4.4, focalLengthMax: 88, maxApertureWide: 2.0, maxApertureTele: 3.8, mount: 'integrated', type: 'integrated', notes: '20x optical zoom' }, - { id: 'ptz-sony-srg-x120', manufacturer: 'Sony', model: 'SRG-X120 integrated 12x', focalLengthMin: 4.4, focalLengthMax: 52.8, maxApertureWide: 2.0, maxApertureTele: 3.7, mount: 'integrated', type: 'integrated', notes: '12x optical zoom' }, - { id: 'ptz-sony-brc-h800', manufacturer: 'Sony', model: 'BRC-H800 integrated 12x', focalLengthMin: 9.3, focalLengthMax: 111.6, maxApertureWide: 2.8, maxApertureTele: 4.5, mount: 'integrated', type: 'integrated', notes: '12x optical zoom, HD PTZ' }, + { id: 'ptz-sony-brc-x400', manufacturerUrl: 'https://pro.sony/en_GB/products/ptz-network-cameras/brc-x400', manufacturer: 'Sony', model: 'BRC-X400 integrated 20x', focalLengthMin: 4.4, focalLengthMax: 88, maxApertureWide: 2.0, maxApertureTele: 3.8, mount: 'integrated', type: 'integrated', notes: '20x optical zoom' }, + { id: 'ptz-sony-srg-x120', manufacturerUrl: 'https://pro.sony/en_GB/products/ptz-network-cameras/srg-x120', manufacturer: 'Sony', model: 'SRG-X120 integrated 12x', focalLengthMin: 4.4, focalLengthMax: 52.8, maxApertureWide: 2.0, maxApertureTele: 3.7, mount: 'integrated', type: 'integrated', notes: '12x optical zoom' }, + { id: 'ptz-sony-brc-h800', manufacturerUrl: 'https://pro.sony/en_GB/products/ptz-network-cameras/brc-h800', manufacturer: 'Sony', model: 'BRC-H800 integrated 12x', focalLengthMin: 9.3, focalLengthMax: 111.6, maxApertureWide: 2.8, maxApertureTele: 4.5, mount: 'integrated', type: 'integrated', notes: '12x optical zoom, HD PTZ' }, { id: 'ptz-pana-ue150', manufacturerUrl: 'https://pro-av.panasonic.net/en/products/aw-ue150/', manufacturer: 'Panasonic', model: 'AW-UE150 integrated 20x', focalLengthMin: 8.8, focalLengthMax: 176.0, maxApertureWide: 2.8, maxApertureTele: 4.5, mount: 'integrated', type: 'integrated', notes: '20x optical zoom' }, { id: 'ptz-pana-ue40', manufacturerUrl: 'https://connect.na.panasonic.com/av/video/ptz/aw-ue40?hsLang=en', manufacturer: 'Panasonic', model: 'AW-UE40 integrated 24x', focalLengthMin: 4.12, focalLengthMax: 98.9, maxApertureWide: 1.8, maxApertureTele: 4.0, mount: 'integrated', type: 'integrated', notes: '24x optical zoom' }, - { id: 'ptz-canon-crn500', manufacturer: 'Canon', model: 'CR-N500 integrated 15x', focalLengthMin: 8.3, focalLengthMax: 124.5, maxApertureWide: 2.8, maxApertureTele: 4.5, mount: 'integrated', type: 'integrated', notes: '15x optical zoom' }, - { id: 'ptz-canon-crn300', manufacturer: 'Canon', model: 'CR-N300 integrated 20x', focalLengthMin: 3.67, focalLengthMax: 73.4, maxApertureWide: 1.8, maxApertureTele: 2.8, mount: 'integrated', type: 'integrated', notes: '20x optical zoom' }, + { id: 'ptz-canon-crn500', manufacturerUrl: 'https://www.canon-europe.com/ptz-cameras/cr-n500/specifications/', manufacturer: 'Canon', model: 'CR-N500 integrated 15x', focalLengthMin: 8.3, focalLengthMax: 124.5, maxApertureWide: 2.8, maxApertureTele: 4.5, mount: 'integrated', type: 'integrated', notes: '15x optical zoom' }, + { id: 'ptz-canon-crn300', manufacturerUrl: 'https://www.canon-europe.com/ptz-cameras/cr-n300/specifications/', manufacturer: 'Canon', model: 'CR-N300 integrated 20x', focalLengthMin: 3.67, focalLengthMax: 73.4, maxApertureWide: 1.8, maxApertureTele: 2.8, mount: 'integrated', type: 'integrated', notes: '20x optical zoom' }, { id: 'ptz-sony-fr7', manufacturer: 'Sony', model: 'FR7 (no integrated lens)', focalLengthMin: 28, focalLengthMax: 135, maxApertureWide: 4, mount: 'E', type: 'zoom', notes: 'Use E-mount lens' }, // ══════════════════════════════════════════════ // CINEMA-OPTIKEN (PL/LPL) — T-Blenden laut Hersteller (2026-09) // ══════════════════════════════════════════════ // ── Angénieux Optimo (PL, S35) ── - { id: 'ang-optimo-24-290', manufacturer: 'Angénieux', model: 'Optimo 24-290 T2.8', focalLengthMin: 24, focalLengthMax: 290, maxApertureWide: 2.8, mount: 'PL', imageCircle: 'S35', type: 'zoom' }, - { id: 'ang-optimo-28-76', manufacturer: 'Angénieux', model: 'Optimo 28-76 T2.6', focalLengthMin: 28, focalLengthMax: 76, maxApertureWide: 2.6, mount: 'PL', imageCircle: 'S35', type: 'zoom' }, - { id: 'ang-optimo-15-40', manufacturer: 'Angénieux', model: 'Optimo 15-40 T2.6', focalLengthMin: 15, focalLengthMax: 40, maxApertureWide: 2.6, mount: 'PL', imageCircle: 'S35', type: 'zoom' }, - { id: 'ang-optimo-17-80', manufacturer: 'Angénieux', model: 'Optimo 17-80 T2.2', focalLengthMin: 17, focalLengthMax: 80, maxApertureWide: 2.2, mount: 'PL', imageCircle: 'S35', type: 'zoom' }, + { id: 'ang-optimo-24-290', manufacturerUrl: 'https://www.angenieux.com/lenses/legacy-series/optimo-long-lens-zoom-24-290/', manufacturer: 'Angénieux', model: 'Optimo 24-290 T2.8', focalLengthMin: 24, focalLengthMax: 290, maxApertureWide: 2.8, mount: 'PL', imageCircle: 'S35', type: 'zoom' }, + { id: 'ang-optimo-28-76', manufacturerUrl: 'https://www.angenieux.com/optimo-compact-lens-zoom-28-76/', manufacturer: 'Angénieux', model: 'Optimo 28-76 T2.6', focalLengthMin: 28, focalLengthMax: 76, maxApertureWide: 2.6, mount: 'PL', imageCircle: 'S35', type: 'zoom' }, + { id: 'ang-optimo-15-40', manufacturerUrl: 'https://www.angenieux.com/wp-content/uploads/2016/01/15-40-Product-Sheet.pdf', manufacturer: 'Angénieux', model: 'Optimo 15-40 T2.6', focalLengthMin: 15, focalLengthMax: 40, maxApertureWide: 2.6, mount: 'PL', imageCircle: 'S35', type: 'zoom' }, + { id: 'ang-optimo-17-80', manufacturerUrl: 'https://www.angenieux.com/wp-content/uploads/2019/03/User-Manual-17-80.pdf', manufacturer: 'Angénieux', model: 'Optimo 17-80 T2.2', focalLengthMin: 17, focalLengthMax: 80, maxApertureWide: 2.2, mount: 'PL', imageCircle: 'S35', type: 'zoom' }, // ── ARRI Signature Primes (LPL, Full Frame, T1.8) ── { id: 'arri-sig-25', manufacturerUrl: 'https://www.arri.com/en/cine-lenses/signature-lenses/signature-primes-zooms/signature-primes', manufacturer: 'ARRI', model: 'Signature Prime 25mm T1.8', focalLengthMin: 25, focalLengthMax: 25, maxApertureWide: 1.8, mount: 'LPL', imageCircle: 'FF', type: 'prime' }, { id: 'arri-sig-35', manufacturerUrl: 'https://www.arri.com/en/cine-lenses/signature-lenses/signature-primes-zooms/signature-primes', manufacturer: 'ARRI', model: 'Signature Prime 35mm T1.8', focalLengthMin: 35, focalLengthMax: 35, maxApertureWide: 1.8, mount: 'LPL', imageCircle: 'FF', type: 'prime' }, @@ -458,18 +458,18 @@ export const LENSES: Lens[] = [ { id: 'leitz-summicron-c-100', manufacturerUrl: 'https://leitz-cine.com/product/summicron-c/', manufacturer: 'Leitz', model: 'Summicron-C 100mm T2.0', focalLengthMin: 100, focalLengthMax: 100, maxApertureWide: 2.0, mount: 'PL', imageCircle: 'S35', type: 'prime' }, { id: 'leitz-summicron-c-135', manufacturerUrl: 'https://leitz-cine.com/product/summicron-c/', manufacturer: 'Leitz', model: 'Summicron-C 135mm T2.0', focalLengthMin: 135, focalLengthMax: 135, maxApertureWide: 2.0, mount: 'PL', imageCircle: 'S35', type: 'prime' }, // ── Angénieux Optimo Prime (PL, Full Frame; T1.8) ── - { id: 'ang-optimoprime-18', manufacturer: 'Angénieux', model: 'Optimo Prime 18mm T1.8', focalLengthMin: 18, focalLengthMax: 18, maxApertureWide: 2.0, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-21', manufacturer: 'Angénieux', model: 'Optimo Prime 21mm T1.8', focalLengthMin: 21, focalLengthMax: 21, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-24', manufacturer: 'Angénieux', model: 'Optimo Prime 24mm T1.8', focalLengthMin: 24, focalLengthMax: 24, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-28', manufacturer: 'Angénieux', model: 'Optimo Prime 28mm T1.8', focalLengthMin: 28, focalLengthMax: 28, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-32', manufacturer: 'Angénieux', model: 'Optimo Prime 32mm T1.8', focalLengthMin: 32, focalLengthMax: 32, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-40', manufacturer: 'Angénieux', model: 'Optimo Prime 40mm T1.8', focalLengthMin: 40, focalLengthMax: 40, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-50', manufacturer: 'Angénieux', model: 'Optimo Prime 50mm T1.8', focalLengthMin: 50, focalLengthMax: 50, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-60', manufacturer: 'Angénieux', model: 'Optimo Prime 60mm T1.8', focalLengthMin: 60, focalLengthMax: 60, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-75', manufacturer: 'Angénieux', model: 'Optimo Prime 75mm T1.8', focalLengthMin: 75, focalLengthMax: 75, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-100', manufacturer: 'Angénieux', model: 'Optimo Prime 100mm T1.8', focalLengthMin: 100, focalLengthMax: 100, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-135', manufacturer: 'Angénieux', model: 'Optimo Prime 135mm T1.8', focalLengthMin: 135, focalLengthMax: 135, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, - { id: 'ang-optimoprime-200', manufacturer: 'Angénieux', model: 'Optimo Prime 200mm T1.8', focalLengthMin: 200, focalLengthMax: 200, maxApertureWide: 2.2, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-18', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 18mm T2', focalLengthMin: 18, focalLengthMax: 18, maxApertureWide: 2.0, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-21', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 21mm T1.8', focalLengthMin: 21, focalLengthMax: 21, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-24', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 24mm T1.8', focalLengthMin: 24, focalLengthMax: 24, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-28', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 28mm T1.8', focalLengthMin: 28, focalLengthMax: 28, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-32', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 32mm T1.8', focalLengthMin: 32, focalLengthMax: 32, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-40', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 40mm T1.8', focalLengthMin: 40, focalLengthMax: 40, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-50', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 50mm T1.8', focalLengthMin: 50, focalLengthMax: 50, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-60', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 60mm T1.8', focalLengthMin: 60, focalLengthMax: 60, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-75', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 75mm T1.8', focalLengthMin: 75, focalLengthMax: 75, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-100', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 100mm T1.8', focalLengthMin: 100, focalLengthMax: 100, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-135', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 135mm T1.8', focalLengthMin: 135, focalLengthMax: 135, maxApertureWide: 1.8, mount: 'PL', imageCircle: 'FF', type: 'prime' }, + { id: 'ang-optimoprime-200', manufacturerUrl: 'https://www.angenieux.com/lenses/optimo-prime-series/', manufacturer: 'Angénieux', model: 'Optimo Prime 200mm T2.2', focalLengthMin: 200, focalLengthMax: 200, maxApertureWide: 2.2, mount: 'PL', imageCircle: 'FF', type: 'prime' }, // ── Sigma Art DG (E-Mount, Full Frame) ── { id: 'sigma-art-e-14', manufacturerUrl: 'https://www.sigma-global.com/en/lenses/a017_14_18/', manufacturer: 'Sigma', model: '14mm f/1.8 DG HSM Art', focalLengthMin: 14, focalLengthMax: 14, maxApertureWide: 1.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, { id: 'sigma-art-e-20', manufacturerUrl: 'https://www.sigma-global.com/en/lenses/a015_20_14/', manufacturer: 'Sigma', model: '20mm f/1.4 DG HSM Art', focalLengthMin: 20, focalLengthMax: 20, maxApertureWide: 1.4, mount: 'E', imageCircle: 'FF', type: 'prime' }, @@ -518,23 +518,23 @@ export const LENSES: Lens[] = [ { id: 'tamron-e-90macro', manufacturerUrl: 'https://www.tamron.com/global/consumer/lenses/f072/', manufacturer: 'Tamron', model: '90 F2.8 Di III Macro VXD', focalLengthMin: 90, focalLengthMax: 90, maxApertureWide: 2.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, // ── Viltrox (Sony E, Full Frame) ── { id: 'viltrox-e-16', manufacturerUrl: 'https://viltrox.com/products/viltrox-af-16mm-f1-8-fe', manufacturer: 'Viltrox', model: 'AF 16 F1.8 FE', focalLengthMin: 16, focalLengthMax: 16, maxApertureWide: 1.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, - { id: 'viltrox-e-24', manufacturer: 'Viltrox', model: 'AF 24 F1.8 FE', focalLengthMin: 24, focalLengthMax: 24, maxApertureWide: 1.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, + { id: 'viltrox-e-24', manufacturerUrl: 'https://viltrox.com/pages/af-24-1-8-fe', manufacturer: 'Viltrox', model: 'AF 24 F1.8 FE', focalLengthMin: 24, focalLengthMax: 24, maxApertureWide: 1.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, { id: 'viltrox-e-28', manufacturerUrl: 'https://viltrox.com/products/viltrox-af-28mm-f1-8-full-frame-lens-for-sony-e-mount', manufacturer: 'Viltrox', model: 'AF 28 F1.8 FE', focalLengthMin: 28, focalLengthMax: 28, maxApertureWide: 1.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, { id: 'viltrox-e-35-18', manufacturerUrl: 'https://viltrox.com/products/af-35mm-f1-8-fe', manufacturer: 'Viltrox', model: 'AF 35 F1.8 FE', focalLengthMin: 35, focalLengthMax: 35, maxApertureWide: 1.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, { id: 'viltrox-e-35-12lab', manufacturerUrl: 'https://viltrox.com/products/af-35mm-f1-2-fe', manufacturer: 'Viltrox', model: 'AF 35 F1.2 LAB FE', focalLengthMin: 35, focalLengthMax: 35, maxApertureWide: 1.2, mount: 'E', imageCircle: 'FF', type: 'prime' }, - { id: 'viltrox-e-50', manufacturer: 'Viltrox', model: 'AF 50 F1.8 FE', focalLengthMin: 50, focalLengthMax: 50, maxApertureWide: 1.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, + { id: 'viltrox-e-50', manufacturerUrl: 'https://viltrox.com/pages/af-50-1-8-fe', manufacturer: 'Viltrox', model: 'AF 50 F1.8 FE', focalLengthMin: 50, focalLengthMax: 50, maxApertureWide: 1.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, { id: 'viltrox-e-85', manufacturerUrl: 'https://viltrox.com/products/viltrox-85mm-f18-e-mount-sony-markii', manufacturer: 'Viltrox', model: 'AF 85 F1.8 FE', focalLengthMin: 85, focalLengthMax: 85, maxApertureWide: 1.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, { id: 'viltrox-e-135lab', manufacturerUrl: 'https://viltrox.com/products/af-135mm-f1-8-lab-fe', manufacturer: 'Viltrox', model: 'AF 135 F1.8 LAB FE', focalLengthMin: 135, focalLengthMax: 135, maxApertureWide: 1.8, mount: 'E', imageCircle: 'FF', type: 'prime' }, // ── Canon Broadcast UHDgc / DIGISUPER (B4, 2/3"; 4K) ── - { id: 'canon-cj12ex4.3', manufacturer: 'Canon', model: 'UHDgc CJ12ex4.3B', focalLengthMin: 4.3, focalLengthMax: 51.6, maxApertureWide: 1.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, + { id: 'canon-cj12ex4.3', manufacturerUrl: 'https://www.canon-europe.com/broadcast/eng-efp-pro/cj12ex4.3b/', manufacturer: 'Canon', model: 'UHDgc CJ12ex4.3B', focalLengthMin: 4.3, focalLengthMax: 52, maxApertureWide: 1.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, { id: 'canon-cj14ex4.3', manufacturerUrl: 'https://canon.jp/biz/product/camera/pro-imaging/bctv/lineup/cj14e-43b', manufacturer: 'Canon', model: 'UHDgc CJ14ex4.3B', focalLengthMin: 4.3, focalLengthMax: 60, maxApertureWide: 1.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, - { id: 'canon-cj17ex6.2', manufacturer: 'Canon', model: 'UHDgc CJ17ex6.2B', focalLengthMin: 6.2, focalLengthMax: 106, maxApertureWide: 1.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, + { id: 'canon-cj17ex6.2', manufacturerUrl: 'https://www.canon-europe.com/broadcast/cj17ex6-2b/', manufacturer: 'Canon', model: 'UHDgc CJ17ex6.2B', focalLengthMin: 6.2, focalLengthMax: 106, maxApertureWide: 1.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, { id: 'canon-cj18ex7.6', manufacturerUrl: 'https://canon.jp/biz/product/camera/pro-imaging/bctv/lineup/cj18e-76b', manufacturer: 'Canon', model: 'UHDgc CJ18ex7.6B', focalLengthMin: 7.6, focalLengthMax: 137, maxApertureWide: 1.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, { id: 'canon-cj18ex28', manufacturerUrl: 'https://canon.jp/biz/product/camera/pro-imaging/bctv/lineup/cj18e-28b', manufacturer: 'Canon', model: 'UHDgc CJ18ex28B', focalLengthMin: 28, focalLengthMax: 500, maxApertureWide: 2.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, { id: 'canon-cj20ex5', manufacturerUrl: 'https://canon.jp/biz/product/camera/pro-imaging/bctv/lineup/cj20e-5b', manufacturer: 'Canon', model: 'UHDgc CJ20ex5B', focalLengthMin: 5, focalLengthMax: 100, maxApertureWide: 1.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, { id: 'canon-cj24ex7.5', manufacturerUrl: 'https://canon.jp/biz/product/camera/pro-imaging/bctv/lineup/cj24e-75b', manufacturer: 'Canon', model: 'UHDgc CJ24ex7.5B', focalLengthMin: 7.5, focalLengthMax: 180, maxApertureWide: 1.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, { id: 'canon-cj25ex7.6', manufacturerUrl: 'https://canon.jp/biz/product/camera/pro-imaging/bctv/lineup/cj25e-76b', manufacturer: 'Canon', model: 'UHDgc CJ25ex7.6B', focalLengthMin: 7.6, focalLengthMax: 190, maxApertureWide: 1.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, - { id: 'canon-cj45ex13.6', manufacturer: 'Canon', model: 'UHDgc CJ45ex13.6B', focalLengthMin: 13.6, focalLengthMax: 612, maxApertureWide: 2.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, + { id: 'canon-cj45ex13.6', manufacturerUrl: 'https://www.canon-europe.com/support/consumer/products/lenses/bctv/cj45ex136b-iase-v-h.html', manufacturer: 'Canon', model: 'UHDgc CJ45ex13.6B', focalLengthMin: 13.6, focalLengthMax: 612, maxApertureWide: 2.8, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, { id: 'canon-uj90x9', manufacturerUrl: 'https://canon.jp/biz/product/camera/pro-imaging/bctv/lineup/uhd-sp90', manufacturer: 'Canon', model: 'UHD DIGISUPER 90 (UJ90x9B)', focalLengthMin: 9, focalLengthMax: 810, maxApertureWide: 2.4, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, { id: 'canon-uj111x8.3', manufacturerUrl: 'https://canon.jp/biz/product/camera/pro-imaging/bctv/lineup/uhd-sp111', manufacturer: 'Canon', model: 'UHD DIGISUPER 111 (UJ111x8.3B)', focalLengthMin: 8.3, focalLengthMax: 925, maxApertureWide: 1.7, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, { id: 'canon-uj122x8.2', manufacturerUrl: 'https://www.canon-europe.com/lenses/uhd_digisuper_122/specifications/', manufacturer: 'Canon', model: 'UHD DIGISUPER 122 (UJ122x8.2B)', focalLengthMin: 8.2, focalLengthMax: 1000, maxApertureWide: 1.7, mount: 'B4', imageCircle: '2/3', extenderFactors: [2], type: 'zoom' }, @@ -715,10 +715,10 @@ export const LENSES: Lens[] = [ { id: 'nikon-z-dx-18-140', manufacturerUrl: 'https://imaging.nikon.com/imaging/lineup/lens/z-mount/z_dx_18-140mmf35-63_vr/', manufacturer: 'Nikon', model: 'Z DX 18-140 F3.5-6.3', focalLengthMin: 18, focalLengthMax: 140, maxApertureWide: 3.5, mount: 'Z', imageCircle: 'APSC', type: 'zoom' }, { id: 'nikon-z-dx-24', manufacturerUrl: 'https://imaging.nikon.com/imaging/lineup/lens/z-mount/z_dx_24mmf17/', manufacturer: 'Nikon', model: 'Z DX 24 F1.7', focalLengthMin: 24, focalLengthMax: 24, maxApertureWide: 1.7, mount: 'Z', imageCircle: 'APSC', type: 'prime' }, // ── L-Mount (weitere) ── - { id: 'leica-sl-24-70', manufacturer: 'Leica', model: 'SL 24-70 F2.8', focalLengthMin: 24, focalLengthMax: 70, maxApertureWide: 2.8, mount: 'L', imageCircle: 'FF', type: 'zoom' }, - { id: 'leica-sl-24-90', manufacturer: 'Leica', model: 'SL 24-90 F2.8-4', focalLengthMin: 24, focalLengthMax: 90, maxApertureWide: 2.8, mount: 'L', imageCircle: 'FF', type: 'zoom' }, - { id: 'leica-sl-90-280', manufacturer: 'Leica', model: 'SL 90-280 F2.8-4', focalLengthMin: 90, focalLengthMax: 280, maxApertureWide: 2.8, mount: 'L', imageCircle: 'FF', type: 'zoom' }, - { id: 'leica-sl-50-14', manufacturer: 'Leica', model: 'SL 50 F1.4', focalLengthMin: 50, focalLengthMax: 50, maxApertureWide: 1.4, mount: 'L', imageCircle: 'FF', type: 'prime' }, + { id: 'leica-sl-24-70', manufacturerUrl: 'https://leica-camera.com/en-US/photography/lenses/sl/vario-elmarit-sl-24-70-f2-8-asph-black-finish', manufacturer: 'Leica', model: 'SL 24-70 F2.8', focalLengthMin: 24, focalLengthMax: 70, maxApertureWide: 2.8, mount: 'L', imageCircle: 'FF', type: 'zoom' }, + { id: 'leica-sl-24-90', manufacturerUrl: 'https://leica-camera.com/sites/default/files/pm-55652-Datenblatt_Vario-Elmarit-SL_24-90_en.pdf', manufacturer: 'Leica', model: 'SL 24-90 F2.8-4', focalLengthMin: 24, focalLengthMax: 90, maxApertureWide: 2.8, mount: 'L', imageCircle: 'FF', type: 'zoom' }, + { id: 'leica-sl-90-280', manufacturerUrl: 'https://leica-camera.com/sites/default/files/pm-55506-Datenblatt_APO-Vario-Elmarit-SL_90-280_en.pdf', manufacturer: 'Leica', model: 'SL 90-280 F2.8-4', focalLengthMin: 90, focalLengthMax: 280, maxApertureWide: 2.8, mount: 'L', imageCircle: 'FF', type: 'zoom' }, + { id: 'leica-sl-50-14', manufacturerUrl: 'https://leica-camera.com/sites/default/files/pm-55409-160915_Datenblatt_Summilux-SL-50mm-ASPH_e.pdf', manufacturer: 'Leica', model: 'SL 50 F1.4', focalLengthMin: 50, focalLengthMax: 50, maxApertureWide: 1.4, mount: 'L', imageCircle: 'FF', type: 'prime' }, { id: 'sigma-l-24-70dgdn', manufacturerUrl: 'https://www.sigma-global.com/en/lenses/a024_24_70_28_ii/', manufacturer: 'Sigma', model: '24-70 F2.8 DG DN Art (L)', focalLengthMin: 24, focalLengthMax: 70, maxApertureWide: 2.8, mount: 'L', imageCircle: 'FF', type: 'zoom' }, { id: 'sigma-l-70-200dgdn', manufacturerUrl: 'https://www.sigma-global.com/en/lenses/s023_70_200_28/', manufacturer: 'Sigma', model: '70-200 F2.8 DG DN OS Sports (L)', focalLengthMin: 70, focalLengthMax: 200, maxApertureWide: 2.8, mount: 'L', imageCircle: 'FF', type: 'zoom' }, // ── Canon CN-E Cine (EF, Full Frame) ── diff --git a/src/i18n/de/library.ts b/src/i18n/de/library.ts index 5508d71..1a07f24 100644 --- a/src/i18n/de/library.ts +++ b/src/i18n/de/library.ts @@ -63,13 +63,14 @@ export const library: Record = { 'library.autoUpload.hint': 'Eigene und geänderte Kameras und Objektive gehen beim Start und wenige Sekunden nach jeder Änderung hoch; die Bibliothek ordnet sie über Hersteller und Modell zu.', 'library.upload.summary': - 'Eigene Einträge: {live} live oder abgeglichen, {waiting} warten auf Moderation, {blocked} blockiert, {failed} fehlgeschlagen.', + 'Eigene Einträge: {live} live, {waiting} warten auf Moderation, {blocked} blockiert, {failed} fehlgeschlagen.', 'library.upload.none': 'Noch nicht in der Gerätebibliothek.', 'library.upload.changed': 'Seit dem letzten Hochladen geändert — geht beim nächsten Abgleich hoch.', 'library.upload.created': 'Als neues Gerät hochgeladen — wartet auf Moderation.', 'library.upload.editProposed': 'Als nächste Version eines vorhandenen Geräts hochgeladen — wartet auf Moderation.', 'library.upload.pendingUpdated': 'Der wartende Upload wurde durch diese Fassung ersetzt.', 'library.upload.approved': 'Live in der Gerätebibliothek.', + 'library.upload.inSyncPending': 'Hochgeladen — wartet noch auf Moderation.', 'library.upload.inSync': 'Die Gerätebibliothek führt genau diesen Stand.', 'library.upload.blocked': 'Von den Prüfungen der Bibliothek blockiert.', 'library.upload.error': 'Hochladen fehlgeschlagen.', diff --git a/src/library/upload.ts b/src/library/upload.ts index ca0ede2..f618615 100644 --- a/src/library/upload.ts +++ b/src/library/upload.ts @@ -8,8 +8,11 @@ // Version, statt doppelt angelegt zu werden. // // Je Eintrag merkt sich das Protokoll den Hash dessen, was zuletzt ankam. -// Erneut geschickt wird nur, was sich seitdem geaendert hat oder mit -// `error` endete — `blocked` mit gleichem Inhalt waere wieder blockiert. Der +// Erneut geschickt wird nur, was sich seitdem geaendert hat, mit `error` +// endete oder noch in der Moderation wartet — Letzteres, damit die Anzeige +// „live" wird, sobald jemand freigegeben hat (der Server antwortet dann +// `in-sync` mit `moderation: 'approved'`). `blocked` mit gleichem Inhalt +// waere wieder blockiert. Der // Knopf „Sync now" schickt trotzdem alles (`force`): der Server antwortet bei // Unveraendertem mit `in-sync`, und eine inzwischen geaenderte Pruefung auf // dem Server wird so sichtbar. @@ -23,6 +26,8 @@ export interface UploadRecord { hash: string; state: UploadState; slug?: string; + /** Stand in der Moderation, wie der Server ihn meldet — auch bei `in-sync`. */ + moderation?: 'pending' | 'approved'; at: string; error?: string; /** Warum blockiert — wie der Server es sagt (`kind` je Befund). */ @@ -74,10 +79,19 @@ export function pendingUploads(items: LibraryItem[], ledger: UploadLedger, force return items.map(toUploadItem).filter((u) => { if (force) return true; const r = ledger.records[u.localId]; - return !r || r.hash !== u.hash || r.state === 'error'; + return !r || r.hash !== u.hash || r.state === 'error' || uploadBucket(r) === 'waiting'; }); } +/** Wohin ein Ergebnis fuer die Anzeige gehoert. Aeltere Protokolle ohne + * `moderation` schliessen aus dem Zustand. */ +export function uploadBucket(r: UploadRecord): 'live' | 'waiting' | 'blocked' | 'failed' { + if (r.state === 'blocked') return 'blocked'; + if (r.state === 'error') return 'failed'; + if (r.moderation) return r.moderation === 'approved' ? 'live' : 'waiting'; + return r.state === 'approved' || r.state === 'in-sync' ? 'live' : 'waiting'; +} + const befunde = (f: unknown): string[] | undefined => Array.isArray(f) ? f.map((x) => (x && typeof x === 'object' ? String((x as { kind?: unknown; message?: unknown }).kind ?? (x as { message?: unknown }).message ?? '') : String(x))).filter(Boolean) @@ -100,6 +114,7 @@ export function applyUploadResults( state: r.state, // `in-sync` ohne Slug (unveraendert, Server nannte ihn nicht): den alten behalten. ...((r.slug ?? vorher?.slug) ? { slug: r.slug ?? vorher?.slug } : {}), + ...(r.moderation ? { moderation: r.moderation } : {}), at, ...(r.error ? { error: r.error } : {}), ...(befunde(r.findings)?.length ? { findings: befunde(r.findings) } : {}), diff --git a/src/library/uploadText.ts b/src/library/uploadText.ts index 1433e81..f714002 100644 --- a/src/library/uploadText.ts +++ b/src/library/uploadText.ts @@ -7,6 +7,10 @@ type T = (key: string, en: string) => string; export function uploadStateText(t: T, r: UploadRecord | undefined, changed: boolean): string { if (!r) return t('library.upload.none', 'Not in the device library yet.'); if (changed) return t('library.upload.changed', 'Changed since the last upload — goes up with the next sync.'); + if (r.state === 'in-sync' || r.state === 'approved') { + if (r.moderation === 'pending') return t('library.upload.inSyncPending', 'Uploaded — still waiting for moderation.'); + if (r.moderation === 'approved') return t('library.upload.approved', 'Live in the device library.'); + } const TEXT: Record = { created: t('library.upload.created', 'Uploaded as a new device — waiting for moderation.'), 'edit-proposed': t('library.upload.editProposed', 'Uploaded as the next version of an existing device — waiting for moderation.'), diff --git a/src/utils/deviceLibraryClient.ts b/src/utils/deviceLibraryClient.ts index 0e0dfb2..313e7f8 100644 --- a/src/utils/deviceLibraryClient.ts +++ b/src/utils/deviceLibraryClient.ts @@ -254,6 +254,8 @@ export interface UploadResult { localId: string state: UploadState slug?: string + /** Stand in der Moderation — auch bei `in-sync`: wartet noch oder ist live. */ + moderation?: 'pending' | 'approved' findings?: unknown error?: string }