Two faults, both of my own making, reported from the tablet as "I cannot select a location" and "it does not upload". **The picker button was gated on `target-selected == 1`.** That index was Remote's position while both targets were offered. Making the target list platform-aware narrowed Android's to Remote alone, so Remote became index 0 and the button disappeared — on the one platform where the picker is the *only* way to set a destination, since a device folder is not reachable there at all. It is gated on a boolean derived from the target now. An index into a list whose length varies is not a fact about the target, and writing it as one is what made a correct change break the thing it was meant to fix. **A queued export waited for a sync pass.** Staging first is deliberate — an export is finished on disk the moment it is written, and offline is then just a longer queue — but nothing drained the outbox until the next sync, so "Queued for Exports" sat unchanged and read, fairly, as an upload that never happened. A finished batch that wrote anything now drains immediately. The sync-pass drain stays: the first makes an upload feel immediate, the second is what eventually delivers the exports made in a tunnel. Committed without the parallel session's in-flight collection work, which is mid-save and does not compile; verified by stashing it and building this tree alone. 281 dr-ui tests pass, clippy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
705 lines
31 KiB
Plaintext
705 lines
31 KiB
Plaintext
import { Theme } from "theme.slint";
|
||
import { Button, PanelHeading, Label, Value, Caption, Panel, ProgressBar, ActivityRow } from "widgets.slint";
|
||
import { Segmented, TextRow, Check, SliderRow } from "controls.slint";
|
||
|
||
// Settings: how much disk the app may spend, and what an export defaults to.
|
||
//
|
||
// A full-window page rather than a modal dialogue. Two reasons, and the second
|
||
// is the load-bearing one. A modal has to be dismissed to check anything it
|
||
// refers to, and these settings refer to the library constantly — how full the
|
||
// cache is, where exports land. And every control here saves on change
|
||
// (see `settings_ui.rs`), so there is no OK/Cancel pair for a modal to host;
|
||
// a dialogue frame with only a close button is a window pretending to be a
|
||
// decision.
|
||
//
|
||
// It replaces the view rather than overlaying it because the app already
|
||
// switches views this way — launch, library, develop — and an overlay would be
|
||
// a fourth mechanism for the same job.
|
||
|
||
// One background job: what it is, how far along, and what it last said.
|
||
//
|
||
// A row rather than a `Value`/`Caption` pair written out at the call site,
|
||
// because the list is built by a `for` and every row has to answer the same
|
||
// three questions in the same order — otherwise a failure reads as a different
|
||
// kind of thing from the job it happened to.
|
||
//
|
||
// The bar is drawn only while the job runs. A stopped job's fraction is either
|
||
// 1 (it finished, and a full bar is noise) or frozen part-way (it failed, and
|
||
// a bar sitting at 60% invites the reading "still going").
|
||
component ActivityItem inherits VerticalLayout {
|
||
in property <ActivityRow> job;
|
||
|
||
spacing: 4px;
|
||
|
||
HorizontalLayout {
|
||
spacing: Theme.gap;
|
||
|
||
Label {
|
||
text: root.job.title;
|
||
body: true;
|
||
// Faded once it is over: the list is ordered running-first, and
|
||
// this is what makes that ordering visible at a glance rather than
|
||
// something the reader has to work out from the text.
|
||
opacity: root.job.running ? 1.0 : 0.6;
|
||
}
|
||
|
||
Caption {
|
||
text: root.job.detail;
|
||
// A failure is the one thing here worth a colour (NFR-A11Y-3 is
|
||
// satisfied by the words, which say what failed; the hue only
|
||
// finds them faster).
|
||
warn: root.job.failed;
|
||
horizontal-alignment: right;
|
||
horizontal-stretch: 1;
|
||
overflow: elide;
|
||
}
|
||
}
|
||
|
||
if root.job.running: ProgressBar {
|
||
indeterminate: !root.job.determinate;
|
||
fraction: root.job.fraction;
|
||
}
|
||
}
|
||
|
||
export component SettingsPage inherits Rectangle {
|
||
// --- background activity --------------------------------------------
|
||
in property <[ActivityRow]> activity-rows;
|
||
in property <int> activity-running: 0;
|
||
/// Stopped jobs still listed: recent successes, and failures held until
|
||
/// they are read.
|
||
in property <int> activity-kept: 0;
|
||
callback clear-finished();
|
||
|
||
// --- cache ---------------------------------------------------------
|
||
in-out property <string> original-budget;
|
||
in property <bool> original-unlimited: false;
|
||
in-out property <string> thumbnail-budget;
|
||
in property <bool> thumbnail-unlimited: false;
|
||
in property <bool> keep-opened: true;
|
||
/// TRACES: FR-DEV-6
|
||
/// Whether copying settings carries the crop and rotation with it.
|
||
in property <bool> copy-includes-framing: false;
|
||
/// What the cache currently holds. Empty hides the line.
|
||
in property <string> cache-usage;
|
||
|
||
callback original-budget-changed(string);
|
||
callback original-unlimited-toggled(bool);
|
||
callback thumbnail-budget-changed(string);
|
||
callback thumbnail-unlimited-toggled(bool);
|
||
callback keep-opened-toggled(bool);
|
||
callback copy-includes-framing-toggled(bool);
|
||
|
||
// --- export --------------------------------------------------------
|
||
in property <[string]> format-labels;
|
||
in property <int> format-selected: 0;
|
||
in property <int> quality: 90;
|
||
in property <bool> quality-enabled: true;
|
||
in property <[string]> colour-labels;
|
||
in property <int> colour-selected: 0;
|
||
in property <[string]> sizing-labels;
|
||
in property <int> sizing-selected: 0;
|
||
in-out property <int> sizing-value: 0;
|
||
in property <bool> sizing-has-value: false;
|
||
in property <string> sizing-unit: "px";
|
||
in property <bool> allow-upscaling: false;
|
||
in property <[string]> sharpening-labels;
|
||
in property <int> sharpening-selected: 0;
|
||
in-out property <string> filename-template;
|
||
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 ---------------------------------------
|
||
//
|
||
// 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;
|
||
|
||
callback format-picked(int);
|
||
callback quality-changed(int);
|
||
callback colour-picked(int);
|
||
callback sizing-picked(int);
|
||
callback sizing-value-changed(string);
|
||
callback upscaling-toggled(bool);
|
||
callback sharpening-picked(int);
|
||
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();
|
||
|
||
/// A save failed. The page's whole contract is that what it shows is
|
||
/// stored, so this cannot be swallowed.
|
||
in property <string> error;
|
||
|
||
callback close();
|
||
callback reset-defaults();
|
||
|
||
background: Theme.ground;
|
||
|
||
// Somewhere for a key to start from.
|
||
//
|
||
// Slint delivers a key to the focused item and bubbles it up the ancestors;
|
||
// with nothing focused there is no chain at all and the event is dropped.
|
||
// This page has only text fields, none of which claims focus on show — so
|
||
// without this holder Android's back gesture would find no focus item here
|
||
// and close the application instead of closing the page. Zero height, so it
|
||
// takes no room in the column, and it handles nothing itself: the shell
|
||
// above answers `Key.Back`.
|
||
FocusScope {
|
||
width: 0px;
|
||
height: 0px;
|
||
init => { self.focus(); }
|
||
}
|
||
|
||
VerticalLayout {
|
||
// --- header ----------------------------------------------------
|
||
//
|
||
// 44px and `surface`, matching the library's header exactly: this is
|
||
// the same kind of bar in the same place, and a page that drew its own
|
||
// height would read as a different application.
|
||
Rectangle {
|
||
height: 44px;
|
||
background: Theme.surface;
|
||
|
||
HorizontalLayout {
|
||
padding-left: Theme.gap;
|
||
padding-right: Theme.gap;
|
||
spacing: Theme.gap;
|
||
|
||
Button {
|
||
text: "‹ Back";
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.close(); }
|
||
}
|
||
|
||
Value { text: "Settings"; }
|
||
|
||
Rectangle { horizontal-stretch: 1; }
|
||
|
||
Caption {
|
||
// Says where the file is, because a settings page that
|
||
// saves silently gives the user nothing to point a backup
|
||
// or a support question at.
|
||
text: "Saved automatically";
|
||
vertical-alignment: center;
|
||
}
|
||
|
||
Button {
|
||
text: "Reset to defaults";
|
||
y: (parent.height - self.height) / 2;
|
||
clicked => { root.reset-defaults(); }
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
y: parent.height - 1px;
|
||
height: 1px;
|
||
background: Theme.rule;
|
||
}
|
||
}
|
||
|
||
// A failed write, above the content: it applies to everything below
|
||
// and the user needs it before they keep editing into a file that is
|
||
// not being written.
|
||
if root.error != "": Rectangle {
|
||
height: 32px;
|
||
background: Theme.surface;
|
||
HorizontalLayout {
|
||
padding-left: Theme.gap;
|
||
padding-right: Theme.gap;
|
||
Caption { text: root.error; warn: true; overflow: elide; }
|
||
}
|
||
}
|
||
|
||
Flickable {
|
||
vertical-stretch: 1;
|
||
viewport-height: content.preferred-height;
|
||
|
||
content := VerticalLayout {
|
||
width: 100%;
|
||
padding: Theme.gap-lg;
|
||
spacing: Theme.gap-lg;
|
||
alignment: start;
|
||
|
||
// The column is capped rather than filling the window. A
|
||
// settings form stretched across a 2560px display puts its
|
||
// label at one edge and its control at the other; 680px is
|
||
// about 90 characters of `text`, which is a readable measure.
|
||
// `min` so a narrow window still uses what it has (FR-UI-1).
|
||
property <length> column: min(root.width - 2 * Theme.gap-lg, 680px);
|
||
|
||
// --- background activity ---------------------------------
|
||
//
|
||
// First on the page, and the only panel here that is not a
|
||
// preference. It earns the position by being the one thing
|
||
// that changes while the page is open: a transfer the user
|
||
// came here to check on is answered before they have scrolled.
|
||
Rectangle {
|
||
width: content.column;
|
||
height: activity.preferred-height;
|
||
|
||
activity := Panel {
|
||
width: 100%;
|
||
|
||
HorizontalLayout {
|
||
spacing: Theme.gap;
|
||
|
||
PanelHeading { text: "BACKGROUND ACTIVITY"; }
|
||
|
||
// The count beside the heading, because the list
|
||
// below carries stopped jobs too and "how many are
|
||
// running" should not have to be counted by eye.
|
||
Caption {
|
||
text: root.activity-running > 0
|
||
? root.activity-running + " running" : "";
|
||
horizontal-alignment: right;
|
||
horizontal-stretch: 1;
|
||
}
|
||
}
|
||
|
||
Caption {
|
||
text: "Scans, transfers and syncs running behind the "
|
||
+ "interface. Everything here survives leaving "
|
||
+ "this page; nothing here needs it open.";
|
||
wrap: word-wrap;
|
||
}
|
||
|
||
Rectangle { height: Theme.gap-sm; }
|
||
|
||
// Says nothing is running, rather than showing an empty
|
||
// box: a list that is empty because the work finished
|
||
// and one that is empty because the register is broken
|
||
// look identical otherwise.
|
||
if root.activity-rows.length == 0: Caption {
|
||
text: "Nothing running.";
|
||
}
|
||
|
||
for row in root.activity-rows: ActivityItem {
|
||
job: row;
|
||
}
|
||
|
||
if root.activity-kept > 0: Rectangle {
|
||
height: Theme.control-height;
|
||
|
||
Button {
|
||
x: 0;
|
||
text: "Clear finished";
|
||
clicked => { root.clear-finished(); }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- storage ---------------------------------------------
|
||
Rectangle {
|
||
width: content.column;
|
||
height: storage.preferred-height;
|
||
|
||
storage := Panel {
|
||
width: 100%;
|
||
|
||
PanelHeading { text: "STORAGE"; }
|
||
|
||
Caption {
|
||
text: "How much of this device's disk DarkRoom may use. "
|
||
+ "These are per-device and never travel with the library.";
|
||
wrap: word-wrap;
|
||
}
|
||
|
||
// What is actually held, before what is allowed:
|
||
// a ceiling means nothing without the current figure
|
||
// to judge it against.
|
||
if root.cache-usage != "": Value {
|
||
text: root.cache-usage;
|
||
compact: true;
|
||
}
|
||
|
||
Rectangle { height: Theme.gap-sm; }
|
||
|
||
TextRow {
|
||
label: "Cached originals";
|
||
hint: "evicted oldest-first when full";
|
||
text <=> root.original-budget;
|
||
enabled: !root.original-unlimited;
|
||
placeholder: "8.0 GB";
|
||
accepted(t) => { root.original-budget-changed(t); }
|
||
}
|
||
|
||
Check {
|
||
label: "No limit on cached originals";
|
||
hint: "Nothing is ever evicted for space. "
|
||
+ "Pinned photographs are kept regardless.";
|
||
checked: root.original-unlimited;
|
||
toggled(on) => { root.original-unlimited-toggled(on); }
|
||
}
|
||
|
||
Rectangle { height: Theme.gap-sm; }
|
||
|
||
TextRow {
|
||
label: "Thumbnails and previews";
|
||
hint: "what the grid draws from";
|
||
text <=> root.thumbnail-budget;
|
||
enabled: !root.thumbnail-unlimited;
|
||
placeholder: "2.0 GB";
|
||
accepted(t) => { root.thumbnail-budget-changed(t); }
|
||
}
|
||
|
||
Check {
|
||
label: "No limit on thumbnails";
|
||
checked: root.thumbnail-unlimited;
|
||
toggled(on) => { root.thumbnail-unlimited-toggled(on); }
|
||
}
|
||
|
||
Rectangle { height: Theme.gap-sm; }
|
||
|
||
Check {
|
||
label: "Keep originals after opening them";
|
||
hint: "The file was downloaded anyway, so keeping it "
|
||
+ "costs no bandwidth and saves the transfer next time.";
|
||
checked: root.keep-opened;
|
||
toggled(on) => { root.keep-opened-toggled(on); }
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- develop (FR-DEV-6) ----------------------------------
|
||
Rectangle {
|
||
width: content.column;
|
||
height: develop-panel.preferred-height;
|
||
|
||
develop-panel := Panel {
|
||
width: 100%;
|
||
spacing: Theme.gap;
|
||
|
||
PanelHeading { text: "DEVELOP"; }
|
||
|
||
Check {
|
||
label: "Copy crop and rotation with settings";
|
||
// Says what the *off* state does, because off is
|
||
// the default and is the behaviour that needs
|
||
// explaining: a user who has never opened this
|
||
// page should still be able to predict what a
|
||
// paste did to their crops.
|
||
hint: "Off, pasting settings changes only colour and "
|
||
+ "tone, and each photograph keeps its own "
|
||
+ "composition. On, the crop, straightening, "
|
||
+ "rotation and flips travel too — which "
|
||
+ "re-frames every image pasted onto.";
|
||
checked: root.copy-includes-framing;
|
||
toggled(on) => { root.copy-includes-framing-toggled(on); }
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- export ----------------------------------------------
|
||
Rectangle {
|
||
width: content.column;
|
||
height: export-panel.preferred-height;
|
||
|
||
export-panel := Panel {
|
||
width: 100%;
|
||
spacing: Theme.gap;
|
||
|
||
PanelHeading { text: "EXPORT DEFAULTS"; }
|
||
|
||
Caption {
|
||
text: "What an export starts from. Every one of these "
|
||
+ "is still changeable per export.";
|
||
wrap: word-wrap;
|
||
}
|
||
|
||
Segmented {
|
||
label: "Format";
|
||
options: root.format-labels;
|
||
selected: root.format-selected;
|
||
picked(i) => { root.format-picked(i); }
|
||
}
|
||
|
||
// A bounded number, so it gets the control for one.
|
||
//
|
||
// This was a free-text field: the range lived in the
|
||
// hint and was enforced nowhere, and `to-float()`
|
||
// answers 0 for anything unparseable — so a typo saved
|
||
// a quality of 0 and the page then showed the 0 back as
|
||
// though it had been asked for. The track carries the
|
||
// range and the box refuses what it cannot read.
|
||
SliderRow {
|
||
label: "Quality";
|
||
// Says why it is greyed rather than leaving the
|
||
// user to work out that PNG has no quality.
|
||
hint: root.quality-enabled ? "1 to 100"
|
||
: "the chosen format is lossless";
|
||
value: root.quality;
|
||
// No meaningful neutral: quality has a sensible
|
||
// default but not a *zero*, and a default marker
|
||
// partway along a track reads as one.
|
||
default-value: 1;
|
||
minimum: 1;
|
||
maximum: 100;
|
||
enabled: root.quality-enabled;
|
||
changed(v) => { root.quality-changed(v); }
|
||
}
|
||
|
||
Segmented {
|
||
label: "Colour space";
|
||
hint: "profile embedded on export";
|
||
options: root.colour-labels;
|
||
selected: root.colour-selected;
|
||
picked(i) => { root.colour-picked(i); }
|
||
}
|
||
|
||
Segmented {
|
||
label: "Size";
|
||
options: root.sizing-labels;
|
||
selected: root.sizing-selected;
|
||
picked(i) => { root.sizing-picked(i); }
|
||
}
|
||
|
||
// Only where the chosen mode carries a number:
|
||
// "Original" has none, and a field showing 0 beside it
|
||
// would invite the reading "zero pixels".
|
||
if root.sizing-has-value: TextRow {
|
||
label: "Size value";
|
||
text: root.sizing-value;
|
||
unit: root.sizing-unit;
|
||
field-width: 90px;
|
||
accepted(t) => { root.sizing-value-changed(t); }
|
||
}
|
||
|
||
Check {
|
||
label: "Allow upscaling";
|
||
hint: "Off, a request larger than the source exports "
|
||
+ "at source size rather than failing.";
|
||
checked: root.allow-upscaling;
|
||
toggled(on) => { root.upscaling-toggled(on); }
|
||
}
|
||
|
||
Segmented {
|
||
label: "Output sharpening";
|
||
hint: "scaled by the resize factor";
|
||
options: root.sharpening-labels;
|
||
selected: root.sharpening-selected;
|
||
picked(i) => { root.sharpening-picked(i); }
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- naming and destination ------------------------------
|
||
//
|
||
// Its own panel rather than more of the export one: format and
|
||
// size describe the image, these describe the file. The export
|
||
// panel was long enough that the boundary was worth drawing.
|
||
Rectangle {
|
||
width: content.column;
|
||
height: naming.preferred-height;
|
||
|
||
naming := Panel {
|
||
width: 100%;
|
||
spacing: Theme.gap;
|
||
|
||
PanelHeading { text: "FILES AND METADATA"; }
|
||
|
||
TextRow {
|
||
label: "Filename template";
|
||
hint: "{name} {seq} {date} {dimensions} {preset}";
|
||
text <=> root.filename-template;
|
||
field-width: 260px;
|
||
placeholder: "{name}";
|
||
accepted(t) => { root.template-changed(t); }
|
||
}
|
||
|
||
Segmented {
|
||
label: "If the file exists";
|
||
options: root.collision-labels;
|
||
selected: root.collision-selected;
|
||
picked(i) => { root.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: root.target-labels;
|
||
selected: root.target-selected;
|
||
picked(i) => { root.target-picked(i); }
|
||
}
|
||
|
||
TextRow {
|
||
label: "Destination";
|
||
hint: "empty asks each time";
|
||
text <=> root.destination;
|
||
field-width: 320px;
|
||
placeholder: root.destination-hint;
|
||
accepted(t) => { root.destination-changed(t); }
|
||
}
|
||
|
||
// 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 root.browse-available && !root.browse-open: HorizontalLayout {
|
||
alignment: start;
|
||
Button {
|
||
text: "Choose folder…";
|
||
clicked => { root.browse-open-picker(); }
|
||
}
|
||
}
|
||
|
||
if root.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: !root.browse-at-root;
|
||
clicked => { root.browse-up(); }
|
||
}
|
||
|
||
Value {
|
||
text: root.browse-path;
|
||
overflow: elide;
|
||
horizontal-stretch: 1;
|
||
vertical-alignment: center;
|
||
}
|
||
|
||
Caption {
|
||
text: root.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 root.browse-entries.length == 0
|
||
&& !root.browse-loading: Caption {
|
||
text: "No folders here. "
|
||
+ "Use this one, or go up.";
|
||
}
|
||
|
||
for name in root.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 => { root.browse-into(name); }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
HorizontalLayout {
|
||
spacing: Theme.gap-sm;
|
||
alignment: end;
|
||
|
||
Button {
|
||
text: "Cancel";
|
||
clicked => { root.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 => { root.browse-confirm(); }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Check {
|
||
label: "Strip location and personal metadata";
|
||
hint: "On. An export is usually the copy that leaves "
|
||
+ "this machine, and a location embedded in a "
|
||
+ "published photograph cannot be recalled.";
|
||
checked: root.strip-location;
|
||
toggled(on) => { root.strip-location-toggled(on); }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|