breezewiki/archiver/archiver-gui.rkt

391 lines
15 KiB
Racket
Raw Normal View History

#lang racket/base
(require racket/class
racket/draw
racket/format
2023-12-15 08:23:12 +00:00
racket/function
racket/list
2023-12-15 08:23:12 +00:00
racket/math
racket/port
racket/set
racket/splicing
racket/string
2023-12-15 08:23:12 +00:00
(except-in pict text table)
db
net/http-easy
memo
(only-in racket/gui timer%)
racket/gui/easy
racket/gui/easy/operator
(only-in pict bitmap)
images/icons/arrow
images/icons/control
images/icons/stickman
images/icons/style
images/icons/symbol
"archiver-database.rkt"
"archiver.rkt"
"../lib/url-utils.rkt"
"../lib/xexpr-utils.rkt")
(default-icon-material rubber-icon-material)
(require (for-syntax racket/base racket/match racket/set racket/string))
(define-syntax (@> stx)
(define form (cdr (syntax->datum stx)))
(match form
[(list form) ; (@> (fn @obs))
;; identify the observables and replace with non-@ symbols
(define collection (mutable-set))
(define updated
(let loop ([sexp form])
(cond [(symbol? sexp)
(let ([as-s (symbol->string sexp)])
(if (string-prefix? as-s "@")
(let ([without-@ (string->symbol (substring as-s 1))])
(set-add! collection (cons sexp without-@))
without-@)
sexp))]
[(pair? sexp) (cons (loop (car sexp)) (loop (cdr sexp)))]
[#t sexp])))
(define collection-l (set->list collection))
;; return obs-combine -> updated-form
(datum->syntax stx `(obs-combine (λ (,@(map cdr collection-l)) ,updated) ,@(map car collection-l)))]
[(list (? string? str) args ...) ; (@> "Blah: ~a/~a" @arg1 arg2)
;; identify the observables and replace with non-@ symbols
(define collection-l
(for/list ([arg args])
(if (symbol? arg)
(let ([as-s (symbol->string arg)])
(if (string-prefix? as-s "@")
(let ([without-@ (string->symbol (substring as-s 1))])
(cons arg without-@))
(cons #f arg)))
(cons #f arg))))
(define collection-lo (filter car collection-l))
;; return obs-combine -> format
(datum->syntax stx `(obs-combine (λ (,@(map cdr collection-lo)) (format ,str ,@(map cdr collection-l))) ,@(map car collection-lo)))]))
(define/obs @auto-retry #f)
2023-12-15 08:23:12 +00:00
(define-struct qi^ (wikiname st stage progress max-progress ticks eta th) #:transparent) ;; queue item
(define rows (query-rows* "select wikiname, progress from wiki where progress < 4"))
(define/obs @queue null)
(define (add-wikiname-to-queue wikiname st stage)
(@queue . <~ . (λ (queue)
(define already-exists? (findf (λ (qi) (equal? (qi^-wikiname qi) wikiname)) queue))
(if already-exists?
queue
2023-12-15 08:23:12 +00:00
(append queue (list (qi^ wikiname st stage 0 1 0 "..." #f)))))))
(for ([row rows])
(add-wikiname-to-queue (vector-ref row 0)
(if (= (vector-ref row 1) 4)
'complete
'queued)
(vector-ref row 1)))
(define status-icon-size 32)
(define status-icon-min-width 36)
(define button-icon-size 12)
(define color-green (make-color 90 212 68))
2023-12-15 08:23:12 +00:00
(define (resize coords fraction)
(for/list ([coord (in-list coords)])
(cons (* fraction (car coord))
(* fraction (cdr coord)))))
(define (flat-right-arrow #:height [height 32] #:color [color #f])
((if color
(curryr colorize color)
values)
(dc (λ (dc dx dy)
(send dc draw-polygon (resize
(list '(0 . 9) '(15 . 9) '(14 . 0)
'(31 . 15.5)
'(14 . 31) '(15 . 22) '(0 . 22))
(/ height 32))))
height height)))
(define (double-left-arrow-icon #:height [height 32])
(define shift (/ height 48))
(pict->bitmap
(scale-to-fit
(panorama
(pin-under
(bitmap
(left-over-arrow-icon #:color halt-icon-color #:height height
#:material rubber-icon-material))
(- (* -20 shift) 2) (+ (* 6 shift) 1)
(bitmap
(bitmap-render-icon
(pict->bitmap
(rotate
(flat-right-arrow #:color (make-object color% 255 64 64) #:height (/ height 1.26))
(* pi 1.23)))))
#;(rotate
(flat-right-arrow #:color (make-object color% 255 64 64) #:height (/ height 1.26))
(* pi 1.23))))
height height #:mode 'preserve/max)))
2023-12-13 10:09:00 +00:00
(splicing-let ([frame-count 20])
(define stickman-frames
(for/vector ([s (in-range 0 1 (/ 1 frame-count))])
(running-stickman-icon
s
#:height status-icon-size
2023-12-13 10:09:00 +00:00
#:material (default-icon-material)))))
2023-12-13 10:09:00 +00:00
(define (stick n)
(vector-ref stickman-frames (modulo n (vector-length stickman-frames))))
(define status-icons
(hasheq 'queued (stop-icon #:color syntax-icon-color #:height status-icon-size)
'paused (continue-forward-icon #:color syntax-icon-color #:height status-icon-size)
2023-12-13 10:09:00 +00:00
'running (stick 0)
'error (x-icon #:height status-icon-size)
'complete (check-icon #:color color-green #:height status-icon-size)))
(define action-icons
(hasheq 'pause (pause-icon #:color syntax-icon-color #:height button-icon-size)
'resume (play-icon #:color color-green #:height button-icon-size)
2023-12-15 08:23:12 +00:00
'reset (left-over-arrow-icon #:color halt-icon-color #:height button-icon-size)
'reseter (double-left-arrow-icon #:height button-icon-size)))
(define (bitmap-view @the-bitmap [min-width 1])
(pict-canvas #:min-size (@> (list (max min-width (send @the-bitmap get-width)) (send @the-bitmap get-height))) #;(if min-size (list min-size min-size) #f)
#:stretch '(#f #f)
#:style '(transparent)
@the-bitmap
bitmap))
(define (exn->string e)
(with-output-to-string
(λ ()
(displayln (exn-message e))
(displayln "context:")
(for ([item (continuation-mark-set->context (exn-continuation-marks e))])
(printf " ~a" (srcloc->string (cdr item)))
(when (car item)
(printf ": ~a" (car item)))
(displayln "")))))
(define ((handle-graphical-exn @qi) e)
(displayln (exn->string e) (current-error-port))
(cond
[(obs-peek @auto-retry)
(void) ;; TODO
#;(do-retry-end wikiname)]
[#t
(update-qi @qi [st 'error])
(do-try-unpause-next-entry)
(thread
(λ ()
(define/obs @visible? #t)
(render
(dialog #:title "Download Error"
#:style '(resize-border)
#:mixin (λ (%) (class % (super-new)
(obs-observe! @visible? (λ (visible?) (send this show visible?)))))
(vpanel #:margin '(15 15)
2023-12-13 10:09:00 +00:00
(text (format "Encountered this error while downloading ~a:" (qi^-wikiname (obs-peek @qi))))
(input #:style '(multiple hscroll)
#:min-size '(#f 200)
(exn->string e))
;; TODO
#;(button "Retry Now" (λ () (:= @visible? #f) (do-retry-now wikiname)))
#;(button "Retry Round-Robin" (λ () (:= @visible? #f) (do-retry-end wikiname)))
#;(button "Skip Wiki" (λ () (:= @visible? #f) (do-continue)))
#;(button "Use Auto-Retry" (λ ()
(:= @auto-retry #t)
(:= @visible? #f)
(do-retry-end wikiname)))
#;(text "Be careful not to auto-retry an infinite loop!")))
main-window)))
(sleep)
; make sure the broken thread is gone
(define th (qi^-th (obs-peek @qi)))
(when th (kill-thread th))]))
(define segments
(list
(list 5/100 (make-color 0 223 217))
(list 88/100 color-green)
(list 2/100 (make-color 0 223 217))
(list 5/100 color-green)))
(define segment-spacing 2)
(unless (= (apply + (map car segments)) 1)
(error 'segments "segments add up to ~a, not 1" (apply + (map car segments))))
;; return the new bitmap, which can be drawn on a dc<%>
(define/memoize (ray-trace width height stage progress max-progress)
;; (printf "rendering ~a ~a/~a at ~a~n" stage progress max-progress (current-inexact-milliseconds))
(define bm (make-object bitmap% width height #f #t))
(define dc (make-object bitmap-dc% bm))
(define width-available (- width (* (length segments) segment-spacing)))
(send dc set-smoothing 'unsmoothed)
(send dc set-pen "black" 0 'transparent)
(for/fold ([offset 0])
([segment segments]
[i (in-naturals 0)]) ;; zero indexed stages?
;; calculate start and end locations of grey bar
(define-values (segment-proportion segment-color) (apply values segment))
(define segment-start (if (= offset 0) 0 (+ offset segment-spacing)))
(define segment-width (* width-available segment-proportion))
;; draw grey bar
(send dc set-brush (make-color 180 180 180 0.4) 'solid)
(send dc draw-rectangle segment-start 0 segment-width height)
;; draw solid bar according to the current item's progress
(define proportion
(cond [(stage . < . i) 0]
[(stage . > . i) 1]
[(max-progress . <= . 0) 0]
[(progress . < . 0) 0]
[(progress . >= . max-progress) 1]
[else (progress . / . max-progress)]))
(send dc set-brush segment-color 'solid)
(send dc draw-rectangle segment-start 0 (* proportion segment-width) height)
(+ segment-start segment-width))
(bitmap-render-icon bm 6/8))
;; get ray traced bitmap (possibly from cache) and draw on dc<%>
(define (draw-bar orig-dc qi)
;; (println ray-traced)
(define-values (width height) (send orig-dc get-size))
(send orig-dc draw-bitmap (ray-trace width height (qi^-stage qi) (qi^-progress qi) (qi^-max-progress qi)) 0 0))
(define ((make-progress-updater @qi) a b c)
;; (printf "~a: ~a/~a ~a~n" (qi^-wikiname (obs-peek @qi)) a b c)
2023-12-15 08:23:12 +00:00
(update-qi @qi [progress a] [max-progress b] [ticks (add1 (qi^-ticks (obs-peek @qi)))]))
(define/obs @input "")
(define (do-add-to-queue)
(define wikiname (string-trim (obs-peek @input)))
(when ((string-length wikiname) . > . 0)
(add-wikiname-to-queue wikiname 'queued 0)) ;; TODO: automatically start?
(:= @input ""))
(define-syntax-rule (update-qi @qi args ...)
(let ([wikiname (qi^-wikiname (obs-peek @qi))])
(@queue . <~ . (λ (queue)
(for/list ([qi queue])
(if (equal? (qi^-wikiname qi) wikiname)
(struct-copy qi^ qi args ...)
qi))))))
(define (do-start-qi @qi)
(define th
(thread (λ ()
(with-handlers ([exn? (handle-graphical-exn @qi)])
(define last-stage
(for/last ([stage all-stages]
[i (in-naturals)])
(update-qi @qi [stage i])
(stage (qi^-wikiname (obs-peek @qi)) (make-progress-updater @qi))
i))
(update-qi @qi [st 'complete] [stage (add1 last-stage)])
(do-try-unpause-next-entry)))))
(update-qi @qi [st 'running] [th th]))
(define (do-stop-qi @qi)
(define th (qi^-th (obs-peek @qi)))
(when th (kill-thread th))
(update-qi @qi [th #f] [st 'paused]))
(define (do-reset-qi @qi)
2023-12-13 10:09:00 +00:00
(define reset-progress-to 0)
(define th (qi^-th (obs-peek @qi)))
(when th (kill-thread th))
2023-12-13 10:09:00 +00:00
(update-qi @qi [th #f] [st 'queued] [stage reset-progress-to] [progress 0] [max-progress 0])
(query-exec* "update wiki set progress = ? where wikiname = ?" reset-progress-to (qi^-wikiname (obs-peek @qi))))
2023-12-15 08:23:12 +00:00
(define (do-reseter-qi @qi)
(do-reset-qi @qi)
(query-exec* "delete from page where wikiname = ?" (qi^-wikiname (obs-peek @qi))))
(define (do-try-unpause-next-entry)
(define queue (obs-peek @queue))
2023-12-13 10:09:00 +00:00
(define next-qi (for/last ([qi queue]
#:when (memq (qi^-st qi) '(paused queued)))
qi))
(when next-qi
(define @qi (@queue . ~> . (λ (queue) (findf (λ (qi) (equal? (qi^-wikiname qi) (qi^-wikiname next-qi))) queue))))
(do-start-qi @qi)))
(define main-window
(render
(window
#:title "Fandom Archiver"
#:size '(400 300)
#:mixin (λ (%) (class %
(super-new)
(define/augment (on-close)
(for ([qi (obs-peek @queue)])
(when (qi^-th qi)
(kill-thread (qi^-th qi))))
#;(disconnect*))))
(vpanel
#:spacing 10
#:margin '(5 5)
(hpanel
#:stretch '(#t #f)
#:spacing 10
(hpanel
(text "https://")
(input @input
(λ (event data) (cond
[(eq? event 'input) (:= @input data)]
[(eq? event 'return) (do-add-to-queue)])))
(text ".fandom.com"))
(button "Download Wiki" do-add-to-queue))
(list-view
#:style '(vertical)
@queue
#:key qi^-wikiname
(λ (k @qi)
(define @status-icons
(@> (case (qi^-st @qi)
2023-12-15 08:23:12 +00:00
[(running) (stick (qi^-ticks @qi))]
[else (hash-ref status-icons (qi^-st @qi))])))
(define @is-running?
(@> (memq (qi^-st @qi) '(running))))
(define @is-complete?
(@> (eq? (qi^-st @qi) 'complete)))
;; state icon at the left side
(hpanel #:stretch '(#t #f)
#:alignment '(left center)
#:spacing 8
(bitmap-view @status-icons status-icon-min-width)
(vpanel
;; name and buttons (top half)
(hpanel #:alignment '(left bottom)
(text (@> (qi^-wikiname @qi)))
(spacer)
(hpanel
#:stretch '(#f #f)
2023-12-15 08:23:12 +00:00
(if-view @is-running?
(button (hash-ref action-icons 'pause)
(λ () (do-stop-qi @qi)))
2023-12-15 08:23:12 +00:00
(hpanel
#:stretch '(#f #f)
(button (hash-ref action-icons 'reseter)
(λ () (do-reseter-qi @qi)))
(button (hash-ref action-icons 'reset)
(λ () (do-reset-qi @qi)))
(button (hash-ref action-icons 'resume)
(λ () (do-start-qi @qi)))))))
;; progress bar (bottom half)
(hpanel
(canvas
@qi
#:style '(transparent)
#:margin '(3 3)
draw-bar)
(hpanel #:min-size '(68 #f)
#:stretch '(#f #f)
#:alignment '(right center)
(text (@> (format "eta ~a" (qi^-eta @qi))))))))))))))