From 855405362d34b8963ecbaa1d640670fcfa89acea Mon Sep 17 00:00:00 2001 From: red_prs Date: Wed, 4 Nov 2026 11:33:31 +0900 Subject: [PATCH 1/2] feat(files): add Frame No. sort and contact-sheet order Film Strip Sort and the contact-sheet Order gain a Frame No. option. It orders by the metadata Roll, then the Frame No., the file name a tie. The name, date and scene orders are unchanged. Adds the label to the four catalogs and updates the frames and contact_sheet guide panels (en, de, ja, ko, zh_CN). Co-Authored-By: Claude Code --- docs/USER_GUIDE.md | 4 ++-- negpy/desktop/session.py | 13 +++++++++++-- negpy/desktop/view/sidebar/files.py | 8 ++++++-- negpy/desktop/view/widgets/contact_sheet_dialog.py | 9 ++++++--- negpy/services/assets/search.py | 2 ++ negpy/services/export/contact_sheet_layout.py | 2 +- negpy/services/export/contact_sheet_roll.py | 11 +++++++++++ negpy/translations/USER_GUIDE.de.md | 8 ++++---- negpy/translations/USER_GUIDE.en.md | 4 ++-- negpy/translations/USER_GUIDE.ja.md | 8 ++++---- negpy/translations/USER_GUIDE.ko.md | 8 ++++---- negpy/translations/USER_GUIDE.zh_CN.md | 8 ++++---- negpy/translations/de.json | 3 ++- negpy/translations/ja.json | 3 ++- negpy/translations/ko.json | 3 ++- negpy/translations/zh_CN.json | 3 ++- 16 files changed, 65 insertions(+), 32 deletions(-) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 1def3f736..a8af242bd 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -134,7 +134,7 @@ The toolbar's **Roll** row holds: * The roll's name and its count of frames, keepers and rejects. * The **filter box**: type part of a file name, or terms such as `film:portra iso:>=400` (below). The search button, or **Enter**, searches the whole library. -* **Sort**: by name, date or scene. +* **Sort**: by name, date, scene, or by roll and frame number. * **Sheet filter**: all frames, keepers only, hide rejected, or unmarked only. The button is highlighted while a filter hides frames. * **Roll Settings** (the tag): gear and film details for this frame, the selection or the roll. * **Apply**: copies chosen settings of this frame to the selection or the roll. @@ -711,7 +711,7 @@ On the **Alongside Exports** tab. A contact sheet shows the whole roll as film s * **Contact Sheet…**: opens the sheet. Drag an edge of the paper to resize it. * **Film format**: 35mm, 35mm Half Frame or 120 (with **Frame size** for 120). * **Paper**, **Width**, **Height** and **DPI**: the sheet's size and resolution. The default paper holds a 36-exposure roll. -* **Order**: by file name, by date, or by scene with a new strip or row for each scene. +* **Order**: by file name, by date, by roll and frame number, or by scene with a new strip or row for each scene. * **Print**: **As Edited**, or **Straight Proof**: the whole roll at one exposure and normal contrast, so you see which negatives are thin or dense. * **Pick Frames**: click a frame to leave it out or put it back. Rejects start out. * **Edge Print**: the film maker's markings along the edges, such as the film name, frame numbers and the colored lines a stock prints by its perforations. diff --git a/negpy/desktop/session.py b/negpy/desktop/session.py index 5ef618228..698b46064 100644 --- a/negpy/desktop/session.py +++ b/negpy/desktop/session.py @@ -522,7 +522,7 @@ def __init__(self, state: AppState, facts_provider: Optional[Any] = None): # Returns {asset hash: facts}. Without one, plain queries see file facts only # (name, ext, date), which is all a model built outside a session can know. self._facts_provider = facts_provider - self._sort_order = "name" # "name" | "date" | "scene" + self._sort_order = "name" # "name" | "date" | "scene" | "frame" self._sort_descending = False self._filter_text: str = "" self._filter_regex: bool = False @@ -557,6 +557,9 @@ def _rebuild_indices(self) -> None: order = self.effective_sort_order if order == "date": indices.sort(key=lambda i: _asset_mtime(files[i]), reverse=self._sort_descending) + elif order == "frame": + facts = self._facts_provider() if self._facts_provider else {} + indices.sort(key=lambda i: self._frame_rank(files[i], facts), reverse=self._sort_descending) else: indices.sort(key=lambda i: files[i]["name"].lower(), reverse=self._sort_descending) if order == "scene": @@ -638,7 +641,7 @@ def filter_text(self) -> str: return self._filter_text def set_sort_order(self, order: str) -> None: - if order not in ("name", "date", "scene"): + if order not in ("name", "date", "scene", "frame"): order = "name" self._sort_order = order self._apply_reindex() @@ -666,6 +669,12 @@ def _scene_rank(self, file_info: dict) -> tuple: return (1, 0) return (0, -scene[0] if self._sort_descending else scene[0]) + def _frame_rank(self, file_info: dict, facts: dict) -> tuple: + """Frame No. sort: the frame's metadata Roll, then its Frame No., the name a tie.""" + f = facts.get(file_info["hash"]) or {} + num = f.get("frame") + return (f.get("capture_roll", ""), num if num is not None else 0, file_info["name"].lower()) + def scene_runs(self) -> list[tuple[Optional[int], int, int]]: """``(scene ordinal or None, first row, last row)`` for each scene's block of display rows, frames in no scene as the last block. Empty unless sorted by Scene.""" diff --git a/negpy/desktop/view/sidebar/files.py b/negpy/desktop/view/sidebar/files.py index 58c868093..0b83f79cb 100644 --- a/negpy/desktop/view/sidebar/files.py +++ b/negpy/desktop/view/sidebar/files.py @@ -1503,9 +1503,12 @@ def _init_ui(self) -> None: # The frames' own order; the Library's roll list has a Sort of its own. self.sort_btn = SortButton( - (("name", tr("Name")), ("date", tr("Date")), ("scene", tr("Scene"))), tr("Sort the frames in the Film Strip") + (("name", tr("Name")), ("date", tr("Date")), ("scene", tr("Scene")), ("frame", tr("Frame No."))), + tr("Sort the frames in the Film Strip"), + ) + self.act_sort_name, self.act_sort_date, self.act_sort_scene, self.act_sort_frame = ( + self.sort_btn.order_action(k) for k in ("name", "date", "scene", "frame") ) - self.act_sort_name, self.act_sort_date, self.act_sort_scene = (self.sort_btn.order_action(k) for k in ("name", "date", "scene")) self.act_sort_asc, self.act_sort_desc = self.sort_btn.ascending_action, self.sort_btn.descending_action self.act_sort_scene.setVisible(False) self.sort_btn.order_selected.connect(self._apply_sort_order) @@ -2075,6 +2078,7 @@ def _sync_sort_menu(self) -> None: self.act_sort_name.setChecked(order == "name") self.act_sort_date.setChecked(order == "date") self.act_sort_scene.setChecked(order == "scene") + self.act_sort_frame.setChecked(order == "frame") def _apply_sheet_filter(self, mode: str, save: bool = True) -> None: model = self.session.asset_model diff --git a/negpy/desktop/view/widgets/contact_sheet_dialog.py b/negpy/desktop/view/widgets/contact_sheet_dialog.py index cfe9f5491..621b041a9 100644 --- a/negpy/desktop/view/widgets/contact_sheet_dialog.py +++ b/negpy/desktop/view/widgets/contact_sheet_dialog.py @@ -53,6 +53,7 @@ SheetLook, StraightProof, creation_order, + frame_order, print_aspect, scene_breaks, scene_of, @@ -66,8 +67,8 @@ _CUSTOM_PAPER = len(ILFORD_PAPERS) _PRINTS = (tr("As Edited"), tr("Straight Proof")) _AS_EDITED, _STRAIGHT_PROOF = 0, 1 -_ORDERS = (mark("Name"), mark("Date"), mark("Scene")) -_BY_NAME, _BY_DATE, _BY_SCENE = 0, 1, 2 +_ORDERS = (mark("Name"), mark("Date"), mark("Scene"), mark("Frame No.")) +_BY_NAME, _BY_DATE, _BY_SCENE, _BY_FRAME = 0, 1, 2, 3 _EDITED_TILES, _PROOF_TILES, _SCENE_PROOF_TILES = 0, 1, 2 _CANVAS_PAD = 28.0 _TILE_REPAINT_MS = 120 @@ -415,7 +416,7 @@ def _build_controls(self, settings: ContactSheetSettings) -> QWidget: self.order_btn = StepCombo.of( [tr(order) for order in _ORDERS], - tr("Lay the frames out by file name, in the order they were taken, or with a new strip for each scene."), + tr("Lay the frames out by file name, by the order they were taken, by roll and frame number, or by scene."), ) self.order_btn.set_item_enabled(_BY_SCENE, self._has_scenes) if not self._has_scenes: @@ -651,6 +652,8 @@ def _order_indices(self, order: int) -> list[int]: return scene_order(self._frames) if order == _BY_DATE: return creation_order(self._frames) + if order == _BY_FRAME: + return frame_order(self._frames) return list(range(len(self._frames))) def _on_order_changed(self, index: int) -> None: diff --git a/negpy/services/assets/search.py b/negpy/services/assets/search.py index 8feaf29c0..c04ff3f70 100644 --- a/negpy/services/assets/search.py +++ b/negpy/services/assets/search.py @@ -166,6 +166,8 @@ def facts_for(asset: dict, config: Any = None) -> dict[str, Any]: "format": (meta.format_other if meta.format == "Other" else meta.format).casefold(), "scanning": meta.scanning.casefold(), "roll": " ".join(p for p in (meta.capture_roll, config.process.roll_name or "") if p).casefold(), + # Raw Roll value, not in FIELDS: the Frame No. sort orders by it, not by the combined search term. + "capture_roll": meta.capture_roll.casefold(), "frame": meta.capture_frame, "iso": meta.film_iso, "push": meta.push_pull, diff --git a/negpy/services/export/contact_sheet_layout.py b/negpy/services/export/contact_sheet_layout.py index d9f1899c8..ed6d8ab0f 100644 --- a/negpy/services/export/contact_sheet_layout.py +++ b/negpy/services/export/contact_sheet_layout.py @@ -328,7 +328,7 @@ def frame_turns(upright_aspect: Optional[float], geometry: FilmGeometry, rotatio return 3 -SHEET_ORDERS = ("name", "date", "scene") +SHEET_ORDERS = ("name", "date", "scene", "frame") @dataclass(frozen=True) diff --git a/negpy/services/export/contact_sheet_roll.py b/negpy/services/export/contact_sheet_roll.py index e6962b426..c041bd512 100644 --- a/negpy/services/export/contact_sheet_roll.py +++ b/negpy/services/export/contact_sheet_roll.py @@ -204,6 +204,17 @@ def key(index: int) -> tuple[Any, ...]: return sorted(range(len(frames)), key=key) +def frame_order(frames: Sequence[SheetFrame]) -> list[int]: + """Indices by the frame's Roll and Frame No. from its metadata, the name a tie.""" + + def key(index: int) -> tuple[Any, ...]: + meta = frames[index].config.metadata + num = meta.capture_frame + return (meta.capture_roll.casefold(), num if num is not None else 0, frames[index].name.lower()) + + return sorted(range(len(frames)), key=key) + + def _crop_fractions(config: WorkspaceConfig) -> tuple[float, float]: rect = config.geometry.crop_rect if not rect: diff --git a/negpy/translations/USER_GUIDE.de.md b/negpy/translations/USER_GUIDE.de.md index 98c43977d..8d7825062 100644 --- a/negpy/translations/USER_GUIDE.de.md +++ b/negpy/translations/USER_GUIDE.de.md @@ -18,7 +18,7 @@ Eine Ordnerrolle enthält nur die Dateien ihres Ordners. Eine Datei, die du aus Um die Bilder im Filmstreifen als Rolle zu speichern, nutze den Filmstreifen. **In Ordnerrolle speichern** macht aus Bildern, die alle aus einem Ordner stammen, die Rolle dieses Ordners. Seine anderen Dateien erscheinen als entfernt. **Als virtuelle Rolle speichern…** behält genau diese Bilder, aus beliebigen Ordnern. In Ordnerrolle speichern macht auch aus einer virtuellen Rolle eines Ordners die Rolle dieses Ordners. Die neue Rolle startet mit den Einstellungen, den Kartensperren und den nur in dieser Rolle gemachten Bearbeitungen der geöffneten Rolle. Ein Bild hat in jeder Rolle, die es enthält, dieselbe Bearbeitung. Rechtsklick → **In dieser Rolle unabhängig bearbeiten** gibt ihm in einer Rolle eine eigene Bearbeitung. - + ### Filmstreifen Der **Filmstreifen** unter der Arbeitsfläche zeigt die geöffneten Bilder in einer Reihe. Ziehe seinen oberen Rand, um die Bilder zu vergrößern. @@ -27,7 +27,7 @@ Die Zeile **Rolle** der Werkzeugleiste enthält: * Den Namen der Rolle und die Anzahl der Bilder, der behaltenen und der verworfenen Bilder. * Das **Filterfeld**: Gib einen Teil eines Dateinamens ein oder Begriffe wie `film:portra iso:>=400` (siehe unten). Die Suchschaltfläche oder **Enter** durchsucht die ganze Bibliothek. -* **Sortieren**: nach Name, Datum oder Szene. +* **Sortieren**: nach Name, Datum, Szene oder nach Rolle und Bildnummer. * **Bogenfilter**: alle Bilder, nur behaltene, verworfene ausblenden oder nur unmarkierte. Die Schaltfläche ist hervorgehoben, solange ein Filter Bilder ausblendet. * **Rolleneinstellungen** (das Etikett): Ausrüstung und Filmdetails für dieses Bild, die Auswahl oder die Rolle. * **Anwenden**: kopiert gewählte Einstellungen dieses Bildes auf die Auswahl oder die Rolle. @@ -505,7 +505,7 @@ Zusätzliche Dateien neben dem Abzug. Jede hat links einen Schalter und rechts e * **Auto-Dichte / Gradation**: **Beim Export festschreiben** fixiert die automatische Helligkeit und den Kontrast jedes exportierten Bildes, damit sie sich später nicht ändern. **In Rolle festschreiben** tut das jetzt für jedes sichtbare Bild. * **Bearbeitungs-Sidecar-Dateien**: **Beim Export speichern** schreibt neben jede Quelle eine `.negpy`-Datei mit der Bearbeitung. **Sidecar-Dateien exportieren** schreibt sie für alle sichtbaren Bilder. Rechtsklick auf ein Bild → **Bearbeitung aus Sidecar-Datei laden…** liest eine zurück. - + ### Kontaktbogen Auf dem Tab **Neben dem Export**. Ein Kontaktbogen zeigt die ganze Rolle als Filmstreifen auf einem Blatt Papier, wie ein Kontaktabzug in der Dunkelkammer. @@ -514,7 +514,7 @@ Auf dem Tab **Neben dem Export**. Ein Kontaktbogen zeigt die ganze Rolle als Fil * **Kontaktbogen…**: öffnet den Bogen. Ziehe an einer Kante des Papiers, um die Größe zu ändern. * **Filmformat**: 35mm, 35mm Half Frame oder 120 (bei 120 mit **Bildgröße**). * **Papier**, **Breite**, **Höhe** und **DPI**: Größe und Auflösung des Bogens. Das Standardpapier fasst eine Rolle mit 36 Aufnahmen. -* **Reihenfolge**: nach Dateiname, nach Datum oder nach Szene, mit einem neuen Streifen oder einer neuen Zeile für jede Szene. +* **Reihenfolge**: nach Dateiname, nach Datum, nach Rolle und Bildnummer oder nach Szene, mit einem neuen Streifen oder einer neuen Zeile für jede Szene. * **Abzug**: **Wie bearbeitet** oder **Reiner Probeabzug**: die ganze Rolle mit einer Belichtung und normalem Kontrast. So siehst du, welche Negative dünn oder dicht sind. * **Bilder wählen**: Klicke auf ein Bild, um es auszulassen oder wieder aufzunehmen. Verworfene Bilder sind zu Beginn ausgelassen. * **Randaufdruck**: die Markierungen des Filmherstellers am Rand, zum Beispiel Filmname, Bildnummern und die farbigen Linien, die ein Film neben seinen Perforationen druckt. diff --git a/negpy/translations/USER_GUIDE.en.md b/negpy/translations/USER_GUIDE.en.md index 1def3f736..a8af242bd 100644 --- a/negpy/translations/USER_GUIDE.en.md +++ b/negpy/translations/USER_GUIDE.en.md @@ -134,7 +134,7 @@ The toolbar's **Roll** row holds: * The roll's name and its count of frames, keepers and rejects. * The **filter box**: type part of a file name, or terms such as `film:portra iso:>=400` (below). The search button, or **Enter**, searches the whole library. -* **Sort**: by name, date or scene. +* **Sort**: by name, date, scene, or by roll and frame number. * **Sheet filter**: all frames, keepers only, hide rejected, or unmarked only. The button is highlighted while a filter hides frames. * **Roll Settings** (the tag): gear and film details for this frame, the selection or the roll. * **Apply**: copies chosen settings of this frame to the selection or the roll. @@ -711,7 +711,7 @@ On the **Alongside Exports** tab. A contact sheet shows the whole roll as film s * **Contact Sheet…**: opens the sheet. Drag an edge of the paper to resize it. * **Film format**: 35mm, 35mm Half Frame or 120 (with **Frame size** for 120). * **Paper**, **Width**, **Height** and **DPI**: the sheet's size and resolution. The default paper holds a 36-exposure roll. -* **Order**: by file name, by date, or by scene with a new strip or row for each scene. +* **Order**: by file name, by date, by roll and frame number, or by scene with a new strip or row for each scene. * **Print**: **As Edited**, or **Straight Proof**: the whole roll at one exposure and normal contrast, so you see which negatives are thin or dense. * **Pick Frames**: click a frame to leave it out or put it back. Rejects start out. * **Edge Print**: the film maker's markings along the edges, such as the film name, frame numbers and the colored lines a stock prints by its perforations. diff --git a/negpy/translations/USER_GUIDE.ja.md b/negpy/translations/USER_GUIDE.ja.md index b301b54d9..7ae5e8d08 100644 --- a/negpy/translations/USER_GUIDE.ja.md +++ b/negpy/translations/USER_GUIDE.ja.md @@ -6,7 +6,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室 ロールを読み込み、フィルムモードとスキャン装置を選びます。光源・レンズ補正、トリミング、ロール解析を行ってから、各フレームの濃度、コントラスト、色補正、修復を調整し、最後に書き出します。カード見出しのフレーム/ロールで適用範囲を選び、ロール設定への復帰で共有値を戻せます。個々のトリミング位置と回転はフレームに保持されます。 - + ## フィルムストリップ キャンバスの下の**フィルムストリップ**は、開いているフレームを1列に表示します。上端をドラッグすると、フレームが大きくなります。 @@ -15,7 +15,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室 - ロール名と、フレーム数、キーパー数、不採用数。 - **絞り込みボックス**:ファイル名の一部、または `film:portra iso:>=400` のような条件(下記)を入力します。検索ボタンまたは **Enter** でライブラリ全体を検索します。 -- **並べ替え**:名前、日付、シーン。 +- **並べ替え**:名前、日付、シーン、またはロールとフレーム番号。 - **シートフィルター**:すべてのフレーム、キーパーのみ、拒否されたものを非表示、マークなしのみ。フィルターでフレームが隠れている間、ボタンが強調表示されます。 - **ロール設定**(タグのアイコン):このフレーム、選択、またはロールの機材とフィルムの情報。 - **適用**:このフレームの選んだ設定を、選択またはロールにコピーします。 @@ -509,7 +509,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室 - **自動濃度/グレード**:**書き出し時に確定**は、書き出した各フレームの自動の明るさとコントラストを固定し、後で変わらないようにします。**ロールに確定**は表示中のすべてのフレームで今すぐ固定します。 - **編集サイドカー**:**書き出し時に保存**は各元ファイルの横に編集を含む `.negpy` ファイルを書きます。**サイドカーのエクスポート**は表示中のすべてのフレームで書きます。フレームを右クリック →**サイドカーから編集をロード…**で読み戻します。 - + ## コンタクトシート **書き出しと一緒に**タブにあります。コンタクトシートは、暗室のコンタクトプリントのように、ロール全体をフィルムの帯として 1 枚の紙に並べます。 @@ -518,7 +518,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室 - **コンタクトシート…**:シートを開きます。紙の端をドラッグしてサイズを変えます。 - **フィルムフォーマット**:35mm、35mm ハーフサイズ、120(120 は**フレームサイズ**も設定)。 - **印画紙**、**幅**、**高さ**、**DPI**:シートのサイズと解像度。既定の紙で 36 枚撮りのロールが収まります。 -- **順序**:ファイル名順、日付順、またはシーンごとに新しい帯か行を始めるシーン順。 +- **順序**:ファイル名順、日付順、ロールとフレーム番号順、またはシーンごとに新しい帯か行を始めるシーン順。 - **プリント**:**編集済み**、または**ストレートプルーフ**:ロール全体を一つの露光と標準のコントラストでプリントし、どのネガが薄いか濃いかを見られます。 - **フレームの選択**:フレームをクリックして外したり戻したりします。不採用のフレームは最初から外れています。 - **エッジプリント**:フィルム名やフレーム番号、パーフォレーションの片側にある色付きラインなど、フィルムの縁のメーカーの印字。 diff --git a/negpy/translations/USER_GUIDE.ko.md b/negpy/translations/USER_GUIDE.ko.md index 7f52b1aea..e9e3dc6a7 100644 --- a/negpy/translations/USER_GUIDE.ko.md +++ b/negpy/translations/USER_GUIDE.ko.md @@ -6,7 +6,7 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입 롤을 불러와 필름 모드와 스캔 장치를 선택합니다. 광원과 렌즈를 보정하고 프레임 자르기와 롤 분석을 수행한 다음, 각 프레임의 농도·대비·색 보정·복구를 조정하여 내보냅니다. 카드 제목의 프레임/롤 버튼으로 적용 범위를 정하고 롤 설정으로 되돌려 공유 값을 복원할 수 있습니다. 자르기 위치와 회전은 프레임별로 유지됩니다. - + ## 필름 스트립 캔버스 아래의 **필름 스트립**은 열린 프레임을 한 줄로 보여줍니다. 위쪽 가장자리를 드래그하면 프레임이 커집니다. @@ -15,7 +15,7 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입 - 롤 이름과 프레임, 키퍼, 거부 수. - **필터 상자**: 파일 이름 일부나 `film:portra iso:>=400` 같은 조건(아래)을 입력합니다. 검색 버튼이나 **Enter**는 라이브러리 전체를 검색합니다. -- **정렬**: 이름, 날짜, 장면. +- **정렬**: 이름, 날짜, 장면, 또는 롤과 프레임 번호. - **시트 필터**: 모든 프레임, 키퍼 전용, 거부됨 숨기기, 표시되지 않은 것만. 필터가 프레임을 숨기는 동안 버튼이 강조됩니다. - **롤 설정**(태그 아이콘): 이 프레임, 선택 항목 또는 롤의 장비와 필름 정보. - **적용**: 이 프레임의 선택한 설정을 선택 항목이나 롤에 복사합니다. @@ -509,7 +509,7 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입 - **자동 농도 / 등급**: **내보낼 때 고정**은 내보낸 각 프레임의 자동 밝기와 대비를 고정해 나중에 바뀌지 않게 합니다. **롤에 고정**은 보이는 모든 프레임에 지금 적용합니다. - **편집 사이드카**: **내보내기 시 저장**은 각 원본 옆에 편집을 담은 `.negpy` 파일을 씁니다. **사이드카 내보내기**는 보이는 모든 프레임에 씁니다. 프레임 오른쪽 클릭 → **사이드카에서 편집 로드…**로 다시 읽습니다. - + ## 컨택트 시트 **내보내기와 함께** 탭에 있습니다. 컨택트 시트는 암실의 밀착 인화처럼 롤 전체를 필름 띠로 종이 한 장에 보여줍니다. @@ -518,7 +518,7 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입 - **컨택트 시트…**: 시트를 엽니다. 종이 가장자리를 드래그해 크기를 바꿉니다. - **필름 규격**: 35mm, 35mm 하프 프레임, 120(120은 **프레임 크기**도 설정). - **인화지**, **폭**, **높이**, **DPI**: 시트의 크기와 해상도입니다. 기본 용지에 36컷 롤이 다 들어갑니다. -- **순서**: 파일 이름순, 날짜순, 또는 장면마다 새 띠나 줄을 시작하는 장면순. +- **순서**: 파일 이름순, 날짜순, 롤과 프레임 번호순, 또는 장면마다 새 띠나 줄을 시작하는 장면순. - **인화**: **편집됨**, 또는 **스트레이트 프루프**: 롤 전체를 한 노출과 보통 대비로 인화해 어느 네거티브가 옅거나 짙은지 보여줍니다. - **프레임 선택**: 프레임을 클릭해 빼거나 다시 넣습니다. 거부한 프레임은 처음부터 빠져 있습니다. - **가장자리 인화**: 필름 이름, 프레임 번호, 퍼포레이션 한쪽의 색선 같은 필름 가장자리의 제조사 표시입니다. diff --git a/negpy/translations/USER_GUIDE.zh_CN.md b/negpy/translations/USER_GUIDE.zh_CN.md index f89da1f33..7c55af274 100644 --- a/negpy/translations/USER_GUIDE.zh_CN.md +++ b/negpy/translations/USER_GUIDE.zh_CN.md @@ -6,7 +6,7 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样 载入胶卷后,选择胶片模式与扫描装置,校正光源和镜头,裁切画面并分析胶卷,再调整单帧密度、反差、滤色与修复,最后导出。卡片标题的画面/胶卷按钮控制应用范围,恢复胶卷设置可拉回共享值。单帧裁切位置与旋转保留在画面上。 - + ## 胶片条 画布下方的**胶片条**把打开的帧排成一行。拖动其上边缘可放大帧。 @@ -15,7 +15,7 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样 - 胶卷名称,以及帧数、保留数和拒绝数。 - **筛选框**:输入文件名的一部分,或 `film:portra iso:>=400` 这样的条件(见下)。搜索按钮或 **Enter** 会搜索整个图库。 -- **排序**:按名称、日期或场景。 +- **排序**:按名称、日期、场景,或按胶卷和帧号。 - **画面筛选**:所有画面、仅保留画面、隐藏拒绝画面或仅未标记画面。筛选隐藏画面时,按钮会高亮。 - **胶卷设置**(标签图标):此帧、所选帧或胶卷的器材和胶片信息。 - **应用**:把此帧选定的设置复制到所选帧或整卷。 @@ -509,7 +509,7 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样 - **自动密度 / 反差**:**导出时固化**会固定每个导出帧的自动亮度和反差,使其以后不再变化。**固化到胶卷**立即对所有可见帧执行。 - **编辑附属文件**:**导出时保存**在每个源文件旁写入含编辑内容的 `.negpy` 文件。**导出附属文件**为所有可见帧写入。右键点击某帧 → **从附属文件加载编辑…**可读回。 - + ## 接触印相 位于**随导出**选项卡。接触印相像暗房印样一样,把整卷以胶片条的形式排在一张纸上。 @@ -518,7 +518,7 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样 - **接触印相…**:打开印样。拖动纸张边缘可改变大小。 - **胶片画幅**:35mm、35mm 半格或 120(120 还需设置**画面尺寸**)。 - **相纸**、**宽度**、**高度**和 **DPI**:印样的尺寸和分辨率。默认相纸可容纳 36 张一卷。 -- **顺序**:按文件名、按日期,或按场景(每个场景另起一条或一行)。 +- **顺序**:按文件名、按日期、按胶卷和帧号,或按场景(每个场景另起一条或一行)。 - **印相**:**按编辑结果**,或**直接样片**:整卷以同一曝光和常规反差印出,便于看出哪些底片偏薄或偏厚。 - **选择画面**:点击某帧可将其排除或放回。拒绝的帧一开始就被排除。 - **边缘印相**:胶片边缘的厂商标记,如胶片名称、帧编号和齿孔一侧的彩色线。 diff --git a/negpy/translations/de.json b/negpy/translations/de.json index 9935e4173..b24ef1551 100644 --- a/negpy/translations/de.json +++ b/negpy/translations/de.json @@ -1085,6 +1085,7 @@ "Frame Counter": "Bildzähler", "Frame Info": "Bildinfo", "Frame Info: {value}": "Bildinfo: {value}", + "Frame No.": "Bildnummer", "Frame info on the canvas: off, basic or full": "Bildinfo auf der Arbeitsfläche: aus, einfach oder vollständig", "Frame length on the loaded film; Auto where the holder fixes it": "Bildlänge auf dem geladenen Film; Auto, wo der Halter sie festlegt", "Frame or Roll": "Bild oder Rolle", @@ -1375,7 +1376,7 @@ "Later edits never delete a work print; this is the only way to delete one.": "Spätere Bearbeitungen löschen nie einen Arbeitsabzug. Nur so lässt er sich löschen.", "Latitude and longitude, or a pasted OpenStreetMap or Google Maps link.": "Breiten- und Längengrad oder ein eingefügter OpenStreetMap- oder Google-Maps-Link.", "Lay every visible frame out as film strips on photographic paper, then export the sheet": "Alle sichtbaren Bilder als Filmstreifen auf Fotopapier anordnen und den Bogen exportieren", - "Lay the frames out by file name, in the order they were taken, or with a new strip for each scene.": "Ordnet die Bilder nach Dateiname, nach Aufnahmereihenfolge oder mit einem neuen Streifen für jede Szene an.", + "Lay the frames out by file name, by the order they were taken, by roll and frame number, or by scene.": "Ordnen Sie die Bilder nach Dateiname, Aufnahmereihenfolge, Rolle und Bildnummer oder Szene an.", "Leave at the default unless you know why you need to change it.": "Belasse den Standardwert, außer du weißt, warum du ihn ändern musst.", "Leave frames in the rejected state out of the run; the numbers fill the frames that remain.": "Verworfene Bilder bleiben im Durchlauf unberücksichtigt. Die Nummern füllen die übrigen Bilder.", "Leave the current view (peek, split, strip, zone overlay) or cancel the active tool; Crop and Draw Region undo their changes": "Die aktuelle Ansicht (Vorschau, Teilung, Streifen, Zonenüberlagerung) verlassen oder das aktive Werkzeug abbrechen. Zuschnitt und Bereich zeichnen machen ihre Änderungen rückgängig", diff --git a/negpy/translations/ja.json b/negpy/translations/ja.json index 3fefc0c81..92428b51a 100644 --- a/negpy/translations/ja.json +++ b/negpy/translations/ja.json @@ -1085,6 +1085,7 @@ "Frame Counter": "フレーム番号", "Frame Info": "フレーム情報", "Frame Info: {value}": "フレーム情報: {value}", + "Frame No.": "フレーム番号", "Frame info on the canvas: off, basic or full": "キャンバス上のフレーム情報: オフ、ベーシック、またはフル", "Frame length on the loaded film; Auto where the holder fixes it": "装填したフィルムのフレーム長。ホルダーで決まる場合は自動", "Frame or Roll": "フレームまたはロール", @@ -1375,7 +1376,7 @@ "Later edits never delete a work print; this is the only way to delete one.": "後の編集でワークプリントが消えることはありません。削除できるのはここだけです。", "Latitude and longitude, or a pasted OpenStreetMap or Google Maps link.": "緯度と経度、または貼り付けた OpenStreetMap か Google マップのリンク。", "Lay every visible frame out as film strips on photographic paper, then export the sheet": "表示されているすべてのフレームをフィルム ストリップとして印画紙に配置し、シートをエクスポートします。", - "Lay the frames out by file name, in the order they were taken, or with a new strip for each scene.": "フレームをファイル名順か撮影した順に並べるか、シーンごとに新しいストリップを始めます。", + "Lay the frames out by file name, by the order they were taken, by roll and frame number, or by scene.": "フレームをファイル名、撮影順、ロールとフレーム番号、シーンで並べます。", "Leave at the default unless you know why you need to change it.": "変更する必要がある理由がわからない場合は、デフォルトのままにしておきます。", "Leave frames in the rejected state out of the run; the numbers fill the frames that remain.": "リジェクト済みのフレームを除外し、残りに連番を付けます。", "Leave the current view (peek, split, strip, zone overlay) or cancel the active tool; Crop and Draw Region undo their changes": "現在のビュー(のぞき見、分割、ストリップ、ゾーンオーバーレイ)を終了するか、アクティブなツールをキャンセルします。トリミングと範囲を描画は変更を元に戻します", diff --git a/negpy/translations/ko.json b/negpy/translations/ko.json index ca1e39fe5..7daa7d4b7 100644 --- a/negpy/translations/ko.json +++ b/negpy/translations/ko.json @@ -1085,6 +1085,7 @@ "Frame Counter": "프레임 번호", "Frame Info": "프레임 정보", "Frame Info: {value}": "프레임 정보: {value}", + "Frame No.": "프레임 번호", "Frame info on the canvas: off, basic or full": "캔버스의 프레임 정보: 꺼짐, 기본 또는 전체", "Frame length on the loaded film; Auto where the holder fixes it": "로드된 필름의 프레임 길이입니다. 홀더가 정하는 경우 자동", "Frame or Roll": "프레임 또는 롤", @@ -1375,7 +1376,7 @@ "Later edits never delete a work print; this is the only way to delete one.": "이후의 편집으로 워크 프린트가 지워지지 않습니다. 삭제하는 방법은 이것뿐입니다.", "Latitude and longitude, or a pasted OpenStreetMap or Google Maps link.": "위도와 경도, 또는 붙여 넣은 OpenStreetMap이나 Google Maps 링크.", "Lay every visible frame out as film strips on photographic paper, then export the sheet": "눈에 보이는 모든 프레임을 인화지 위에 필름 스트립으로 배치한 다음 시트를 내보냅니다.", - "Lay the frames out by file name, in the order they were taken, or with a new strip for each scene.": "프레임을 파일 이름순이나 찍은 순서대로 놓거나, 장면마다 새 스트립을 시작합니다.", + "Lay the frames out by file name, by the order they were taken, by roll and frame number, or by scene.": "프레임을 파일 이름, 촬영 순서, 롤과 프레임 번호, 장면으로 배치합니다.", "Leave at the default unless you know why you need to change it.": "변경해야 하는 이유를 알지 못하는 한 기본값을 그대로 유지합니다.", "Leave frames in the rejected state out of the run; the numbers fill the frames that remain.": "거부된 프레임은 제외하고 남은 프레임에 연속 번호를 지정합니다.", "Leave the current view (peek, split, strip, zone overlay) or cancel the active tool; Crop and Draw Region undo their changes": "현재 보기(피크, 분할, 스트립, 존 오버레이)를 종료하거나 활성 도구를 취소합니다. 자르기와 영역 그리기는 변경을 되돌립니다.", diff --git a/negpy/translations/zh_CN.json b/negpy/translations/zh_CN.json index dfc0acb03..d0230056d 100644 --- a/negpy/translations/zh_CN.json +++ b/negpy/translations/zh_CN.json @@ -1085,6 +1085,7 @@ "Frame Counter": "画面计数", "Frame Info": "画面信息", "Frame Info: {value}": "画面信息:{value}", + "Frame No.": "帧号", "Frame info on the canvas: off, basic or full": "画布画面信息:关闭、基本或完整", "Frame length on the loaded film; Auto where the holder fixes it": "已载入胶片的画幅长度;片夹固定时选择自动", "Frame or Roll": "画面或胶卷", @@ -1375,7 +1376,7 @@ "Later edits never delete a work print; this is the only way to delete one.": "之后的编辑不会删除工作样片;只有这里能删除它。", "Latitude and longitude, or a pasted OpenStreetMap or Google Maps link.": "经纬度,或粘贴的 OpenStreetMap 或 Google 地图链接。", "Lay every visible frame out as film strips on photographic paper, then export the sheet": "将所有可见画面按胶片条排列在相纸上并导出", - "Lay the frames out by file name, in the order they were taken, or with a new strip for each scene.": "按文件名或拍摄顺序排列画面,或为每个场景另起一条。", + "Lay the frames out by file name, by the order they were taken, by roll and frame number, or by scene.": "将帧按文件名、拍摄顺序、卷和帧号或场景排列。", "Leave at the default unless you know why you need to change it.": "除非明确需要更改,请保留默认值。", "Leave frames in the rejected state out of the run; the numbers fill the frames that remain.": "跳过已拒绝的画面,其余画面连续编号。", "Leave the current view (peek, split, strip, zone overlay) or cancel the active tool; Crop and Draw Region undo their changes": "退出当前查看模式,或取消活动工具;裁切和绘制区域会撤销其更改", From 22760bcdbcfc58f15e5b8f921f1c123ac0e8c12b Mon Sep 17 00:00:00 2001 From: Marcin Zawalski Date: Thu, 5 Nov 2026 11:26:19 +0100 Subject: [PATCH 2/2] Fill the vi and nb catalogs for Frame No. sort, trim the Film Strip guide, add order tests --- docs/USER_GUIDE.md | 4 ++-- negpy/translations/USER_GUIDE.de.md | 2 +- negpy/translations/USER_GUIDE.en.md | 4 ++-- negpy/translations/USER_GUIDE.ja.md | 2 +- negpy/translations/USER_GUIDE.ko.md | 2 +- negpy/translations/USER_GUIDE.nb.md | 8 ++++---- negpy/translations/USER_GUIDE.zh_CN.md | 2 +- negpy/translations/nb.json | 3 ++- negpy/translations/vi.json | 3 ++- tests/test_contact_sheet_roll.py | 10 ++++++++++ tests/test_desktop_session.py | 16 ++++++++++++++++ 11 files changed, 42 insertions(+), 14 deletions(-) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index ebddf9b54..d90b9eb1b 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -132,9 +132,9 @@ The **Film Strip** under the canvas shows the open frames in one row. Drag its t The toolbar's **Roll** row holds: -* The roll's name and its count of frames, keepers and rejects. +* The roll's name and count of frames, keepers and rejects. * The **filter box**: type part of a file name, or terms such as `film:portra iso:>=400` (below). The search button, or **Enter**, searches the whole library. -* **Sort**: by name, date, scene, or by roll and frame number. +* **Sort**: name, date, scene or frame number. * **Sheet filter**: all frames, keepers only, hide rejected, or unmarked only. The button is highlighted while a filter hides frames. * **Roll Settings** (the tag): gear and film details for this frame, the selection or the roll. * **Apply**: copies chosen settings of this frame to the selection or the roll. diff --git a/negpy/translations/USER_GUIDE.de.md b/negpy/translations/USER_GUIDE.de.md index 360cfe231..2468e3e31 100644 --- a/negpy/translations/USER_GUIDE.de.md +++ b/negpy/translations/USER_GUIDE.de.md @@ -18,7 +18,7 @@ Eine Ordnerrolle enthält nur die Dateien ihres Ordners. Eine Datei, die du aus Um die Bilder im Filmstreifen als Rolle zu speichern, nutze den Filmstreifen. **In Ordnerrolle speichern** macht aus Bildern, die alle aus einem Ordner stammen, die Rolle dieses Ordners. Seine anderen Dateien erscheinen als entfernt. **Als virtuelle Rolle speichern…** behält genau diese Bilder, aus beliebigen Ordnern. In Ordnerrolle speichern macht auch aus einer virtuellen Rolle eines Ordners die Rolle dieses Ordners. Die neue Rolle startet mit den Einstellungen, den Kartensperren und den nur in dieser Rolle gemachten Bearbeitungen der geöffneten Rolle. Ein Bild hat in jeder Rolle, die es enthält, dieselbe Bearbeitung. Rechtsklick → **In dieser Rolle unabhängig bearbeiten** gibt ihm in einer Rolle eine eigene Bearbeitung. - + ### Filmstreifen Der **Filmstreifen** unter der Arbeitsfläche zeigt die geöffneten Bilder in einer Reihe. Ziehe seinen oberen Rand, um die Bilder zu vergrößern. diff --git a/negpy/translations/USER_GUIDE.en.md b/negpy/translations/USER_GUIDE.en.md index ebddf9b54..d90b9eb1b 100644 --- a/negpy/translations/USER_GUIDE.en.md +++ b/negpy/translations/USER_GUIDE.en.md @@ -132,9 +132,9 @@ The **Film Strip** under the canvas shows the open frames in one row. Drag its t The toolbar's **Roll** row holds: -* The roll's name and its count of frames, keepers and rejects. +* The roll's name and count of frames, keepers and rejects. * The **filter box**: type part of a file name, or terms such as `film:portra iso:>=400` (below). The search button, or **Enter**, searches the whole library. -* **Sort**: by name, date, scene, or by roll and frame number. +* **Sort**: name, date, scene or frame number. * **Sheet filter**: all frames, keepers only, hide rejected, or unmarked only. The button is highlighted while a filter hides frames. * **Roll Settings** (the tag): gear and film details for this frame, the selection or the roll. * **Apply**: copies chosen settings of this frame to the selection or the roll. diff --git a/negpy/translations/USER_GUIDE.ja.md b/negpy/translations/USER_GUIDE.ja.md index 32be59f63..db2f26f1a 100644 --- a/negpy/translations/USER_GUIDE.ja.md +++ b/negpy/translations/USER_GUIDE.ja.md @@ -6,7 +6,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室 ロールを読み込み、フィルムモードとスキャン装置を選びます。光源・レンズ補正、トリミング、ロール解析を行ってから、各フレームの濃度、コントラスト、色補正、修復を調整し、最後に書き出します。カード見出しのフレーム/ロールで適用範囲を選び、ロール設定への復帰で共有値を戻せます。個々のトリミング位置と回転はフレームに保持されます。 - + ## フィルムストリップ キャンバスの下の**フィルムストリップ**は、開いているフレームを1列に表示します。上端をドラッグすると、フレームが大きくなります。 diff --git a/negpy/translations/USER_GUIDE.ko.md b/negpy/translations/USER_GUIDE.ko.md index 82c594f23..637d91e39 100644 --- a/negpy/translations/USER_GUIDE.ko.md +++ b/negpy/translations/USER_GUIDE.ko.md @@ -6,7 +6,7 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입 롤을 불러와 필름 모드와 스캔 장치를 선택합니다. 광원과 렌즈를 보정하고 프레임 자르기와 롤 분석을 수행한 다음, 각 프레임의 농도·대비·색 보정·복구를 조정하여 내보냅니다. 카드 제목의 프레임/롤 버튼으로 적용 범위를 정하고 롤 설정으로 되돌려 공유 값을 복원할 수 있습니다. 자르기 위치와 회전은 프레임별로 유지됩니다. - + ## 필름 스트립 캔버스 아래의 **필름 스트립**은 열린 프레임을 한 줄로 보여줍니다. 위쪽 가장자리를 드래그하면 프레임이 커집니다. diff --git a/negpy/translations/USER_GUIDE.nb.md b/negpy/translations/USER_GUIDE.nb.md index 38ff18b66..8182644ff 100644 --- a/negpy/translations/USER_GUIDE.nb.md +++ b/negpy/translations/USER_GUIDE.nb.md @@ -18,7 +18,7 @@ En mapperull inneholder bare filene i sin egen mappe. En fil du legger til fra e Bruk filmstripen for å lagre bildene i filmstripen som en rull. **Lagre i mapperull** gjør bilder som alle kommer fra én mappe, til rullen for den mappen. De andre filene i mappen vises som fjernet. **Lagre som virtuell rull…** beholder nøyaktig disse bildene, fra hvilke som helst mapper. Lagre i mapperull gjør også en virtuell rull fra én mappe til rullen for den mappen. Den nye rullen starter med den åpne rullens innstillinger, kortlåser og redigeringene som bare ble gjort i den rullen. Et bilde har én redigering i hver rull som inneholder det. Høyreklikk → **Rediger uavhengig i denne rullen** gir det en egen redigering i én rull. - + ### Filmstripe **Filmstripen** under lerretet viser de åpne bildene på én rad. Dra den øvre kanten for å gjøre bildene større. @@ -27,7 +27,7 @@ Raden **Rull** på verktøylinjen inneholder: * Navnet på rullen og antallet bilder, beholdte og forkastede. * **Filterfeltet**: skriv en del av et filnavn, eller uttrykk som `film:portra iso:>=400` (se nedenfor). Søkeknappen, eller **Enter**, søker i hele biblioteket. -* **Sorter**: etter navn, dato eller scene. +* **Sorter**: etter navn, dato, scene, eller etter rull og bildenummer. * **Arkfilter**: alle bilder, bare beholdte, skjul forkastede, eller bare umerkede. Knappen er uthevet mens et filter skjuler bilder. * **Rullinnstillinger** (merkelappen): detaljer om utstyr og film for dette bildet, utvalget eller rullen. * **Bruk**: kopierer valgte innstillinger fra dette bildet til utvalget eller rullen. @@ -505,7 +505,7 @@ Ekstra filer ved siden av kopien. Hver har en bryter til venstre og en engangsha * **Autotetthet / -gradering**: **Bak inn ved eksport** fastsetter den automatiske lysheten og kontrasten for hvert eksporterte bilde, så de ikke endres senere. **Bak inn i rullen** gjør det for hvert synlig bilde nå. * **Redigeringssidefiler**: **Lagre ved eksport** skriver en `.negpy`-fil med redigeringen ved siden av hver kilde. **Eksporter sidefiler** skriver dem for alle synlige bilder. Høyreklikk et bilde → **Last inn redigering fra sidefil…** leser en tilbake. - + ### Kontaktark På fanen **Ved siden av eksportene**. Et kontaktark viser hele rullen som filmstriper på ett papirark, slik en kontaktkopi i mørkerommet gjør. @@ -514,7 +514,7 @@ På fanen **Ved siden av eksportene**. Et kontaktark viser hele rullen som films * **Kontaktark…**: åpner arket. Dra en kant av papiret for å endre størrelsen. * **Filmformat**: 35 mm, 35 mm halvformat eller 120 (med **Bildestørrelse** for 120). * **Papir**, **Bredde**, **Høyde** og **DPI**: størrelsen og oppløsningen til arket. Standardpapiret har plass til en rull med 36 bilder. -* **Rekkefølge**: etter filnavn, etter dato, eller etter scene med en ny stripe eller rad for hver scene. +* **Rekkefølge**: etter filnavn, etter dato, etter rull og bildenummer, eller etter scene med en ny stripe eller rad for hver scene. * **Kopi**: **Som redigert**, eller **Rett prøve**: hele rullen med én eksponering og normal kontrast, så du ser hvilke negativer som er tynne eller tette. * **Velg bilder**: klikk et bilde for å utelate det eller sette det tilbake. Forkastede er utelatt i starten. * **Kantpåskrift**: produsentens merking langs kantene, for eksempel filmnavnet, bildenumrene og de fargede linjene en filmtype trykker ved perforeringen. diff --git a/negpy/translations/USER_GUIDE.zh_CN.md b/negpy/translations/USER_GUIDE.zh_CN.md index a3933b591..f9da7fb14 100644 --- a/negpy/translations/USER_GUIDE.zh_CN.md +++ b/negpy/translations/USER_GUIDE.zh_CN.md @@ -6,7 +6,7 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样 载入胶卷后,选择胶片模式与扫描装置,校正光源和镜头,裁切画面并分析胶卷,再调整单帧密度、反差、滤色与修复,最后导出。卡片标题的画面/胶卷按钮控制应用范围,恢复胶卷设置可拉回共享值。单帧裁切位置与旋转保留在画面上。 - + ## 胶片条 画布下方的**胶片条**把打开的帧排成一行。拖动其上边缘可放大帧。 diff --git a/negpy/translations/nb.json b/negpy/translations/nb.json index 31a9837e2..bf1219953 100644 --- a/negpy/translations/nb.json +++ b/negpy/translations/nb.json @@ -1096,6 +1096,7 @@ "Frame Counter": "Bildeteller", "Frame Info": "Bildeinfo", "Frame Info: {value}": "Bildeinfo: {value}", + "Frame No.": "Bildenr.", "Frame info on the canvas: off, basic or full": "Bildeinfo på lerretet: av, grunnleggende eller full", "Frame length on the loaded film; Auto where the holder fixes it": "Bildelengde på den innlastede filmen; Auto der holderen bestemmer den", "Frame or Roll": "Bilde eller rull", @@ -1387,7 +1388,7 @@ "Later edits never delete a work print; this is the only way to delete one.": "Senere redigeringer sletter aldri en arbeidskopi; dette er den eneste måten å slette en på.", "Latitude and longitude, or a pasted OpenStreetMap or Google Maps link.": "Breddegrad og lengdegrad, eller en innlimt lenke fra OpenStreetMap eller Google Maps.", "Lay every visible frame out as film strips on photographic paper, then export the sheet": "Legg ut hvert synlig bilde som filmstriper på fotopapir, og eksporter så arket", - "Lay the frames out by file name, in the order they were taken, or with a new strip for each scene.": "Ordne bildene etter filnavn, i rekkefølgen de ble tatt, eller med en ny stripe for hver scene.", + "Lay the frames out by file name, by the order they were taken, by roll and frame number, or by scene.": "Ordne bildene etter filnavn, etter rekkefølgen de ble tatt i, etter rull og bildenummer, eller etter scene.", "Leave at the default unless you know why you need to change it.": "La standardverdien stå med mindre du vet hvorfor du må endre den.", "Leave frames in the rejected state out of the run; the numbers fill the frames that remain.": "Utelat forkastede bilder fra rekken; numrene fyller bildene som er igjen.", "Leave the current view (peek, split, strip, zone overlay) or cancel the active tool; Crop and Draw Region undo their changes": "Forlat den gjeldende visningen (titt, deling, stripe, soneoverlegg) eller avbryt det aktive verktøyet; Beskjær og Tegn område angrer endringene sine", diff --git a/negpy/translations/vi.json b/negpy/translations/vi.json index af9f6645f..dc29ca582 100644 --- a/negpy/translations/vi.json +++ b/negpy/translations/vi.json @@ -1096,6 +1096,7 @@ "Frame Counter": "Bộ đếm khung hình", "Frame Info": "Thông tin khung hình", "Frame Info: {value}": "Thông tin khung hình: {value}", + "Frame No.": "Số khung hình", "Frame info on the canvas: off, basic or full": "Thông tin khung hình trên khung vẽ: tắt, cơ bản hoặc đầy đủ", "Frame length on the loaded film; Auto where the holder fixes it": "Chiều dài khung hình trên phim đã nạp; Tự động nếu khay giữ cố định nó", "Frame or Roll": "Khung hình hoặc cuộn", @@ -1387,7 +1388,7 @@ "Later edits never delete a work print; this is the only way to delete one.": "Các chỉnh sửa sau này không bao giờ xóa bản in làm việc; đây là cách duy nhất để xóa một bản.", "Latitude and longitude, or a pasted OpenStreetMap or Google Maps link.": "Vĩ độ và kinh độ, hoặc một liên kết OpenStreetMap hay Google Maps đã dán.", "Lay every visible frame out as film strips on photographic paper, then export the sheet": "Sắp xếp mọi khung hình đang hiển thị thành các dải phim trên giấy ảnh, rồi xuất tờ đó", - "Lay the frames out by file name, in the order they were taken, or with a new strip for each scene.": "Sắp xếp khung hình theo tên tệp, theo thứ tự chụp, hoặc với một dải mới cho mỗi cảnh.", + "Lay the frames out by file name, by the order they were taken, by roll and frame number, or by scene.": "Xếp khung hình theo tên tệp, theo thứ tự chụp, theo cuộn và số khung hình, hoặc theo cảnh.", "Leave at the default unless you know why you need to change it.": "Để ở mặc định trừ khi bạn biết tại sao cần thay đổi.", "Leave frames in the rejected state out of the run; the numbers fill the frames that remain.": "Bỏ các khung hình đang ở trạng thái bị loại ra khỏi lượt đánh số; các số được điền cho những khung hình còn lại.", "Leave the current view (peek, split, strip, zone overlay) or cancel the active tool; Crop and Draw Region undo their changes": "Rời khung xem hiện tại (xem nhanh, chia đôi, dải, lớp phủ vùng) hoặc hủy công cụ đang dùng; Cắt và Vẽ vùng hoàn tác thay đổi của chúng", diff --git a/tests/test_contact_sheet_roll.py b/tests/test_contact_sheet_roll.py index 51b0f726e..b515d5cf8 100644 --- a/tests/test_contact_sheet_roll.py +++ b/tests/test_contact_sheet_roll.py @@ -17,6 +17,7 @@ FrameFacts, SheetFrame, creation_order, + frame_order, name_order, cropped_aspect, infer_format, @@ -69,6 +70,15 @@ def test_ties_break_by_name_then_half(self): frames = [_frame("x.tif", half=2, birth=5), _frame("x.tif", half=1, birth=5), _frame("a.tif", birth=5)] assert creation_order(frames) == [2, 1, 0] + def test_frame_order_is_roll_then_frame_number_then_name(self): + frames = [ + _frame("a.tif", capture_roll="02", capture_frame=1), + _frame("b.tif", capture_roll="01", capture_frame=10), + _frame("c.tif", capture_roll="01", capture_frame=2), + _frame("e.tif", capture_roll="01", capture_frame=2), + ] + assert frame_order(frames) == [2, 3, 1, 0] + def test_name_order_ignores_case_and_time(self): frames = [_frame("b.tif", birth=1), _frame("x.tif", half=2), _frame("A.tif", birth=9), _frame("x.tif", half=1)] assert [(f.name, f.half) for f in name_order(frames)] == [("A.tif", 0), ("b.tif", 0), ("x.tif", 1), ("x.tif", 2)] diff --git a/tests/test_desktop_session.py b/tests/test_desktop_session.py index 447e43187..553cc1841 100644 --- a/tests/test_desktop_session.py +++ b/tests/test_desktop_session.py @@ -1957,6 +1957,22 @@ def test_no_blocks_under_other_orders_or_a_semantic_query(self): self.assertEqual(self.model.scene_runs(), []) +class TestFrameNumberSort(unittest.TestCase): + def test_frames_sort_by_roll_then_frame_number_then_name(self): + state = AppState() + state.uploaded_files = [{"name": n, "path": f"/r/{n}", "hash": n} for n in ("a.tif", "b.tif", "c.tif", "d.tif")] + facts = { + "a.tif": {"capture_roll": "02", "frame": 1}, + "b.tif": {"capture_roll": "01", "frame": 10}, + "c.tif": {"capture_roll": "01", "frame": 2}, + "d.tif": {"capture_roll": "01", "frame": 2}, + } + model = AssetListModel(state, facts_provider=lambda: facts) + model.set_sort_order("frame") + names = [state.uploaded_files[i]["name"] for i in model._sorted_indices] + self.assertEqual(names, ["c.tif", "d.tif", "b.tif", "a.tif"]) + + class TestNavButtonBoundaries(unittest.TestCase): """Regression for #407: Next/Prev enable must be computed in display space (sorted/filtered order), matching session.next_file/prev_file — not raw