import { ScrollView, Palette } from "std-widgets.slint"; import { Global } from "global.slint"; import { ImageBucket, Visibility, ImagePreview } from "types.slint"; export component ImagePreview inherits Rectangle { in property preview; in property size: 32px; callback clicked <=> touch.clicked; width: size; height: size; clip: true; Image { width: preview.ratio < 1.0 ? size : size * preview.ratio; height: preview.ratio > 1.0 ? size : size / preview.ratio; source: preview.image; } touch := TouchArea {} } component TimelineBlock inherits VerticalLayout { in property index: -1; in-out property bucket; property min-image-size: Global.min-image-size; property min-size-with-margin: min-image-size + Global.image-margin; property count-x: Math.floor(self.width / min-size-with-margin); // TODO: or is it ceil? property count-y: Math.ceil(bucket.count / count-x); function calc-image-size() -> length { let remaining-length = Math.mod(self.width, min-size-with-margin); min-image-size + remaining-length / count-x } property image-size: calc-image-size(); property image-size-with-margin: image-size + Global.image-margin; property title-box-height: 44px; height: title-box-height + count-y * image-size-with-margin; y: bucket.y; min-width: min-image-size; alignment: start; title-box := Rectangle { property checked: false; HorizontalLayout { alignment: space-between; height: title-box-height; padding: 8px; title := Text { text: bucket.title; font-size: 20px; } if !checked : Image { source: @image-url("../assets/unchecked.svg"); colorize: Palette.foreground; opacity: 0.8; height: title.height; width: self.height; } if checked : Image { source: @image-url("../assets/checked.svg"); colorize: Palette.accent-background; height: title.height; width: self.height; } } title-touch := TouchArea { clicked => { parent.checked = !parent.checked; } } } image-box := Rectangle { width: 100%; height: count-y * image-size-with-margin; for preview[i] in bucket.previews : ImagePreview { preview: preview; size: image-size; x: Global.image-margin / 2 + Math.mod(i, count-x) * (Global.image-margin + image-size); y: Math.floor(i / count-x) * (image-size + Global.image-margin); clicked => { Global.viewed-image = preview; Global.view-image(preview.asset-id); } } } } export component ScrollHandle { out property maximum: 1; out property minimum: 0; in-out property value; callback dragged(float); width: handle.width * 0.66; horizontal-stretch: 0; vertical-stretch: 1; height: 100%; handle := Rectangle { x: 0; width: 64px; height: self.width; border-width: 3px; border-radius: self.height / 2; background: touch.pressed ? Palette.accent-background : Palette.alternate-background; border-color: Palette.accent-foreground; y: (root.height - handle.height) * (root.value - root.minimum)/(root.maximum - root.minimum); touch := TouchArea { moved => { if (self.enabled && self.pressed) { root.value = max(root.minimum, min(root.maximum, root.value + (self.mouse-y - self.pressed-y) * (root.maximum - root.minimum) / root.height)); dragged(root.value) } } } } } export component Timeline { scroll-view := ScrollView { mouse-drag-pan-enabled: true; viewport-height: rect.height; vertical-scrollbar-policy: always-off; horizontal-scrollbar-policy: always-off; scrolled => { // sync ScrollHandle with ScrollView scroll-handle.value = (-scroll-view.viewport-y) / scroll-view.viewport-height; } changed viewport-y => { Global.timeline-scrolled(-self.viewport-y); } rect := Rectangle { y: 0; x: 0; width: root.width; height: Global.timeline-height; preferred-width: self.width; preferred-height: self.height; for bucket[i] in Global.image-buckets : Rectangle { if bucket.visibility == Visibility.InView : TimelineBlock { width: root.width; index: i; bucket: bucket; } } } } scroll-handle := ScrollHandle { x: parent.x + parent.width - self.width; height: root.height; dragged(value) => { // sync ScrollView with ScrollHandle scroll-view.viewport-y = -(value * scroll-view.viewport-height); } } }