import { AboutSlint, Button, Palette, HorizontalBox, ScrollView } from "std-widgets.slint"; enum PreviewKind { None, Thumbhash, Thumbnail, } struct ImagePreview { asset_id: string, image: image, kind: PreviewKind, } struct ImageBucket { key: string, title: string, count: int, previews: [ImagePreview], } enum ViewState { Hidden, NearView, InView, } export global Global { in-out property min-image-size: 100px; in-out property image-margin: 2px; in-out property <[ImageBucket]> image-buckets: [ { key: "2026-02-01", title: "Feb 1, 2026", count: 12 }, { key: "2026-02-02", title: "Feb 2, 2026", count: 12 }, { key: "2026-02-03", title: "Feb 3, 2026", count: 12 }, ]; callback bucket-view-state(string, ViewState); } component Header inherits Rectangle { width: 100%; height: 48px; background: Palette.alternate-background; HorizontalBox { height: parent.height; Text { text: "immich"; } } } component ImagePreview inherits Rectangle { in property preview; in property size: 32px; width: size; height: size; Image { width: 100%; height: 100%; source: preview; } touch := TouchArea { clicked => { } } } component TimelineBlock inherits VerticalLayout { in property index: -1; in property bucket; function calc-view-state() -> ViewState { // TODO: fix this function return ViewState.InView; } in property view-state: calc-view-state(); property min-image-size: Global.min-image-size; property min-size-with-margin: min-image-size + Global.image-margin; property count-x: self.width / min-size-with-margin; 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; min-width: min-image-size; alignment: start; height: title-box.height + count-y * image-size-with-margin; // FIXME: this triggers recursion errors in slint. // changed view-state => { // Global.bucket-view-state(bucket.key, view-state); // } // TODO: don't render subtree when self.view-state = Hidden title-box := HorizontalBox { alignment: space-between; height: 36px; Text { text: bucket.title; } // TODO: checkbox thingy Text { text: "O"; } } // TODO: don't render subtree when self.view-state = Hidden image-box := Rectangle { width: 100%; height: count-y * image-size-with-margin; for preview[i] in bucket.previews : ImagePreview { preview: preview.image; 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); } } } // component ImageViewer inherits Rectangle { // in property image; // width: 100%; // height: 100%; // background: black; // } export component AppWindow inherits Window { VerticalLayout { padding: 0px; width: 100%; Header {} ScrollView { width: 100%; mouse-drag-pan-enabled: true; // viewport-height: 20000px; // self.viewport-y goes into the negative. invert it to make things easier. // property viewport-top-y: -self.viewport-y; // property viewport-bottom-y: viewport-top-y + self.height; // property almost-visible-margin: self.height; // function is-visible(top-y: length, bottom-y: length) -> bool { // top-y <= viewport-bottom-y && viewport-top-y <= bottom-y // } // function is-almost-visible(top-y: length, bottom-y: length) -> bool { // top-y <= (viewport-bottom-y + almost-visible-margin) // && (viewport-top-y - almost-visible-margin) <= bottom-y // } // function view-state(top-y: length, bottom-y: length) -> ViewState { // if is-visible(top-y, bottom-y) { // return ViewState.InView; // } // if is-almost-visible(top-y, bottom-y) { // return ViewState.NearView; // } // return ViewState.Hidden; // } VerticalLayout { alignment: start; padding: 0px; width: 100%; for bucket[i] in Global.image-buckets : TimelineBlock { width: 100%; index: i; bucket: bucket; // view-state: view-state(self.y, self.y + self.height); } } } } }