-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathDYE.tcl
10849 lines (9231 loc) · 404 KB
/
DYE.tcl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#######################################################################################################################
### A Decent DE1app extension that provides shot logging (metadata description) of any shot in the history.
###
### Source code available in GitHub: https://github.com/ebengoechea/dye_de1app_dsx_plugin/
### This code is released under GPLv3 license. See LICENSE file under the DE1 source folder in github.
###
### By Enrique Bengoechea <[email protected]>
### (with lots of copy/paste/tweak from Damian, John and Johanna's code!)
########################################################################################################################
#set ::skindebug 1
#plugins enable SDB
#plugins enable DYE
#fconfigure $::logging::_log_fh -buffering line
#dui config debug_buttons 1
package require http
package require tls
package require json
# zint may not be available in some standard Tcl/Tk distributions, for example on MacOS.
try {
package require zint
} on error err {
msg -WARNING "::plugins::DYE can't generate QR codes: $err"
}
namespace eval ::plugins::DYE {
variable author "Enrique Bengoechea"
variable contact "[email protected]"
variable version 2.50
variable github_repo ebengoechea/de1app_plugin_DYE
variable name [translate "Describe Your Espresso"]
variable description [translate "Describe any shot from your history and plan the next one: beans, grinder, extraction parameters and people. Also includes beans-based workflow, shot history management, and profile tools."]
# Dependece: 1.42.1.102 should contain SDB update to support favorites & workflow,
# and DUI update to support correct z-order in dui::page::add_items.
variable min_de1app_version {1.43.1}
variable min_DSx_version {4.79}
variable debug_text {}
# Store widgets used in the skin-specific GUI integration
variable widgets
array set widgets {}
variable desc_text_fields {bean_brand bean_type roast_date roast_level bean_notes grinder_model grinder_setting \
espresso_notes my_name drinker_name skin repository_links}
variable desc_numeric_fields {grinder_dose_weight drink_weight drink_tds drink_ey espresso_enjoyment}
variable propagated_fields {bean_brand bean_type roast_date roast_level bean_notes grinder_model grinder_setting \
my_name drinker_name}
# About Steam settings:
# - Milk jug? Stored on DSx2-only variables atm
# - Flow and temp? Not exposed by DSx2 Steam workflow
# Note that steam_disabled is *always* imported and flush settings are *never* imported.
variable workflow_settings_vars
array set workflow_settings_vars {
espresso {steam_disabled flush_flow flush_seconds}
latte {steam_timeout steam_disabled flush_flow flush_seconds}
long {hotwater_flow water_temperature water_volume steam_disabled flush_flow flush_seconds}
americano {hotwater_flow water_temperature water_volume steam_disabled flush_flow flush_seconds}
none {steam_timeout hotwater_flow water_temperature water_volume steam_disabled flush_flow flush_seconds}
}
variable profile_shot_extra_vars {profile profile_filename profile_to_save original_profile_title}
# Shot summary description strings appearing in:
# All skins:
# - On DYE-launch buttons popup-menu (3 lines, Last & Next, full description beans/profile/ratio/grinder)
# To add? Source shot (when copying from it to Next, same parameters)
# Computed when loading the menu page
# DSx:
# - Home page (3 lines, Last & Next, include TDS, EY & rating, but not ratio as it already appears on DSx home),
# kept on DYE $settings(last_shot_desc) & $settings(next_shot_desc)
# - History viewer (1 line & 2 lines, Past and Past 2, as selected by user, XX) - stored on DYE vars past_shot_desc*
# DSx2: Home pages (3 lines, Next & Last or Copy Source, workflow?/beans/profile/ratio/grinder/extraction??)
variable default_shot_desc_font_color {#206ad4}
}
### PLUGIN WORKFLOW ###################################################################################################
# Startup the Describe Your Espresso plugin.
proc ::plugins::DYE::main {} {
variable settings
variable ::plugins::DYE::shots::src_shot
msg "Starting the 'Describe Your Espresso' plugin"
check_versions
# Load skin-specific integration code
regsub -all { } $::settings(skin) "_" skin
set skin_src_fn "[plugin_directory]/DYE/setup_${skin}.tcl"
if { [file exists $skin_src_fn] } {
source $skin_src_fn
}
if { $skin eq "Insight_Dark" } {
source "[plugin_directory]/DYE/setup_Insight.tcl"
} elseif { $skin eq "Streamline_Dark" } {
source "[plugin_directory]/DYE/setup_Streamline.tcl"
}
if { [namespace which -command "::plugins::DYE::setup_ui_$skin"] ne "" } {
::plugins::DYE::setup_ui_$skin
}
if { $skin eq "Insight_Dark" } {
::plugins::DYE::setup_ui_Insight
} elseif { $skin eq "Streamline_Dark" } {
::plugins::DYE::setup_ui_Streamline
}
# Buttons in the "default" skin (profile setting pages)
if { [string is true $settings(button_in_settings_presets)] } {
set widgets(launch_dye_profile_selector) [dui add dbutton settings_1 1140 1085 -bwidth 130 -bheight 120 -shape round -radius 30 \
-tags launch_dye_ps -fill "#c1c5e4" -symbol arrow-right-arrow-left -symbol_pos {0.5 0.4} -symbol_fill white \
-tap_pad {20 40 40 80} -label [translate {DYE PS}] -label_font_size 12 -label_pos {0.5 0.8} -label_anchor center -label_justify center \
-label_fill "#8991cc" -label_font_family notosansuibold -label_width 130 -command [list [namespace current]::open_profile_tools select]]
}
if { [string is true $settings(button_in_settings_preview)] } {
set widgets(launch_dye_pv_basic) [dui add dbutton {settings_2a settings_2b} 915 1460 -bwidth 130 -bheight 120 \
-shape round -radius 30 -fill "#c1c5e4" -tags launch_dye_pv_basic \
-symbol signature -symbol_pos {0.5 0.4} -symbol_fill white -label [translate {DYE PV}] -label_font_size 12 \
-tap_pad {40 40 20 40} -label_pos {0.5 0.8} -label_anchor center -label_justify center -label_fill "#8991cc" -label_width 130 \
-label_font_family notosansuibold -command [list [namespace current]::open_profile_tools viewer]]
set widgets(launch_dye_pv_advanced) [dui add dbutton settings_2c 768 542 -bwidth 130 -bheight 120 -shape round \
-radius 30 -fill "#c1c5e4" -tags launch_dye_pv_advanced \
-symbol signature -symbol_pos {0.5 0.4} -symbol_fill white -label [translate {DYE PV}] -label_font_size 12 \
-tap_pad {40 40 30 60} -label_pos {0.5 0.8} -label_anchor center -label_justify center -label_fill "#8991cc" -label_width 130 \
-label_font_family notosansuibold -command [list [namespace current]::open_profile_tools viewer]]
}
# Declare DYE pages
foreach page {DYE DYE_fsh} {
dui page add $page -namespace true -type fpdialog
}
# Default slice/button height in menu dialogs: 120
dui page add dye_edit_dlg -namespace true -type dialog -bbox {0 0 1150 960}
dui page add dye_manage_dlg -namespace true -type dialog -bbox {0 0 800 720}
dui page add dye_visualizer_dlg -namespace true -type dialog -bbox {0 0 900 1160}
dui page add dye_which_shot_dlg -namespace true -type dialog -bbox {0 0 1100 820}
dui page add dye_profile_viewer_dlg -namespace true -type dialog -bbox {100 160 2460 1550}
dui page add dye_profile_select_dlg -namespace true -type dialog -bbox {100 160 2460 1550}
dui page add dye_shot_select_dlg -namespace true -type dialog -bbox {100 160 2460 1550}
dui page add dye_item_select_dlg -namespace true -type dialog -bbox {0 0 900 1600} \
-bg_shape rect -width 2 -outline [dui aspect get dtext fill]
# foreach page $::dui::pages::DYE_v3::pages {
# dui page add $page -namespace ::dui::pages::DYE_v3 -type fpdialog
# }
# Update/propagate the describe settings when the a shot is started
trace add execution ::reset_gui_starting_espresso leave ::plugins::DYE::reset_gui_starting_espresso_leave_hook
# Ensure the description summary is updated whenever last shot is saved to history.
# We don't use 'register_state_change_handler' as that would not update the shot file if its metadata is
# changed in the Godshots page in Insight or DSx (though currently that does not work)
#register_state_change_handler Espresso Idle ::plugins::SDB::save_espresso_to_history_hook
if { [plugins enabled visualizer_upload] } {
plugins load visualizer_upload
trace add execution ::plugins::visualizer_upload::uploadShotData leave \
::plugins::DYE::save_espresso_to_history_hook
} else {
trace add execution ::plugins::SDB::save_espresso_to_history_hook leave \
::plugins::DYE::save_espresso_to_history_hook
}
# Ensure DYE Favorites and next shot descriptions are updated when profile is modified
trace add execution ::select_profile enter ::plugins::DYE::select_profile_enter_hook
# Screensaver icon
dui page add_action saver show ::plugins::DYE::saver_page_onshow
# Initialize favorites
favorites::update_recent
# Initialize source shot and shot summaries
if { $settings(next_src_clock) > 0 } {
array set src_shot [::plugins::SDB::load_shot $settings(next_src_clock) 1 1 1 1]
# Note this can't be on DSx2 setup because that is run before this code, and so it
# doesn't have shots::src_shot available. We put it here which is not as nice, but
# avoids loading the source shot twice.
if { [is_DSx2] && \
$settings(next_src_clock) != [value_or_default ::settings(espresso_clock) 0] && \
[string is true $settings(dsx2_update_chart_on_copy)] && \
[string is true $settings(dsx2_show_shot_desc_on_home)] } {
# Called proc already defines the source shot desc
pages::dsx2_dye_home::load_home_graph_from {} src_shot
} else {
shots::define_last_desc src_shot
}
} else {
shots::define_last_desc
}
shots::define_next_desc
# App window name when not on Android
if { [ifexists ::debugging 0] == 1 && $::android != 1 } {
ifexists ::debugging_window_title "Decent"
wm title . "$::debugging_window_title DYE v$::plugins::DYE::version"
}
}
# Paint settings screen
proc ::plugins::DYE::preload {} {
if { [plugins available SDB] } {
plugins preload SDB
}
package require de1_logging 1.0
package require de1_dui 1.0
# Because DUI calls the page setup commands automatically we need to initialize stuff here
dui add image_dirs "[homedir]/[plugin_directory]/DYE/"
check_settings
plugins save_settings DYE
setup_default_aspects
dui page add DYE_settings -namespace true -theme default -type fpdialog
dui page add DYE_settings2 -namespace true -theme default -type fpdialog
return DYE_settings
}
proc ::plugins::DYE::open { args } {
variable settings
if { [llength $args] == 1 } {
set use_dye_v3 0
set which_shot [lindex $args 0]
set args {}
} elseif { [llength $args] > 1 } {
if { [string range [lindex $args 0] 0 0] ne "-" } {
set which_shot [lindex $args 0]
set args [lrange $args 1 end]
} else {
set which_shot [dui::args::get_option -which_shot "default" 1]
}
set use_dye_v3 [string is true [dui::args::get_option -use_dye_v3 [value_or_default ::plugins::DYE::settings(use_dye_v3) 0] 1]]
}
if { $which_shot eq {} || $which_shot eq "default" } {
set which_shot $settings(default_launch_action)
}
set dlg_coords [dui::args::get_option -coords {2400 975} 1]
set dlg_anchor [dui::args::get_option -anchor "e" 1]
if { $use_dye_v3 } {
dui page load DYE_v3 -which_shot $which_shot {*}$args
} elseif { $which_shot eq "dialog" } {
dui page open_dialog dye_which_shot_dlg -coords $dlg_coords -anchor $dlg_anchor {*}$args
} else {
dui page load DYE $which_shot {*}$args
}
}
proc ::plugins::DYE::open_profile_tools { args } {
variable settings
if { [llength $args] > 0 && [lindex $args 0] eq "viewer" } {
dui page open_dialog dye_profile_viewer_dlg "next" ""
} else {
dui page open_dialog dye_profile_select_dlg \
-selected [value_or_default ::settings(profile_filename)] -change_settings_on_exit 1 \
-bean_brand $::settings(bean_brand) -bean_type $::settings(bean_type) \
-grinder_model $::settings(grinder_model)
}
}
proc ::plugins::DYE::msg { {flag ""} args } {
if { [string range $flag 0 0] eq "-" && [llength $args] > 0 } {
::logging::default_logger $flag "::plugins::DYE" {*}$args
} else {
::logging::default_logger "::plugins::DYE" $flag {*}$args
}
}
proc ::plugins::DYE::is_DSx2 { {strict 0} {theme {}} } {
if {[string is true $strict]} {
set isDSx2 [expr {$::settings(skin) eq "DSx2"}]
} else {
# Handle DSx2 forks, must be named "DSx2<something>"
set isDSx2 [expr {[string range $::settings(skin) 0 3] eq "DSx2"}]
}
# Verify that a couple of variable that only DSx2 creates exist. This avoids the situation
# where a user first changes the skin in the app settings (which changes $::settings(skin))
# and then enables DYE before exiting the app (which would run setup_ui_DSx2, and raise
# a runtime error for some unexisting DSx2-only variable).
set isDSx2 [expr {$isDSx2 && [info exists ::skin(theme)] && \
[info exists ::skin_background_colour]}]
if {$isDSx2 && $theme ne {} } {
set isDSx2 [expr {[string tolower [string trim $::skin(theme)]] \
eq [string tolower [string trim $theme]]}]
}
return $isDSx2
}
# Verify the minimum required versions of DE1 app & skin are used, and that required plugins are availabe and installed,
# otherwise prevents startup.
proc ::plugins::DYE::check_versions {} {
msg -INFO "DE1APP VERSION [package version de1app]"
if { [package vcompare [package version de1app] $::plugins::DYE::min_de1app_version] < 0 } {
message_page "[translate {Plugin 'Describe Your Espreso'}] v$::plugins::DYE::plugin_version [translate requires] \
DE1app v$::plugins::DYE::min_de1app_version [translate {or higher}]\r\r[translate {Current DE1app version is}] [package version de1app]" \
[translate Ok]
}
regsub -all { } $::settings(skin) "_" skin
if { $skin ni {Insight Insight_Dark DSx MimojaCafe DSx2 Streamline Streamline_Dark} } {
#plugins disable DYE
msg -WARN [translate "The 'Describe Your Espresso' (DYE) plugin does not yet work with your skin. Please reach out to your skin author"]
#return
}
if { [info exists ::plugins::DYE::min_${skin}_version ] } {
# TODO: Make a proc that properly returns the skin version??
if { $skin eq "DSx" } {
if { [package vcompare $::DSx_settings(version) [subst \$::plugins::DYE::min_$::settings(skin)_version]] < 0 } {
message_page "[translate {Plugin 'Describe Your Espreso'}] v$::plugins::DYE::plugin_version [translate requires]\
$::settings(skin) skin v[subst \$::plugins::DYE::min_$::settings(skin)_version] [translate {or higher}]\r\r[translate {Current $::settings(sking) version is}] $::DSx_settings(version)" \
[translate Ok]
}
}
}
# Check plugin dependencies, and ensure they're loaded in the correct order.
set depends_msg ""
if { [plugins available SDB] } {
plugins load SDB
} else {
append depends_msg "\n[translate {Please install 'Shot DataBase' plugin for 'Describe Your Espresso' to work}]"
}
if { $depends_msg ne "" } {
# Throw an error that is catched by the plugins system and the plugin is disabled
error $depends_msg
}
}
# Ensure all settings values are defined, otherwise set them to their default values.
proc ::plugins::DYE::check_settings {} {
variable settings
if { ![info exists settings(version)] || [package vcompare $settings(version) $::plugins::DYE::version] < 0 } {
upgrade [value_or_default settings(version) ""]
}
set settings(version) $::plugins::DYE::version
ifexists settings(calc_ey_from_tds) on
ifexists settings(show_shot_desc_on_home) 1
ifexists settings(shot_desc_font_color) $::plugins::DYE::default_shot_desc_font_color
ifexists settings(describe_from_sleep) 1
ifexists settings(date_format) "%d/%m/%Y"
ifexists settings(describe_icon) [dui symbol get mug]
ifexists settings(propagate_previous_shot_desc) 1
ifexists settings(reset_next_plan) 0
ifexists settings(backup_modified_shot_files) 0
ifexists settings(use_stars_to_rate_enjoyment) 1
if { [info exists ::DSx_settings(next_shot_DSx_home_coords)] } {
set settings(next_shot_DSx_home_coords) $::DSx_settings(next_shot_DSx_home_coords)
} else {
ifexists settings(next_shot_DSx_home_coords) {500 1165}
}
if { [info exists ::DSx_settings(last_shot_DSx_home_coords)] } {
set settings(last_shot_DSx_home_coords) $::DSx_settings(last_shot_DSx_home_coords)
} else {
ifexists settings(last_shot_DSx_home_coords) {2120 1165}
}
ifexists settings(github_latest_url) "https://api.github.com/repos/ebengoechea/de1app_plugin_DYE/releases/latest"
set settings(use_dye_v3) 0
ifexists settings(relative_dates) 1
if { ![info exists settings(date_input_format)] } {
set settings(date_input_format) "MDY"
if { [info exists settings(date_input_formats)] } {
set fmt [lindex $settings(date_input_formats) 0]
if { $fmt ne "%D" } {
set year_pos [string first "%y" [string tolower $fmt]]
set month_pos [string first "%m" $fmt]
if { $month_pos == -1 } {
set month_pos [string first "%b" [string tolower $fmt]]
}
set day_pos [string first "%d" $fmt]
if { $year_pos > -1 && $month_pos > -1 && $year_pos < $month_pos } {
set settings(date_input_format) "YMD"
} elseif { $month_pos > -1 && $day_pos > -1 && $day_pos < $month_pos } {
set settings(date_input_format) "DMY"
}
}
unset -nocomplain settings(date_input_formats)
}
}
ifexists settings(roast_date_format) ""
ifexists settings(date_output_format) "%b %d %Y"
ifexists settings(time_output_format) "%H:%M"
ifexists settings(time_output_format_ampm) "%I:%M %p"
ifexists settings(default_launch_action) last
ifexists settings(button_in_settings_presets) 1
ifexists settings(button_in_settings_preview) 1
ifexists settings(apply_action_to_beans) 1
ifexists settings(apply_action_to_equipment) 1
ifexists settings(apply_action_to_ratio) 1
ifexists settings(apply_action_to_extraction) 0
ifexists settings(apply_action_to_note) 0
ifexists settings(apply_action_to_people) 1
ifexists settings(apply_action_to_profile) 0
# Don't use ifexists for this, as it always evaluates the default value, inducing it to be changed
if { ![info exists settings(last_shot_desc)] } {
shots::define_last_desc
}
if { ![info exists settings(next_shot_desc)] } {
shots::define_next_desc
}
# Propagation mechanism
# drink_weight and espresso_notes are special as they're not propagated (from last to next), but can be defined in next.
ifexists settings(next_modified) 0
set propagated_fields [metadata fields -domain shot -category description -propagate 1]
foreach field_name [concat $propagated_fields espresso_notes drink_weight] {
if { ! [info exists settings(next_$field_name)] } {
set settings(next_$field_name) {}
}
}
if { $settings(next_modified) == 0 } {
if { $settings(propagate_previous_shot_desc) == 1 } {
foreach field_name $propagated_fields {
if { [info exists ::settings($field_name)] } {
set settings(next_$field_name) $::settings($field_name)
} else {
set settings(next_$field_name) {}
}
}
set settings(next_espresso_notes) {}
} else {
foreach field_name [concat $propagated_fields next_espresso_notes] {
set settings(next_$field_name) {}
}
}
}
ifexists settings(summary_fields) {bean_brand bean_type roast_date "" grinder_setting "" espresso_notes "" espresso_enjoyment}
ifexists settings(next_summary_fields) {grinder_dose_weight drink_weight "" bean_brand bean_type roast_date "" grinder_setting espresso_notes}
# Ensure load_DSx_past_shot and load_DSx_past2_shot in DSx includes exactly all fields we need when they load the
# shots.
if { $::settings(skin) eq "DSx" } {
# clock drink_weight grinder_dose_weight - already included
set ::DSx_settings(extra_past_shot_fields) {bean_brand bean_type roast_date \
roast_level bean_notes grinder_model grinder_setting drink_tds drink_ey espresso_enjoyment \
espresso_notes my_name drinker_name scentone skin beverage_type final_desired_shot_weight repository_links}
}
if {[info exists settings(favorites)] == 0} {
set settings(favorites) [list]
}
if {[llength $settings(favorites)] < [favorites::max_number]} {
#set empty_fav [list "n_recent" "" [list]]
for {set i [llength $settings(favorites)]} {$i < [favorites::max_number]} {incr i 1} {
favorites::set_fav $i "n_recent"
}
}
ifexists settings(favs_n_recent_grouping) {beans profile_title}
ifexists settings(favs_n_recent_what_to_copy) {workflow profile_title beans roast_date grinder grinder_dose_weight drink_weight}
ifexists settings(selected_n_fav) -1
ifexists settings(dsx2_show_shot_desc_on_home) 1
ifexists settings(dsx2_use_dye_favs) 0
ifexists settings(dsx2_n_visible_dye_favs) 4
ifexists settings(dsx2_update_chart_on_copy) 1
ifexists settings(next_src_clock) 0
if { [file exists [::plugins::DYE::grinders::specs_file]] } {
::plugins::DYE::grinders::load_specs
} else {
::plugins::DYE::grinders::infer_spec
::plugins::DYE::grinders::save_specs
}
ifexists settings(grinder_select_load_last_setting) 1
ifexists settings(beans_select_copy_to_next) 0
}
proc ::plugins::DYE::upgrade { previous_version } {
variable settings
variable version
msg -INFO "plugin upgraded from v$previous_version to v$version"
if { $previous_version eq "" } {
set old_settings_file "[homedir]/skins/DSx/DSx_User_Set/DYE_settings.tdb"
if { [file exists $old_settings_file] } {
set settings_file_contents [encoding convertfrom utf-8 [read_binary_file $old_settings_file]]
if {[string length $settings_file_contents] != 0} {
array set old_settings $settings_file_contents
foreach s {calc_ey_from_tds show_shot_desc_on_home shot_desc_font_color describe_from_sleep date_format
describe_icon propagate_previous_shot_desc backup_modified_shot_files use_stars_to_rate_enjoyment
next_shot_DSx_home_coords last_shot_DSx_home_coords github_latest_url next_modified next_espresso_notes
next_bean_brand next_bean_type next_roast_date next_roast_level next_bean_notes next_grinder_model
next_grinder_setting next_my_name next_drinker_name} {
if { [info exists old_settings($s)] } {
set settings($s) $old_settings($s)
}
}
msg -INFO "settings copied from old DSx DYE plugin"
}
}
if { [file exists "[homedir]/skins/DSx/DSx_Plugins/describe_your_espresso.dsx"] } {
file rename -force "[homedir]/skins/DSx/DSx_Plugins/describe_your_espresso.dsx" \
"[homedir]/skins/DSx/DSx_Plugins/describe_your_espresso.off"
msg -INFO "describe_your_espresso.dsx has been disabled"
}
}
}
# Defines the DYE-specific aspect styles for the default theme. These are always needed even if the current theme used is
# another one, to have a default and to build the settings page with the default theme.
proc ::plugins::DYE::setup_default_aspects { args } {
set theme default
dui aspect set -theme $theme -style dsx_settings {dbutton.shape round dbutton.bwidth 384 dbutton.bheight 192
dbutton_symbol.pos {0.2 0.5} dbutton_symbol.font_size 37
dbutton_label.pos {0.65 0.5} dbutton_label.font_size 18
dbutton_label1.pos {0.65 0.8} dbutton_label1.font_size 16}
dui aspect set -theme $theme -style dsx_midsize {dbutton.shape round dbutton.bwidth 220 dbutton.bheight 140
dbutton_label.pos {0.5 0.5} dbutton_symbol.font_size 30}
set bold_font [dui aspect get dtext font_family -theme default -style bold]
dui aspect set -theme $theme -style dsx_done [list dbutton.shape round dbutton.bwidth 220 dbutton.bheight 140 \
dbutton_label.pos {0.5 0.5} dbutton_label.font_size 20 dbutton_label.font_family $bold_font]
dui aspect set -theme $theme -style dye_main_nav_button { dbutton.shape {} dbutton.fill {} dbutton.disabledfill {}
dbutton_symbol.font_size 28 dbutton_symbol.fill "#35363d" dbutton_symbol.disabledfill "#ccc"}
dui aspect set -theme $theme -type dtext -style section_header [list font_family $bold_font font_size 20]
dui aspect set -theme $theme -type dclicker -style dye_double {orient horizontal use_biginc 1 symbol chevrons-left
symbol1 chevron-left symbol2 chevron-right symbol3 chevrons-right }
dui aspect set -theme $theme -type dclicker_symbol -style dye_double {pos {0.075 0.5} font_size 24 anchor center fill "#7f879a"}
dui aspect set -theme $theme -type dclicker_symbol1 -style dye_double {pos {0.275 0.5} font_size 24 anchor center fill "#7f879a"}
dui aspect set -theme $theme -type dclicker_symbol2 -style dye_double {pos {0.725 0.5} font_size 24 anchor center fill "#7f879a"}
dui aspect set -theme $theme -type dclicker_symbol3 -style dye_double {pos {0.925 0.5} font_size 24 anchor center fill "#7f879a"}
dui aspect set -theme $theme -type dclicker -style dye_single {orient horizontal use_biginc 0 symbol chevron-left symbol1 chevron-right}
dui aspect set -theme $theme -type dclicker_symbol -style dye_single {pos {0.1 0.5} font_size 24 anchor center fill "#7f879a"}
dui aspect set -theme $theme -type dclicker_symbol1 -style dye_single {pos {0.9 0.5} font_size 24 anchor center fill "#7f879a"}
# Profile viewer
dui aspect set -theme $theme [subst {
shape.fill.dye_pv_icon_btn CadetBlue2
dtext.fill.dye_pv_profile_title black
dtext.font_size.dye_pv_profile_title +8
dtext.font_family.dye_pv_profile_title notosansuibold
text_tag.spacing1.dye_pv_step [dui::platform::rescale_y 20]
text_tag.foreground.dye_pv_step brown
text_tag.lmargin1.dye_pv_step_line [dui::platform::rescale_x 35]
text_tag.lmargin2.dye_pv_step_line [dui::platform::rescale_x 55]
text_tag.foreground.dye_pv_value blue4
}]
# DYE v3
set bg_color [dui aspect get page bg_color -theme default]
set btn_spacing 100
set half_button_width [expr {int(($::dui::pages::DYE_v3::page_coords(panel_width)-$btn_spacing)/2)}]
set half_button_width 200
dui aspect set -theme default [subst {
dbutton.bheight.dyev3_topnav 90
dbutton.shape.dyev3_topnav rect
dbutton_label.font_size.dyev3_topnav -1
dbutton_label.pos.dyev3_topnav {0.5 0.5}
dbutton_label.anchor.dyev3_topnav center
dbutton_label.justify.dyev3_topnav center
dbutton.bwidth.dyev3_nav_button 100
dbutton.bheight.dyev3_nav_button 120
dbutton.fill.dyev3_nav_button {}
dbutton.disabledfill.dyev3_nav_button {}
dbutton_symbol.pos.dyev3_nav_button {0.5 0.5}
dbutton_symbol.fill.dyev3_nav_button grey
dbutton_symbol.disabledfill.dyev3_nav_button #ccc
text.font_size.dyev3_top_panel_text -1
text.yscrollbar.dyev3_top_panel_text no
text.bg.dyev3_top_panel_text $bg_color
text.borderwidth.dyev3_top_panel_text 0
text.highlightthickness.dyev3_top_panel_text 0
text.relief.dyev3_top_panel_text flat
text.font_size.dyev3_bottom_panel_text -1
dtext.font_family.dyev3_right_panel_title notosansuibold
dtext.font_size.dyev3_right_panel_title +2
dtext.fill.dyev3_right_panel_title black
dtext.anchor.dyev3_right_panel_title center
dtext.justify.dyev3_right_panel_title center
graph.background.dyev3_text_graph white
graph.plotbackground.dyev3_text_graph white
graph.borderwidth.dyev3_text_graph 1
graph.plotrelief.dyev3_text_graph flat
dtext.font_size.dyev3_chart_stage_title +2
dtext.anchor.dyev3_chart_stage_title center
dtext.justify.dyev3_chart_stage_title center
dtext.fill.dyev3_chart_stage_title black
dtext.anchor.dyev3_chart_stage_colheader center
dtext.justify.dyev3_chart_stage_colheader center
dtext.anchor.dyev3_chart_stage_value center
dtext.justify.dyev3_chart_stage_value center
dtext.anchor.dyev3_chart_stage_comp center
dtext.justify.dyev3_chart_stage_comp center
dtext.font_size.dyev3_chart_stage_comp -4
dtext.fill.dyev3_chart_stage_comp white
line.fill.dyev3_chart_stage_line_sep grey
dbutton.shape.dyev3_action_half round
dbutton.bwidth.dyev3_action_half $half_button_width
dbutton.bheight.dyev3_action_half 125
dbutton_symbol.pos.dyev3_action_half {0.2 0.5}
dbutton_label.pos.dyev3_action_half {0.6 0.5}
dbutton_label.width.dyev3_action_half [expr {$half_button_width-75}]
#text_tag.foregroud.which_shot black
text_tag.font.dyev3_which_shot "[dui font get notosansuibold 15]"
text_tag.justify.dyev3_which_shot center
text_tag.justify.dyev3_profile_title center
text_tag.foreground.dyev3_section black
text_tag.font.dyev3_section "[dui font get notosansuibold 17]"
text_tag.spacing1.dyev3_section [dui platform rescale_y 20]
text_tag.foreground.dyev3_field "#7f879a"
text_tag.lmargin1.dyev3_field [dui platform rescale_x 35]
text_tag.lmargin2.dyev3_field [dui platform rescale_x 45]
text_tag.foreground.dyev3_value blue
text_tag.foreground.dyev3_measure_unit blue
text_tag.foreground.dyev3_compare grey
text_tag.font.dyev3_field_highlighted "[dui font get notosansuibold 15]"
text_tag.background.dyev3_field_highlighted pink
text_tag.font.dyev3_field_nonhighlighted "[dui font get notosansuiregular 15]"
text_tag.background.dyev3_field_nonhighlighted {}
}]
}
# Reset the "next" description and update the current/last shot summary description
proc ::plugins::DYE::reset_gui_starting_espresso_leave_hook { args } {
variable settings
variable ::plugins::DYE::shots::src_shot
set skin $::settings(skin)
set isDSx2 [is_DSx2]
# If the target dose or yield have been defined in the skin, ensure they are synchronized to next shot dose
if { $skin eq "DSx" } {
if { [info exists ::DSx_settings(bean_weight)] && $::DSx_settings(bean_weight) > 0 } {
set settings(next_grinder_dose_weight) [round_to_one_digits $::DSx_settings(bean_weight)]
}
if { [info exists ::DSx_settings(saw)] && $::DSx_settings(bean_weight) > 0 } {
set settings(next_drink_weight) [round_to_one_digits $::DSx_settings(saw)]
}
} elseif { $skin eq "MimojaCafe" } {
if { [return_zero_if_blank $::settings(grinder_dose_weight)] > 0 && $settings(next_grinder_dose_weight) != $::settings(grinder_dose_weight) } {
set settings(next_grinder_dose_weight) [round_to_one_digits $::settings(grinder_dose_weight)]
}
if { [::device::scale::expecting_present] } {
if {$::settings(settings_profile_type) eq "settings_2c"} {
if { [return_zero_if_blank $::settings(final_desired_shot_weight_advanced)] > 0 &&
$settings(next_drink_weight) != $::settings(final_desired_shot_weight_advanced) } {
set settings(next_drink_weight) [round_to_one_digits $::settings(final_desired_shot_weight_advanced)]
}
} else {
if { [return_zero_if_blank $::settings(final_desired_shot_weight)] > 0 && \
$settings(next_drink_weight) != $::settings(final_desired_shot_weight) } {
set settings(next_drink_weight) [round_to_one_digits $::settings(final_desired_shot_weight)]
}
}
}
if { [return_zero_if_blank $::settings(grinder_setting)] > 0 && $settings(next_grinder_setting) ne $::settings(grinder_setting) } {
set settings(next_grinder_setting) $::settings(grinder_setting)
}
} elseif { $isDSx2 } {
if { [return_zero_if_blank $::settings(grinder_dose_weight)] > 0 && $settings(next_grinder_dose_weight) != $::settings(grinder_dose_weight) } {
set settings(next_grinder_dose_weight) [round_to_one_digits $::settings(grinder_dose_weight)]
}
if {$::settings(settings_profile_type) eq "settings_2c"} {
if { [return_zero_if_blank $::settings(final_desired_shot_weight_advanced)] > 0 &&
$settings(next_drink_weight) != $::settings(final_desired_shot_weight_advanced) } {
set settings(next_drink_weight) [round_to_one_digits $::settings(final_desired_shot_weight_advanced)]
}
} else {
if { [return_zero_if_blank $::settings(final_desired_shot_weight)] > 0 && \
$settings(next_drink_weight) != $::settings(final_desired_shot_weight) } {
set settings(next_drink_weight) [round_to_one_digits $::settings(final_desired_shot_weight)]
}
}
}
set reset_next [expr { !$settings(propagate_previous_shot_desc) && $settings(reset_next_plan) }]
foreach field [concat [metadata fields -domain shot -category description -propagate 1] espresso_notes] {
set type [metadata get $field data_type]
if { ($type eq "number" || $field eq "grinder_setting") && $settings(next_$field) eq "" } {
set ::settings($field) 0
} else {
set ::settings($field) $settings(next_$field)
if { $reset_next } {
set settings(next_$field) {}
}
}
}
# if { $skin eq "DSx" } {
# if { [info exists ::DSx_settings(live_graph_beans)] && $::DSx_settings(live_graph_beans) > 0 } {
# set ::settings(grinder_dose_weight) $::DSx_settings(live_graph_beans)
# } elseif { [info exists ::DSx_settings(bean_weight)] && $::DSx_settings(bean_weight) > 0 } {
# set ::settings(grinder_dose_weight) [round_to_one_digits [return_zero_if_blank $::DSx_settings(bean_weight)]]
# } else {
# set ::settings(grinder_dose_weight) 0
# }
# }
set settings(next_espresso_notes) {}
set settings(next_modified) 0
#set settings(next_src_clock) $::settings(espresso_clock)
# if { $::undroid == 1 } {
# if { $skin eq "DSx" && [info exists ::DSx_settings(saw)] && $::DSx_settings(saw) > 0 } {
# set ::settings(drink_weight) [round_to_one_digits $::DSx_settings(saw)]
# } elseif { [info exists ::settings(final_desired_shot_weight)] && $::settings(final_desired_shot_weight) > 0 } {
# set ::settings(drink_weight) [round_to_one_digits $::settings(final_desired_shot_weight)]
# } elseif { $skin eq "MimojaCafe" && [info exists ::settings(final_desired_shot_volume_advanced)] &&
# $::settings(final_desired_shot_volume_advanced) > 0 } {
# set ::settings(drink_weight) [round_to_one_digits $::settings(final_desired_shot_volume_advanced)]
# } else {
# set ::settings(drink_weight) 0
# }
# } else {
# if { $skin eq "DSx" && [info exists ::DSx_settings(live_graph_weight)] && $::DSx_settings(live_graph_weight) > 0 } {
# set ::settings(drink_weight) [round_to_one_digits $::DSx_settings(live_graph_weight)]
# # Don't use de1(scale_sensor_weight)? If bluetooth scale disconnects then this is set to the previous shot weight
# } elseif { $::de1(scale_sensor_weight) > 0 } {
# set ::settings(drink_weight) [round_to_one_digits $::de1(scale_sensor_weight)]
# } elseif { $skin eq "DSx" && [info exists ::DSx_settings(saw)] && $::DSx_settings(saw) > 0 } {
# set ::settings(drink_weight) [round_to_one_digits $::DSx_settings(saw)]
# } elseif { [info exists ::settings(final_desired_shot_weight)] && $::settings(final_desired_shot_weight) > 0 } {
# set ::settings(drink_weight) [round_to_one_digits $::settings(final_desired_shot_weight)]
# } elseif { $skin eq "MimojaCafe" && [info exists ::settings(final_desired_shot_volume_advanced)] &&
# $::settings(final_desired_shot_volume_advanced) > 0 } {
# set ::settings(drink_weight) [round_to_one_digits $::settings(final_desired_shot_volume_advanced)]
# } else {
# set ::settings(drink_weight) 0
# }
# }
set settings(last_shot_header) [translate {ONGOING SHOT:}]
set settings(last_shot_desc) "\[ [translate {Please wait until saved}] \]"
shots::define_next_desc
# If on DSx2 with a source shot showing on the main graph, we need to point it again
# to last shot series
if { $isDSx2 && [string is true $settings(dsx2_update_chart_on_copy)] && \
$settings(next_src_clock) > 0 } {
::restore_live_graphs_default_vectors
}
# Settings already saved in reset_gui_starting_espresso, but as we have redefined them...
::save_settings
plugins save_settings DYE
}
# Hook executed after save_espresso_rating_to_history
proc ::plugins::DYE::save_espresso_to_history_hook { args } {
variable settings
variable ::plugins::DYE::shots::src_shot
array set src_shot [::plugins::DYE::shots::get_last]
if { [array size src_shot] > 0 } {
set settings(next_src_clock) $src_shot(clock)
::plugins::DYE::shots::define_last_desc src_shot yes
# Updating recent favorites already saves DYE settings
::plugins::DYE::favorites::update_recent
}
::plugins::DYE::favorites::select_from_clock $src_shot(clock)
}
proc ::plugins::DYE::select_profile_enter_hook { select_profile_args args } {
if { $::plugins::DYE::favorites::_is_loading } { return }
set new_profile [lindex $select_profile_args 1]
if { $new_profile ne [value_or_default ::settings(profile_filename)] } {
::plugins::DYE::favorites::clear_selected_if_needed profile_title
::plugins::DYE::shots::define_next_desc
}
}
proc ::plugins::DYE::saver_page_onshow { args } {
# Shows or hides the DYE button on the sleep screen.
# Note that -initial_state doesn't work correctly on the saver page,
# that's why we need to add this page action.
if { [dui page has_item saver saver_to_dye] } {
dui item show_or_hide [string is true $::plugins::DYE::settings(describe_from_sleep)] \
saver saver_to_dye*
}
}
proc ::plugins::DYE::return_blank_if_zero {in} {
if {$in == 0} { return {} }
return $in
}
proc ::plugins::DYE::singular_or_plural { value singular plural } {
set str "$value "
if { $value == 1 } {
append str [translate $singular]
} else {
append str [translate $plural]
}
return $str
}
# aclock must be in SECONDS
proc ::plugins::DYE::relative_date { aclock {ampm {}} } {
set reldate {}
set now [clock seconds]
set yesterday_threshold [clock scan [clock format $now -format {%Y%m%d 00:00:00}] -format {%Y%m%d %H:%M:%S}]
set 2days_threshold [clock scan [clock format [clock add $now -24 hours] -format {%Y%m%d 00:00:00}] -format {%Y%m%d %H:%M:%S}]
if { $ampm eq {} } {
set ampm [value_or_default ::settings(enable_ampm) 0]
}
if { [string is true $ampm] } {
set hourformat "%I:%M %p"
} else {
set hourformat "%H:%M"
}
if { $aclock >= $yesterday_threshold } {
set mindiff [expr {int(($now-$aclock)/60.0)}]
if { $mindiff < 60 } {
set reldate [singular_or_plural $mindiff {minute ago} {minutes ago}]
} else {
set hourdiff [expr {$mindiff/60}]
set reldate [singular_or_plural $hourdiff {hour} {hours}]
set mindiff [expr {$mindiff-($hourdiff*60)}]
if { $mindiff == 0 } {
append reldate " [translate {ago}]"
} else {
append reldate " [singular_or_plural $mindiff {minute ago} {minutes ago}]"
}
}
} elseif { $aclock >= $2days_threshold } {
set reldate "[translate {Yesterday at}] [clock format $aclock -format $hourformat]"
} else {
set daysdiff [expr {int(($yesterday_threshold-$aclock)/(60.0*60.0*24.0))+1}]
if { $daysdiff < 31 } {
set reldate "[singular_or_plural $daysdiff {day ago} {days ago}] [translate at] [clock format $aclock -format $hourformat]"
} else {
set reldate [clock format $aclock -format "%b %d %Y $hourformat"]
}
}
return $reldate
}
## Adapted from Damian's DSx last_shot_date.
#proc ::dui::pages::DYE::formatted_shot_date {} {
# variable data
# set shot_clock $data(clock)
# if { $shot_clock eq "" || $shot_clock <= 0 } {
# return ""
# }
#
# set date [clock format $shot_clock -format {%a %d %b}]
# if { [ifexists ::settings(enable_ampm) 0] == 0} {
# set a [clock format $shot_clock -format {%H}]
# set b [clock format $shot_clock -format {:%M}]
# set c $a
# } else {
# set a [clock format $shot_clock -format {%I}]
# set b [clock format $shot_clock -format {:%M}]
# set c $a
# regsub {^[0]} $c {\1} c
# }
# if { $::settings(enable_ampm) == 1 } {
# set pm [clock format $shot_clock -format %P]
# } else {
# set pm ""
# }
# return "$date $c$b$pm"
#}
proc ::plugins::DYE::format_date { aclock {relative {}} {ampm {}} {inc_time 1} } {
variable settings
if { $relative eq {} } {
set relative [value_or_default ::plugins::DYE::settings(relative_dates) 0]
}
if { $ampm eq {} } {
set ampm [value_or_default ::settings(enable_ampm) 0]
}
if { [string is true $relative] } {
return [relative_date $aclock $ampm]
} elseif { [string is true $inc_time] } {
if { [string is true $ampm] } {
set hourformat $settings(time_output_format_ampm)
} else {
set hourformat $settings(time_output_format)
}
return [clock format $aclock -format "$settings(date_output_format) $hourformat"]
} else {
return [clock format $aclock -format $settings(date_output_format)]
}
}
proc ::plugins::DYE::roast_date_format {} {
variable settings
set fmt $settings(date_input_format)
if { $settings(roast_date_format) ne "" } {
return $settings(roast_date_format)
} elseif { $fmt eq "DMY" } {
return "%d.%m.%Y"
} elseif { $fmt eq "YMD" } {
return "%Y.%m.%d"
} else {
return "%m.%d.%Y"
}
}
namespace eval ::plugins::DYE::ui {
# Adapted from skin_directory_graphics in utils.tcl
proc plugin_directory_graphics {} {
global screen_size_width
global screen_size_height
set plugindir "[plugin_directory]"
set dir "$plugindir/DYE/${screen_size_width}x${screen_size_height}"
if {[info exists ::rescale_images_x_ratio] == 1} {
set dir "$plugindir/DYE/2560x1600"
}
return $dir
}
proc page_skeleton { page {title {}} {titlevar {}} {done_button yes} {cancel_button yes} {buttons_loc right} \
{buttons_style dsx_done} } {
if { $title ne "" } {
dui add dtext $page 1280 60 -text $title -tags page_title -style page_title
} elseif { $titlevar ne "" } {
dui add variable $page 1280 60 -textvariable $titlevar -tags page_title -style page_title
}
set done_button [string is true $done_button]
set cancel_button [string is true $cancel_button]
set button_width [dui aspect get dbutton bwidth -style $buttons_style -default 220]
if { $buttons_loc eq "center" } {
if { $done_button && $cancel_button } {
set x_cancel [expr {1280-$button_width-75}]
set x_done [expr {1280+75}]
} elseif { $done_button } {
set x_done [expr {1280-$button_width/2}]
} elseif { $cancel_button } {
set x_cancel [expr {1280-$button_width/2}]
}