Export to an album instead of a folder in the settings
Export took a path typed into the settings page, or a folder inside the library on the server. The first is how exports end up somewhere nobody looks; the second put JPEGs into the tree a scan catalogues, where they came back as photographs beside the RAWs they were made from. The destination is now an album (FR-EXP-10), chosen by name in the export sheet. Albums are listed under the collections in the sidebar; "+" there, or "New album…" in the sheet, opens a sheet for its name and its folder — on this device through the platform's dialogue, or on the server through the browser with "New folder". A server folder inside the library is refused, and the sheet says why. Selecting an album narrows the grid to the photographs behind its files: library::Scope is Collection or Album, and scope_clause is the one place the two are spelled, which also retires the two copies of the collection predicate total_images_scoped and read_cells_scoped had inlined. A batch resolves the album when it starts, and refuses in words when none is chosen, it has gone, or its folder is local to another device. Each item reports the image it came from, and the files written are recorded against the album in one transaction when the batch ends. A server album lives outside the library, so its queued uploads are relative to the account root. That is a third line in the outbox's .dest record rather than a leading slash, because a record written before albums may carry a stray slash and must keep the meaning it was written with. An export folder set before albums becomes an album called "Exports" on first open, so upgrading does not lose where exports were going. The old destination fields stay in ExportSettings so older settings files still read.
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
// TRACES: FR-EXP-10 | FR-EXP-6
|
||||
// Albums: named export folders, in the sidebar under the collections.
|
||||
//
|
||||
// An album is where finished pictures go. Its folder holds only the exported
|
||||
// files; the catalog remembers which photograph each came from, so selecting
|
||||
// an album shows the originals behind it, and the export sheet sends a
|
||||
// selection to one by name rather than to a path typed into a field.
|
||||
//
|
||||
// The folder is on this device (chosen in the platform's dialogue, or on
|
||||
// Android the system's folder picker) or on the library's server (chosen in
|
||||
// the browser below, which can make a folder as well as open one). Not inside
|
||||
// the library itself: a JPEG exported there would be catalogued by the next
|
||||
// scan as a photograph of its own.
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Button, Caption, Field, Icon, PanelHeading, Value } from "widgets.slint";
|
||||
import { Segmented } from "controls.slint";
|
||||
|
||||
export struct AlbumRow {
|
||||
id: int,
|
||||
name: string,
|
||||
// Photographs exported into it — what the grid shows when it is opened.
|
||||
count: int,
|
||||
// Where its files go, in words: "On this device · ~/Exports/Web",
|
||||
// "On the server · Shared/Web", or that this device has no folder for it.
|
||||
detail: string,
|
||||
// The album has no folder on this device yet: made elsewhere with a
|
||||
// folder local to that device.
|
||||
homeless: bool,
|
||||
}
|
||||
|
||||
export global Albums {
|
||||
in property <[AlbumRow]> rows;
|
||||
/// The album scoping the grid, 0 for none.
|
||||
in property <int> selected: 0;
|
||||
callback select(int);
|
||||
/// Open the sheet on a new album.
|
||||
callback create();
|
||||
/// Open the sheet on an existing one.
|
||||
callback edit(int);
|
||||
|
||||
// --- the sheet ------------------------------------------------------
|
||||
in property <bool> sheet-open: false;
|
||||
/// Editing an existing album rather than making one.
|
||||
in property <bool> sheet-editing: false;
|
||||
in property <string> sheet-name;
|
||||
/// Whether the library has a server to put a folder on. A folder library
|
||||
/// has only this device.
|
||||
in property <bool> server-available: false;
|
||||
/// 0: this device. 1: the server.
|
||||
in property <int> sheet-where: 0;
|
||||
/// The folder chosen on this device, for display. Empty until chosen.
|
||||
in property <string> sheet-device-folder;
|
||||
/// Something to say — a refusal, a failed request.
|
||||
in property <string> sheet-error;
|
||||
/// Delete has been pressed once and waits to be confirmed.
|
||||
in property <bool> sheet-confirming-delete: false;
|
||||
/// What saving would do, when the button alone would not say.
|
||||
in property <bool> sheet-can-save: false;
|
||||
|
||||
callback sheet-where-picked(int);
|
||||
callback sheet-choose-device-folder();
|
||||
callback sheet-name-edited(string);
|
||||
callback sheet-save();
|
||||
callback sheet-cancel();
|
||||
callback sheet-delete();
|
||||
|
||||
// --- the server folder, while the sheet is choosing one ---------------
|
||||
in property <string> browse-path;
|
||||
in property <[string]> browse-entries;
|
||||
in property <bool> browse-loading: false;
|
||||
in property <bool> browse-at-root: true;
|
||||
/// Why the folder shown cannot be used, or empty when it can.
|
||||
in property <string> browse-refusal;
|
||||
callback browse-into(string);
|
||||
callback browse-up();
|
||||
callback browse-make(string);
|
||||
}
|
||||
|
||||
// A folder on the server: where it is, what is in it, and a way to make one.
|
||||
//
|
||||
// Confirms the folder *shown*, not one selected in the list — the rule the
|
||||
// launch screen's picker already follows — so walking into a folder is
|
||||
// choosing it, and there is no second selection that could disagree.
|
||||
export component ServerFolderBrowser inherits VerticalLayout {
|
||||
in property <string> path;
|
||||
in property <[string]> entries;
|
||||
in property <bool> loading;
|
||||
in property <bool> at-root: true;
|
||||
|
||||
callback into(string);
|
||||
callback up();
|
||||
callback make(string);
|
||||
|
||||
property <bool> naming: false;
|
||||
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
Button {
|
||||
text: "↑ Up";
|
||||
// Disabled rather than hidden at the root: a control that
|
||||
// vanishes moves everything beside it.
|
||||
enabled: !root.at-root && !root.loading;
|
||||
clicked => { root.up(); }
|
||||
}
|
||||
|
||||
Value {
|
||||
text: root.path == "" ? "/" : "/" + root.path;
|
||||
overflow: elide;
|
||||
horizontal-stretch: 1;
|
||||
min-width: 0px;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "New folder";
|
||||
enabled: !root.loading && !root.naming;
|
||||
clicked => { root.naming = true; }
|
||||
}
|
||||
}
|
||||
|
||||
if root.naming: HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
name := Field {
|
||||
horizontal-stretch: 1;
|
||||
label: "New folder name";
|
||||
placeholder: "Folder name";
|
||||
accepted(t) => {
|
||||
if (t != "") {
|
||||
root.make(t);
|
||||
root.naming = false;
|
||||
}
|
||||
}
|
||||
init => { self.take-focus(); }
|
||||
}
|
||||
Button {
|
||||
text: "Make";
|
||||
enabled: name.text != "";
|
||||
clicked => {
|
||||
root.make(name.text);
|
||||
root.naming = false;
|
||||
}
|
||||
}
|
||||
Button {
|
||||
text: "Cancel";
|
||||
clicked => { root.naming = false; }
|
||||
}
|
||||
}
|
||||
|
||||
// A fixed height rather than one that grows with the listing: a folder
|
||||
// with sixty children would otherwise push the buttons off the sheet.
|
||||
Rectangle {
|
||||
height: 200px;
|
||||
background: Theme.ground;
|
||||
border-radius: Theme.radius;
|
||||
border-width: 1px;
|
||||
border-color: Theme.rule;
|
||||
|
||||
if root.loading: Caption {
|
||||
text: "Listing…";
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
if !root.loading: Flickable {
|
||||
x: 4px;
|
||||
y: 4px;
|
||||
width: parent.width - 8px;
|
||||
height: parent.height - 8px;
|
||||
viewport-height: list.preferred-height;
|
||||
|
||||
list := VerticalLayout {
|
||||
width: 100%;
|
||||
spacing: 2px;
|
||||
alignment: start;
|
||||
|
||||
if root.entries.length == 0: Caption {
|
||||
text: "No folders here. Use this one, make one, or go up.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
for folder in root.entries: Rectangle {
|
||||
height: 32px;
|
||||
background: row-touch.has-hover ? Theme.hover : transparent;
|
||||
border-radius: Theme.radius-sm;
|
||||
|
||||
accessible-role: button;
|
||||
accessible-label: folder;
|
||||
accessible-action-default => { root.into(folder); }
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: Theme.gap-sm;
|
||||
padding-right: Theme.gap-sm;
|
||||
spacing: Theme.gap-sm;
|
||||
Icon {
|
||||
name: "chevron-right";
|
||||
ink: Theme.ink-faint;
|
||||
size: 10px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
Value {
|
||||
text: folder;
|
||||
overflow: elide;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
}
|
||||
row-touch := TouchArea { clicked => { root.into(folder); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One album in the sidebar.
|
||||
component AlbumListRow inherits Rectangle {
|
||||
in property <AlbumRow> entry;
|
||||
in property <bool> selected;
|
||||
callback clicked();
|
||||
callback edit();
|
||||
|
||||
height: Theme.row-height;
|
||||
background: root.selected ? Theme.surface-raised
|
||||
: (touch.has-hover ? Theme.hover : transparent);
|
||||
border-radius: Theme.radius-sm;
|
||||
|
||||
accessible-role: button;
|
||||
accessible-label: root.entry.name + ", " + root.entry.detail;
|
||||
accessible-action-default => { root.clicked(); }
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => { root.clicked(); }
|
||||
double-clicked => { root.edit(); }
|
||||
pointer-event(ev) => {
|
||||
if (ev.kind == PointerEventKind.down && ev.button == PointerEventButton.right) {
|
||||
root.edit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: Theme.gap-sm;
|
||||
padding-right: Theme.gap-sm;
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
Text {
|
||||
text: root.entry.name;
|
||||
color: root.entry.homeless ? Theme.ink-faint
|
||||
: (root.selected ? Theme.ink : Theme.ink-dim);
|
||||
font-size: Theme.text;
|
||||
font-weight: root.selected ? 600 : 400;
|
||||
overflow: elide;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
Text {
|
||||
text: root.entry.count > 0 ? root.entry.count : "";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
// The way in to rename, move or delete, where there is no right
|
||||
// button and no double click: a finger.
|
||||
Rectangle {
|
||||
width: 22px;
|
||||
accessible-role: button;
|
||||
accessible-label: "Edit " + root.entry.name;
|
||||
accessible-action-default => { root.edit(); }
|
||||
Icon {
|
||||
name: "chevron-right";
|
||||
ink: Theme.ink-faint;
|
||||
size: 10px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
TouchArea { clicked => { root.edit(); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The sidebar's albums: a heading with "+", and the list.
|
||||
export component AlbumsSection inherits VerticalLayout {
|
||||
spacing: 2px;
|
||||
|
||||
HorizontalLayout {
|
||||
height: 26px;
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
Text {
|
||||
text: "ALBUMS";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.2px;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
background: add-touch.pressed ? Theme.pressed
|
||||
: (add-touch.has-hover ? Theme.hover : transparent);
|
||||
border-radius: Theme.radius-sm;
|
||||
|
||||
accessible-role: button;
|
||||
accessible-label: "New album";
|
||||
accessible-action-default => { Albums.create(); }
|
||||
|
||||
Icon {
|
||||
name: "plus";
|
||||
ink: Theme.ink-dim;
|
||||
size: 12px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
add-touch := TouchArea { clicked => { Albums.create(); } }
|
||||
}
|
||||
}
|
||||
|
||||
if Albums.rows.length == 0: Text {
|
||||
text: "Press + to make an export folder. Exports sent to it are listed here.";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
// At most five rows tall, then it scrolls: the albums share the column
|
||||
// with the collection tree, which is the thing that should grow.
|
||||
if Albums.rows.length > 0: Flickable {
|
||||
height: min(Albums.rows.length, 5) * (Theme.row-height + 2px);
|
||||
viewport-height: Albums.rows.length * (Theme.row-height + 2px);
|
||||
|
||||
for row[i] in Albums.rows: AlbumListRow {
|
||||
y: i * (Theme.row-height + 2px);
|
||||
width: parent.width;
|
||||
entry: row;
|
||||
selected: row.id == Albums.selected;
|
||||
clicked => { Albums.select(row.id); }
|
||||
edit => { Albums.edit(row.id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Making an album, or changing one: its name and where its files go.
|
||||
export component AlbumSheet inherits Rectangle {
|
||||
background: #000000CC;
|
||||
|
||||
TouchArea { clicked => { Albums.sheet-cancel(); } }
|
||||
|
||||
Rectangle {
|
||||
width: min(560px, parent.width - 2 * Theme.gap-lg);
|
||||
height: min(body.preferred-height + 2 * Theme.gap-lg, parent.height - 2 * Theme.gap-lg);
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
background: Theme.surface;
|
||||
border-radius: Theme.radius;
|
||||
border-width: 1px;
|
||||
border-color: Theme.rule;
|
||||
|
||||
TouchArea { }
|
||||
|
||||
Flickable {
|
||||
viewport-height: body.preferred-height + 2 * Theme.gap-lg;
|
||||
|
||||
body := VerticalLayout {
|
||||
x: Theme.gap-lg;
|
||||
y: Theme.gap-lg;
|
||||
width: parent.width - 2 * Theme.gap-lg;
|
||||
spacing: Theme.gap;
|
||||
alignment: start;
|
||||
|
||||
Text {
|
||||
text: Albums.sheet-editing ? "Album" : "New album";
|
||||
color: Theme.ink;
|
||||
font-size: Theme.text-lg;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
PanelHeading { text: "NAME"; }
|
||||
Field {
|
||||
label: "Album name";
|
||||
text: Albums.sheet-name;
|
||||
placeholder: "Web, Prints, For Anna…";
|
||||
edited(t) => { Albums.sheet-name-edited(t); }
|
||||
accepted(t) => { Albums.sheet-name-edited(t); }
|
||||
// The keyboard comes here when the sheet opens. Left on
|
||||
// the grid, a name typed at once is read as shortcuts —
|
||||
// P picks, R rejects, digits rate.
|
||||
init => { self.take-focus(); }
|
||||
}
|
||||
|
||||
PanelHeading { text: "FILES GO TO"; }
|
||||
|
||||
if Albums.server-available: Segmented {
|
||||
label: "Where";
|
||||
options: ["This device", "The server"];
|
||||
selected: Albums.sheet-where;
|
||||
picked(i) => { Albums.sheet-where-picked(i); }
|
||||
}
|
||||
|
||||
if Albums.sheet-where == 0: HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
Value {
|
||||
text: Albums.sheet-device-folder != "" ? Albums.sheet-device-folder : "No folder chosen";
|
||||
placeholder: Albums.sheet-device-folder == "";
|
||||
overflow: elide;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
min-width: 0px;
|
||||
}
|
||||
Button {
|
||||
text: Albums.sheet-device-folder != "" ? "Change…" : "Choose folder…";
|
||||
clicked => { Albums.sheet-choose-device-folder(); }
|
||||
}
|
||||
}
|
||||
|
||||
if Albums.sheet-where == 0: Caption {
|
||||
text: "The dialogue can make a new folder too. Only the exported files are written there.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if Albums.sheet-where == 1: ServerFolderBrowser {
|
||||
path: Albums.browse-path;
|
||||
entries: Albums.browse-entries;
|
||||
loading: Albums.browse-loading;
|
||||
at-root: Albums.browse-at-root;
|
||||
into(name) => { Albums.browse-into(name); }
|
||||
up => { Albums.browse-up(); }
|
||||
make(name) => { Albums.browse-make(name); }
|
||||
}
|
||||
|
||||
if Albums.sheet-where == 1 && Albums.browse-refusal != "": Caption {
|
||||
text: Albums.browse-refusal;
|
||||
warn: true;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if Albums.sheet-error != "": Caption {
|
||||
text: Albums.sheet-error;
|
||||
warn: true;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if Albums.sheet-confirming-delete: Caption {
|
||||
text: "The album goes; the files already in its folder stay where they are.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
if Albums.sheet-editing: Button {
|
||||
text: Albums.sheet-confirming-delete ? "Delete album" : "Delete…";
|
||||
clicked => { Albums.sheet-delete(); }
|
||||
}
|
||||
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
|
||||
Button {
|
||||
text: "Cancel";
|
||||
clicked => { Albums.sheet-cancel(); }
|
||||
}
|
||||
Button {
|
||||
text: Albums.sheet-editing ? "Save" : "Make album";
|
||||
primary: true;
|
||||
enabled: Albums.sheet-can-save;
|
||||
clicked => { Albums.sheet-save(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-4
@@ -16,6 +16,7 @@ import { HistogramPanel, HistogramView, Levels } from "histogram.slint";
|
||||
import { RecoveryPrompt } from "recovery.slint";
|
||||
import { CopySheet, PresetOrigin, PresetRow, PresetSheet, ScopeChips, ScopeKind } from "presets.slint";
|
||||
import { ExportOptions, ExportSheet } from "export.slint";
|
||||
import { Albums, AlbumSheet, AlbumRow } from "albums.slint";
|
||||
import { FocusMarks, FocusPanel, Peaking } from "peaking.slint";
|
||||
import { SettingsPage } from "settings.slint";
|
||||
import { ImportPage } from "import.slint";
|
||||
@@ -37,6 +38,7 @@ export { Develop, Capture, Levels, Peaking, Adjustments, Framing, Transfer, Mask
|
||||
export { Collections }
|
||||
export { Library }
|
||||
export { ExportOptions }
|
||||
export { Albums, AlbumRow }
|
||||
export { Pickers } from "pickers.slint";
|
||||
export { Duplicates, DuplicateRow, DuplicateCopy }
|
||||
export { Scrolling }
|
||||
@@ -939,7 +941,7 @@ in property <bool> panel-visible: true;
|
||||
/// this device. The grid says the same thing as the develop button, but it
|
||||
/// composes its own label around a selection count — so it is given the
|
||||
/// fact and not the sentence.
|
||||
in property <bool> export-to-server: false;
|
||||
in property <string> export-album;
|
||||
/// Show or hide the develop column.
|
||||
callback toggle-panel();
|
||||
/// Export the image on screen, using the settings as they stand.
|
||||
@@ -1516,7 +1518,7 @@ in property <bool> panel-visible: true;
|
||||
|
||||
// TRACES: FR-EXP-7
|
||||
exporting: Library.library-exporting;
|
||||
export-to-server: root.export-to-server;
|
||||
export-album: root.export-album;
|
||||
export-selection => { Library.library-export-selection(); }
|
||||
cancel-export => { Library.library-cancel-export(); }
|
||||
// TRACES: FR-MRG-1
|
||||
@@ -3452,8 +3454,8 @@ in property <bool> panel-visible: true;
|
||||
error: root.settings-error;
|
||||
action-label: root.active-view == View.develop
|
||||
? root.export-label
|
||||
: (root.export-to-server
|
||||
? "Export " + Library.library-selected-count + " to the library"
|
||||
: (root.export-album != ""
|
||||
? "Export " + Library.library-selected-count + " to " + root.export-album
|
||||
: "Export " + Library.library-selected-count);
|
||||
can-export: root.export-sheet-can-export;
|
||||
export => { root.export-from-sheet(); }
|
||||
@@ -3541,6 +3543,15 @@ in property <bool> panel-visible: true;
|
||||
dismiss() => { Collections.collection-menu-dismiss(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-EXP-10
|
||||
// Above the export sheet, which opens it for "New album…", and above
|
||||
// the sidebar that opens it otherwise — which the compact layout
|
||||
// hides, so it cannot be drawn inside it.
|
||||
if Albums.sheet-open: AlbumSheet {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// Last, and therefore over everything including the settings page and
|
||||
// the offline prompt. Not a preference about layering: this is asked
|
||||
// before the grid exists, and nothing else in the window is about a
|
||||
|
||||
@@ -35,6 +35,7 @@ import { Theme } from "theme.slint";
|
||||
import { Duplicates } from "duplicates.slint";
|
||||
import { Keys } from "keys.slint";
|
||||
import { Button, Icon, ScrollBar } from "widgets.slint";
|
||||
import { AlbumsSection } from "albums.slint";
|
||||
|
||||
// One row of the collection tree.
|
||||
export struct CollectionRow {
|
||||
@@ -783,6 +784,19 @@ export component CollectionsPanel inherits Rectangle {
|
||||
clicked => { root.toggle-pin-scope(); }
|
||||
}
|
||||
|
||||
// --- albums -------------------------------------------------------
|
||||
//
|
||||
// TRACES: FR-EXP-10
|
||||
// Under the collections and above the trash: a selection target like
|
||||
// a collection, but not a drop target — photographs reach an album by
|
||||
// being exported to it, never by being filed.
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
}
|
||||
|
||||
AlbumsSection {}
|
||||
|
||||
// --- trash --------------------------------------------------------
|
||||
//
|
||||
// TRACES: FR-CAT-15
|
||||
|
||||
+35
-173
@@ -43,33 +43,16 @@ export global ExportOptions {
|
||||
in property <[string]> collision-labels;
|
||||
in property <int> collision-selected: 0;
|
||||
in property <bool> strip-location: true;
|
||||
in-out property <string> destination;
|
||||
// What the destination field means depends on this, so the placeholder
|
||||
// comes from Rust alongside it rather than being written twice here.
|
||||
in property <string> destination-hint;
|
||||
in property <[string]> target-labels;
|
||||
in property <int> target-selected: 0;
|
||||
|
||||
// --- the remote folder picker ------------------------------------
|
||||
// --- where it goes (FR-EXP-10) -----------------------------------
|
||||
//
|
||||
// The same navigation the launch screen uses to choose a library root,
|
||||
// driven by the same `FolderBrowser` model in Rust. A folder on the
|
||||
// server is not something anyone can be expected to type from memory.
|
||||
in property <bool> browse-open: false;
|
||||
in property <string> browse-path;
|
||||
in property <[string]> browse-entries;
|
||||
in property <bool> browse-loading: false;
|
||||
/// At the library root, so there is nowhere up to go.
|
||||
in property <bool> browse-at-root: true;
|
||||
/// Whether the destination is one that can be walked.
|
||||
///
|
||||
/// A boolean from Rust rather than a test on `target-selected`. The index
|
||||
/// was hardcoded to 1, which was Remote's position while both targets were
|
||||
/// offered — and the moment Android's list narrowed to Remote alone, that
|
||||
/// index became 0 and the button vanished on the one platform where it is
|
||||
/// the *only* way to set a destination. An index into a list whose length
|
||||
/// varies is not a fact about the target.
|
||||
in property <bool> browse-available: false;
|
||||
// An album, by name. The folder behind it — on this device or on the
|
||||
// server — is chosen once, when the album is made, in the platform's
|
||||
// dialogue or the server browser; exporting only says which album.
|
||||
in property <[string]> album-labels;
|
||||
/// -1 while none is chosen, and an export is refused until one is.
|
||||
in property <int> album-selected: -1;
|
||||
/// Where the chosen album's files go, in words.
|
||||
in property <string> album-detail;
|
||||
|
||||
callback format-picked(int);
|
||||
callback quality-changed(int);
|
||||
@@ -83,13 +66,9 @@ export global ExportOptions {
|
||||
callback template-changed(string);
|
||||
callback collision-picked(int);
|
||||
callback strip-location-toggled(bool);
|
||||
callback destination-changed(string);
|
||||
callback target-picked(int);
|
||||
callback browse-open-picker();
|
||||
callback browse-into(string);
|
||||
callback browse-up();
|
||||
callback browse-confirm();
|
||||
callback browse-cancel();
|
||||
callback album-picked(int);
|
||||
/// Make a new album, from here rather than from the sidebar.
|
||||
callback album-new();
|
||||
}
|
||||
|
||||
// What the image becomes: format, quality, colour, size, sharpening. The
|
||||
@@ -222,152 +201,35 @@ export component ExportFileOptions inherits VerticalLayout {
|
||||
picked(i) => { ExportOptions.collision-picked(i); }
|
||||
}
|
||||
|
||||
// Where the file lands, before what it is called: on
|
||||
// Android the answer decides whether an export needs
|
||||
// the Storage Access Framework at all, and on any
|
||||
// platform a server destination is reached over a
|
||||
// network that may not be there.
|
||||
Segmented {
|
||||
label: "Export to";
|
||||
options: ExportOptions.target-labels;
|
||||
selected: ExportOptions.target-selected;
|
||||
picked(i) => { ExportOptions.target-picked(i); }
|
||||
// TRACES: FR-EXP-10
|
||||
// Where the file lands, before what it is called: an album, which
|
||||
// names a folder on this device or on the server and remembers
|
||||
// what was sent to it. Not the library — a JPEG written into the
|
||||
// library's own tree would be catalogued by the next scan as a
|
||||
// photograph of its own.
|
||||
if ExportOptions.album-labels.length > 0: Segmented {
|
||||
label: "Album";
|
||||
options: ExportOptions.album-labels;
|
||||
selected: ExportOptions.album-selected;
|
||||
columns: 3;
|
||||
picked(i) => { ExportOptions.album-picked(i); }
|
||||
}
|
||||
|
||||
TextRow {
|
||||
label: "Destination";
|
||||
// What an empty field does, not what it was once
|
||||
// going to do: nothing asks, and an export with
|
||||
// no folder is refused and says so in the header.
|
||||
hint: "a folder on this device; exports are refused until one is set";
|
||||
text <=> ExportOptions.destination;
|
||||
field-width: 320px;
|
||||
placeholder: ExportOptions.destination-hint;
|
||||
accepted(t) => { ExportOptions.destination-changed(t); }
|
||||
if ExportOptions.album-labels.length == 0: Caption {
|
||||
text: "No album yet. An album is a folder exports go to — on this device or on the server — and the photographs sent to it are listed under Albums in the library.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
// Offered only for a server destination. A folder on
|
||||
// this device is chosen by the platform's own dialogue
|
||||
// or typed; a folder on the server can only be found
|
||||
// by walking it, and expecting anyone to recall the
|
||||
// exact spelling of a path three levels down is how a
|
||||
// destination silently becomes a new folder at the
|
||||
// root.
|
||||
if ExportOptions.browse-available && !ExportOptions.browse-open: HorizontalLayout {
|
||||
if ExportOptions.album-detail != "": Caption {
|
||||
text: ExportOptions.album-detail;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
alignment: start;
|
||||
Button {
|
||||
text: "Choose folder…";
|
||||
clicked => { ExportOptions.browse-open-picker(); }
|
||||
}
|
||||
}
|
||||
|
||||
if ExportOptions.browse-open: Rectangle {
|
||||
background: Theme.ground;
|
||||
border-radius: Theme.radius;
|
||||
height: picker.preferred-height + 2 * Theme.gap;
|
||||
|
||||
picker := VerticalLayout {
|
||||
x: Theme.gap;
|
||||
y: Theme.gap;
|
||||
width: parent.width - 2 * Theme.gap;
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
Button {
|
||||
text: "↑ Up";
|
||||
// Disabled rather than hidden at the
|
||||
// root: a control that vanishes moves
|
||||
// everything beside it, and the row
|
||||
// would jump as the user navigates.
|
||||
enabled: !ExportOptions.browse-at-root;
|
||||
clicked => { ExportOptions.browse-up(); }
|
||||
}
|
||||
|
||||
Value {
|
||||
text: ExportOptions.browse-path;
|
||||
overflow: elide;
|
||||
horizontal-stretch: 1;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Caption {
|
||||
text: ExportOptions.browse-loading ? "Listing…" : "";
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
// A fixed height rather than one that grows
|
||||
// with the listing: a folder with sixty
|
||||
// children would otherwise push the rest of
|
||||
// the settings page off the bottom.
|
||||
Rectangle {
|
||||
height: 180px;
|
||||
background: Theme.surface;
|
||||
border-radius: Theme.radius;
|
||||
|
||||
Flickable {
|
||||
x: 4px;
|
||||
y: 4px;
|
||||
width: parent.width - 8px;
|
||||
height: parent.height - 8px;
|
||||
viewport-height: folders.preferred-height;
|
||||
|
||||
folders := VerticalLayout {
|
||||
width: 100%;
|
||||
spacing: 2px;
|
||||
alignment: start;
|
||||
|
||||
if ExportOptions.browse-entries.length == 0
|
||||
&& !ExportOptions.browse-loading: Caption {
|
||||
text: "No folders here. "
|
||||
+ "Use this one, or go up.";
|
||||
}
|
||||
|
||||
for name in ExportOptions.browse-entries: Rectangle {
|
||||
height: 32px;
|
||||
background: touch.has-hover
|
||||
? Theme.surface-raised
|
||||
: transparent;
|
||||
border-radius: Theme.radius;
|
||||
|
||||
Label {
|
||||
x: Theme.gap-sm;
|
||||
text: name;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
width: parent.width - 2 * Theme.gap-sm;
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => { ExportOptions.browse-into(name); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
alignment: end;
|
||||
|
||||
Button {
|
||||
text: "Cancel";
|
||||
clicked => { ExportOptions.browse-cancel(); }
|
||||
}
|
||||
|
||||
// Confirms the folder currently *shown*,
|
||||
// not one selected in the list — the same
|
||||
// rule the library picker follows, so
|
||||
// "use this one" means the same thing in
|
||||
// both places.
|
||||
Button {
|
||||
text: "Use this folder";
|
||||
active: true;
|
||||
clicked => { ExportOptions.browse-confirm(); }
|
||||
}
|
||||
}
|
||||
text: "New album…";
|
||||
clicked => { ExportOptions.album-new(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2006,7 +2006,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
// where the files go nor the worker that writes them — it reports what is
|
||||
// selected and asks, exactly as it does for a paste.
|
||||
in property <bool> exporting: false;
|
||||
in property <bool> export-to-server: false;
|
||||
/// The album an export goes to, by name; empty when none is chosen.
|
||||
in property <string> export-album;
|
||||
callback export-selection();
|
||||
callback cancel-export();
|
||||
/// TRACES: FR-EXP-7 | FR-UI-5
|
||||
@@ -5217,8 +5218,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
if !root.ranging: Button {
|
||||
text: root.exporting
|
||||
? "Cancel export"
|
||||
: (root.export-to-server
|
||||
? "Export " + root.selected-count + " to the library"
|
||||
: (root.export-album != ""
|
||||
? "Export " + root.selected-count + " to " + root.export-album
|
||||
: "Export " + root.selected-count);
|
||||
active: root.exporting;
|
||||
y: (parent.height - self.height) / 2;
|
||||
|
||||
Reference in New Issue
Block a user