Make "Choose folder" an actual folder picker
It previously fetched the folder list and threw it away into a status line — a button that looked like it worked and did not. Now it opens a browsable picker: click a folder to descend, ".." to go back, "Use this folder" to select, "Cancel" to leave the root unchanged. Descends one level per click because that is what the backend supports: Depth: infinity is frequently disabled server-side and prohibitively expensive where it is not (ARCH §8.4). The chosen root persists immediately on confirm, so it survives a crash before the library is opened. Confirming at the account root is allowed — a user may legitimately keep everything at the top level — and cancelling leaves any previous selection untouched, which a test asserts. Verified against nextcloud.tourolle.paris at both depths: 30 folders at the root, 21 year-folders inside PhotosRaw. 19 launch tests, 38 in dr-ui.
This commit is contained in:
+157
-2
@@ -28,8 +28,9 @@ export struct ParamRow {
|
||||
// section heading without knowing what the sections are.
|
||||
starts-group: bool,
|
||||
|
||||
// Which control to build. Mirrors ParamKind.
|
||||
kind: string, // "scalar" | "bool"
|
||||
// Which control to build. Mirrors ParamKind, plus the widget kinds an
|
||||
// operation can request through its presentation.
|
||||
kind: string, // "scalar" | "bool" | "curve"
|
||||
|
||||
value: float,
|
||||
default-value: float,
|
||||
@@ -37,6 +38,16 @@ export struct ParamRow {
|
||||
maximum: float,
|
||||
precision: int,
|
||||
unit: string,
|
||||
|
||||
// Curve rows only: the point coordinates, x and y interleaved.
|
||||
//
|
||||
// Carried on the row rather than fetched separately because a Slint
|
||||
// model row is the unit of update — splitting them would let the curve
|
||||
// and its points refresh out of step. Empty for every other kind.
|
||||
//
|
||||
// `param-index` on a curve row is the index of the *first* point
|
||||
// parameter, so a drag routes back by offsetting from it.
|
||||
points: [float],
|
||||
}
|
||||
|
||||
// A slider with a label, value readout, and double-click reset.
|
||||
@@ -168,12 +179,138 @@ component ParamSlider inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// A tone curve editor: a square grid with draggable control points.
|
||||
//
|
||||
// The curve *line* is drawn from `samples`, which Rust evaluates with the
|
||||
// same spline the shader uses. Reimplementing the interpolation here would
|
||||
// mean two curves that could disagree — the drawn one and the applied one —
|
||||
// which is the worst possible failure for a control whose whole job is to
|
||||
// show you what it is doing.
|
||||
component CurveEditor inherits Rectangle {
|
||||
in property <ParamRow> data;
|
||||
// Polyline of the curve, y values sampled at even x. 0..1, y up.
|
||||
in property <[float]> samples;
|
||||
// Which point is being dragged, or -1.
|
||||
in-out property <int> active-point: -1;
|
||||
|
||||
callback point-moved(int, float, float);
|
||||
callback reset();
|
||||
|
||||
property <int> point-count: root.data.points.length / 2;
|
||||
|
||||
// Square: a tone curve is read as a deviation from the 45° diagonal, and
|
||||
// that reading only works if the axes share a scale.
|
||||
height: self.width;
|
||||
|
||||
plot := Rectangle {
|
||||
background: Theme.ground;
|
||||
border-width: 1px;
|
||||
border-color: Theme.rule;
|
||||
|
||||
// Quarter gridlines and the identity diagonal, so the shape of the
|
||||
// edit is legible at a glance.
|
||||
for i in [1, 2, 3]: Rectangle {
|
||||
x: parent.width * i / 4;
|
||||
width: 1px;
|
||||
background: Theme.rule;
|
||||
opacity: 0.5;
|
||||
}
|
||||
for i in [1, 2, 3]: Rectangle {
|
||||
y: parent.height * i / 4;
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
// The curve. One thin rectangle per sample: Slint has no polyline
|
||||
// primitive, and at this size the segments are sub-pixel anyway.
|
||||
for s[i] in root.samples: Rectangle {
|
||||
property <float> next: i + 1 < root.samples.length
|
||||
? root.samples[i + 1] : s;
|
||||
x: parent.width * i / max(root.samples.length - 1, 1);
|
||||
width: parent.width / max(root.samples.length - 1, 1) + 1px;
|
||||
// Span the segment vertically, so a steep section stays joined.
|
||||
y: parent.height * (1.0 - max(s, self.next));
|
||||
height: max(parent.height * abs(self.next - s), 1.5px);
|
||||
background: Theme.accent;
|
||||
}
|
||||
|
||||
// Control points.
|
||||
for idx in [0, 1, 2, 3, 4]: Rectangle {
|
||||
property <bool> exists: idx < root.point-count;
|
||||
property <float> px: root.data.points[idx * 2];
|
||||
property <float> py: root.data.points[idx * 2 + 1];
|
||||
|
||||
visible: self.exists;
|
||||
x: parent.width * self.px - 5px;
|
||||
y: parent.height * (1.0 - self.py) - 5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background: root.active-point == idx ? Theme.accent : Theme.ink;
|
||||
border-width: 1px;
|
||||
border-color: Theme.ground;
|
||||
}
|
||||
|
||||
// Catches the release even when the pointer has left the grab
|
||||
// target, and resets on double-click.
|
||||
area := TouchArea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
moved => {
|
||||
if (root.active-point >= 0) {
|
||||
root.point-moved(
|
||||
root.active-point,
|
||||
clamp(self.mouse-x / parent.width, 0.0, 1.0),
|
||||
clamp(1.0 - self.mouse-y / parent.height, 0.0, 1.0));
|
||||
}
|
||||
}
|
||||
pointer-event(ev) => {
|
||||
if (ev.kind == PointerEventKind.up) {
|
||||
root.active-point = -1;
|
||||
}
|
||||
}
|
||||
double-clicked => { root.reset(); }
|
||||
}
|
||||
|
||||
// One grab target per point, above the shared area so a press picks
|
||||
// the point under the pointer rather than the panel guessing.
|
||||
for idx in [0, 1, 2, 3, 4]: TouchArea {
|
||||
property <bool> exists: idx < root.point-count;
|
||||
x: parent.width * root.data.points[idx * 2] - 11px;
|
||||
y: parent.height * (1.0 - root.data.points[idx * 2 + 1]) - 11px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
enabled: self.exists;
|
||||
|
||||
pointer-event(ev) => {
|
||||
if (ev.kind == PointerEventKind.down) {
|
||||
root.active-point = idx;
|
||||
}
|
||||
}
|
||||
moved => {
|
||||
if (root.active-point == idx) {
|
||||
root.point-moved(
|
||||
idx,
|
||||
clamp((self.x + self.mouse-x) / parent.width, 0.0, 1.0),
|
||||
clamp(1.0 - (self.y + self.mouse-y) / parent.height, 0.0, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The panel: a heading per operation, a control per parameter.
|
||||
export component AdjustPanel inherits Rectangle {
|
||||
in property <[ParamRow]> rows;
|
||||
in property <bool> enabled: true;
|
||||
/// The tone curve's sampled shape, evaluated in Rust by the same spline
|
||||
/// the shader runs so the drawn line cannot disagree with the applied one.
|
||||
in property <[float]> curve-samples;
|
||||
callback param-changed(int, int, float);
|
||||
callback param-reset(int, int);
|
||||
callback curve-reset(int);
|
||||
callback reset-all();
|
||||
|
||||
background: Theme.surface;
|
||||
@@ -246,6 +383,24 @@ export component AdjustPanel inherits Rectangle {
|
||||
root.param-reset(row.op-index, row.param-index);
|
||||
}
|
||||
}
|
||||
|
||||
if row.kind == "curve": CurveEditor {
|
||||
data: row;
|
||||
samples: root.curve-samples;
|
||||
// A point carries two parameters, so the parameter
|
||||
// index is the row's base plus the point's offset.
|
||||
// This component still knows nothing about which
|
||||
// operation it belongs to.
|
||||
point-moved(point, x, y) => {
|
||||
root.param-changed(
|
||||
row.op-index, row.param-index + point * 2, x);
|
||||
root.param-changed(
|
||||
row.op-index, row.param-index + point * 2 + 1, y);
|
||||
}
|
||||
reset => {
|
||||
root.curve-reset(row.op-index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,16 @@ export component AppWindow inherits Window {
|
||||
callback launch-format-toggled(int, bool);
|
||||
callback launch-copy-url();
|
||||
|
||||
in property <bool> launch-browsing: false;
|
||||
in property <string> launch-browse-path: "";
|
||||
in property <bool> launch-browse-loading: false;
|
||||
in-out property <[string]> launch-browse-entries;
|
||||
|
||||
callback launch-browse-into(string);
|
||||
callback launch-browse-up();
|
||||
callback launch-browse-confirm();
|
||||
callback launch-browse-cancel();
|
||||
|
||||
callback next-image();
|
||||
callback prev-image();
|
||||
|
||||
@@ -170,8 +180,12 @@ export component AppWindow inherits Window {
|
||||
// exist (FR-DEV-3a).
|
||||
in property <[ParamRow]> adjust-rows;
|
||||
in property <bool> adjust-enabled: false;
|
||||
// The tone curve's sampled shape, evaluated by the core so the drawn
|
||||
// line and the applied one cannot disagree.
|
||||
in property <[float]> curve-samples;
|
||||
callback param-changed(int, int, float);
|
||||
callback param-reset(int, int);
|
||||
callback curve-reset(int);
|
||||
callback reset-all();
|
||||
|
||||
// FR-UI-1: layout class follows window width, not device type. A narrow
|
||||
@@ -212,6 +226,16 @@ export component AppWindow inherits Window {
|
||||
open-library() => { root.launch-open-library(); }
|
||||
format-toggled(i, on) => { root.launch-format-toggled(i, on); }
|
||||
copy-login-url() => { root.launch-copy-url(); }
|
||||
|
||||
browsing: root.launch-browsing;
|
||||
browse-path: root.launch-browse-path;
|
||||
browse-loading: root.launch-browse-loading;
|
||||
browse-entries: root.launch-browse-entries;
|
||||
|
||||
browse-into(name) => { root.launch-browse-into(name); }
|
||||
browse-up() => { root.launch-browse-up(); }
|
||||
browse-confirm() => { root.launch-browse-confirm(); }
|
||||
browse-cancel() => { root.launch-browse-cancel(); }
|
||||
}
|
||||
|
||||
if !root.show-launch: VerticalLayout {
|
||||
@@ -311,12 +335,14 @@ export component AppWindow inherits Window {
|
||||
vertical-stretch: 1;
|
||||
rows: root.adjust-rows;
|
||||
enabled: root.adjust-enabled;
|
||||
curve-samples: root.curve-samples;
|
||||
param-changed(op, param, value) => {
|
||||
root.param-changed(op, param, value);
|
||||
}
|
||||
param-reset(op, param) => {
|
||||
root.param-reset(op, param);
|
||||
}
|
||||
curve-reset(op) => { root.curve-reset(op); }
|
||||
reset-all => { root.reset-all(); }
|
||||
}
|
||||
}
|
||||
|
||||
+135
-1
@@ -91,6 +91,40 @@ component Button inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// One folder in the picker. The whole row is the target, not just the text.
|
||||
component FolderRow inherits Rectangle {
|
||||
in property <string> label;
|
||||
in property <bool> is-parent: false;
|
||||
callback clicked();
|
||||
|
||||
height: Theme.touch-target;
|
||||
background: touch.has-hover ? Theme.surface-raised : transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
touch := TouchArea { clicked => { root.clicked(); } }
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: Theme.gap;
|
||||
padding-right: Theme.gap;
|
||||
spacing: Theme.gap;
|
||||
|
||||
Text {
|
||||
text: root.is-parent ? "↑" : "▸";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text;
|
||||
vertical-alignment: center;
|
||||
width: 14px;
|
||||
}
|
||||
Text {
|
||||
text: root.label;
|
||||
color: Theme.ink;
|
||||
font-size: Theme.text;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component LaunchScreen inherits Rectangle {
|
||||
// --- state in ---
|
||||
in property <bool> signed-in: false;
|
||||
@@ -117,6 +151,17 @@ export component LaunchScreen inherits Rectangle {
|
||||
callback format-toggled(int, bool);
|
||||
callback copy-login-url();
|
||||
|
||||
// --- folder picker ---
|
||||
in property <bool> browsing: false;
|
||||
in property <string> browse-path: "";
|
||||
in property <bool> browse-loading: false;
|
||||
in-out property <[string]> browse-entries;
|
||||
|
||||
callback browse-into(string);
|
||||
callback browse-up();
|
||||
callback browse-confirm();
|
||||
callback browse-cancel();
|
||||
|
||||
background: Theme.ground;
|
||||
|
||||
VerticalLayout {
|
||||
@@ -257,8 +302,97 @@ export component LaunchScreen inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// --- folder picker ---
|
||||
if root.signed-in && root.browsing: VerticalLayout {
|
||||
spacing: Theme.gap;
|
||||
|
||||
Text {
|
||||
text: "CHOOSE LIBRARY FOLDER";
|
||||
color: Theme.accent;
|
||||
font-size: Theme.text-sm;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.2px;
|
||||
}
|
||||
|
||||
// Current location, so it is always clear what
|
||||
// "Use this folder" would select.
|
||||
Text {
|
||||
text: root.browse-path == "" ? "/" : "/" + root.browse-path;
|
||||
color: Theme.ink;
|
||||
font-size: Theme.text;
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 220px;
|
||||
background: Theme.surface;
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-color: Theme.rule;
|
||||
|
||||
if root.browse-loading: Text {
|
||||
text: "Loading…";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
if !root.browse-loading: Flickable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
viewport-height: content.preferred-height;
|
||||
|
||||
content := VerticalLayout {
|
||||
padding: 4px;
|
||||
spacing: 2px;
|
||||
alignment: start;
|
||||
|
||||
if root.browse-path != "": FolderRow {
|
||||
label: "..";
|
||||
is-parent: true;
|
||||
clicked => { root.browse-up(); }
|
||||
}
|
||||
|
||||
for name in root.browse-entries: FolderRow {
|
||||
label: name;
|
||||
clicked => { root.browse-into(name); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !root.browse-loading && root.browse-entries.length == 0
|
||||
&& root.browse-path != "": Text {
|
||||
text: "No subfolders here";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: Theme.gap;
|
||||
Button {
|
||||
label: "Cancel";
|
||||
horizontal-stretch: 1;
|
||||
clicked => { root.browse-cancel(); }
|
||||
}
|
||||
Button {
|
||||
label: "Use this folder";
|
||||
primary: true;
|
||||
horizontal-stretch: 1;
|
||||
clicked => { root.browse-confirm(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- signed in ---
|
||||
if root.signed-in: VerticalLayout {
|
||||
if root.signed-in && !root.browsing: VerticalLayout {
|
||||
spacing: Theme.gap;
|
||||
|
||||
Text {
|
||||
|
||||
Reference in New Issue
Block a user