Quellcode durchsuchen

Save the current state of the recipe java program for reversion.

Sam Jaffe vor 5 Jahren
Commit
9cc085fc36

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*

+ 124 - 0
pom.xml

@@ -0,0 +1,124 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.leumasjaffe</groupId>
+  <artifactId>recipe</artifactId>
+  <version>0.0.1-SNAPSHOT</version>
+    <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.eclipse.m2e</groupId>
+          <artifactId>lifecycle-mapping</artifactId>
+          <version>1.0.0</version>
+          <configuration>
+            <lifecycleMappingMetadata>
+              <pluginExecutions>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>org.projectlombok</groupId>
+                    <artifactId>lombok-maven-plugin</artifactId>
+                    <versionRange>[1,)</versionRange>
+                    <goals>
+                      <goal>delombok</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore />
+                  </action>
+                </pluginExecution>
+              </pluginExecutions>
+            </lifecycleMappingMetadata>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <sourceDirectory>target/generated-sources/delombok</sourceDirectory>
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.5.1</version>
+        <configuration>
+          <compilerVersion>1.8</compilerVersion>
+          <source>1.8</source>
+          <target>1.8</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.projectlombok</groupId>
+        <artifactId>lombok-maven-plugin</artifactId>
+        <version>1.16.18.0</version>
+        <executions>
+          <execution>
+            <id>delombok</id>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>delombok</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <addOutputDirectory>false</addOutputDirectory>
+        </configuration>
+      </plugin>
+        <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>2.5.1</version>
+        <executions>
+          <execution>
+            <id>copy-dependencies</id>
+            <phase>package</phase>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>
+                ${project.build.directory}/dependency-jars/
+              </outputDirectory>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>org.leumasjaffe</groupId>
+      <artifactId>container</artifactId>
+      <version>0.2.1</version>
+    </dependency>
+    <dependency>
+      <groupId>org.leumasjaffe</groupId>
+      <artifactId>event</artifactId>
+      <version>0.1</version>
+    </dependency>
+    <dependency>
+      <groupId>org.swinglabs</groupId>
+      <artifactId>swingx</artifactId>
+      <version>1.6.1</version>
+    </dependency>
+    <dependency>
+      <groupId>org.projectlombok</groupId>
+      <artifactId>lombok</artifactId>
+      <version>1.16.8</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>2.7.3</version>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-all</artifactId>
+      <version>1.3</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>

+ 11 - 0
src/main/lombok/org/leumasjaffe/recipe/App.java

@@ -0,0 +1,11 @@
+package org.leumasjaffe.recipe;
+
+import org.leumasjaffe.recipe.view.RecipeFrame;
+
+public class App {
+
+	public static void main(String[] args) {
+		new RecipeFrame().setVisible(true);
+	}
+
+}

+ 28 - 0
src/main/lombok/org/leumasjaffe/recipe/model/CompoundRecipeComponent.java

@@ -0,0 +1,28 @@
+package org.leumasjaffe.recipe.model;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties({"duration", "ingredients", "components"})
+public interface CompoundRecipeComponent extends RecipeComponent {
+	Collection<RecipeComponent> getComponents();
+	
+	@Override
+	default Duration getDuration() {
+		return getComponents().stream().map(RecipeComponent::getDuration)
+				.reduce(Duration.ZERO, Duration::plus);
+	}
+	
+	@Override
+	default Collection<Ingredient> getIngredients() {
+		final Map<String, Ingredient> map = new HashMap<>();
+		getComponents().stream().flatMap(rc -> rc.getIngredients().stream()).forEach(i -> {
+			map.computeIfPresent(i.getName(), (k, v) -> v.plus(i));
+			map.computeIfAbsent(i.getName(), k -> i);
+		});
+		return map.values();
+	}
+}

+ 49 - 0
src/main/lombok/org/leumasjaffe/recipe/model/Duration.java

@@ -0,0 +1,49 @@
+package org.leumasjaffe.recipe.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor @NoArgsConstructor @Data
+public class Duration {
+	@AllArgsConstructor
+	enum Display {
+		SECONDS("s", 1), MINUTES("min", 60), HOURS("hr", 3600);
+		String abbreviation;
+		int inSeconds;
+	}
+
+	public static final Duration ZERO = new Duration();
+	
+	Display displayAs = Display.SECONDS;
+	boolean isApproximate = false;
+	int minSeconds = 0;
+	int maxSeconds = 0;
+	
+	public Duration plus(Duration rhs) {
+		final Display newDisplayAs = displayAs.ordinal() < rhs.displayAs.ordinal() ?
+				displayAs : rhs.displayAs;
+		return new Duration(newDisplayAs, isApproximate || rhs.isApproximate,
+				minSeconds + rhs.minSeconds, maxSeconds + rhs.maxSeconds);
+	}
+	
+	@Override
+	public String toString() {
+		StringBuilder build = new StringBuilder();
+		if (isApproximate) {
+			build.append("~ ");
+		}
+		if (minSeconds != 0) {
+			build.append(convert(minSeconds, displayAs));
+			build.append(" - ");
+		}
+		build.append(convert(maxSeconds, displayAs));
+		build.append(" ");
+		build.append(displayAs.abbreviation);
+		return build.toString();
+	}
+
+	private static int convert(int seconds, Display as) {
+		return seconds / as.inSeconds;
+	}
+}

+ 73 - 0
src/main/lombok/org/leumasjaffe/recipe/model/Ingredient.java

@@ -0,0 +1,73 @@
+package org.leumasjaffe.recipe.model;
+
+import org.leumasjaffe.container.Either;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+
+@Data @AllArgsConstructor
+public class Ingredient {
+	@AllArgsConstructor @Getter
+	public static enum Volume {
+		ml("ml", 1),
+		tsp("tsp", 5),
+		Tbsp("Tbsp", 15),
+		cup("cup", 240);
+
+		final String displayName;
+		final double atomicUnits;
+	}
+	
+	@AllArgsConstructor @Getter
+	public static enum Weight {
+		g("g", 1),
+		oz("oz", 28.3495),
+		lb("lb", 453.592),
+		kg("kg", 1000),
+		pinch("pinch", 0.36),
+		dash("dash", 0.72);
+		
+		final String displayName;
+		final double atomicUnits;
+	}
+	
+	String name;
+	double value = 0;
+	@JsonIgnore Either<Volume, Weight> measure;
+	
+	Ingredient plus(final Ingredient rhs) {
+		if (!name.equals(rhs.name)) {
+			throw new IllegalArgumentException("Combining ingredients of differing types");
+		} else if (measure.getState() != rhs.measure.getState()) {
+			throw new IllegalArgumentException("Cannot merge mass and volume together");
+		}
+		return new Ingredient(name, value + (rhs.value * conversionRatio(rhs.measure)), measure);
+	}
+
+	private double conversionRatio(Either<Volume, Weight> rhs) {
+		return units(rhs) / units(measure);
+	}
+
+	private static double units(Either<Volume, Weight> measure) {
+		return measure.unify(Volume::getAtomicUnits, Weight::getAtomicUnits);
+	}
+	
+	@JsonProperty("unit")
+	private String[] measureToString() {
+		String[] rval = {null, null};
+		return rval;
+	}
+	
+	@JsonProperty("unit")
+	private void measureFromString(String[] s) {
+		if (s[0] != null) {
+			measure = Either.ofLeft(Volume.valueOf(s[0]));
+		} else if (s[1] != null) {
+			measure = Either.ofRight(Weight.valueOf(s[1]));
+		}
+	}
+}

+ 52 - 0
src/main/lombok/org/leumasjaffe/recipe/model/Product.java

@@ -0,0 +1,52 @@
+package org.leumasjaffe.recipe.model;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import org.leumasjaffe.container.Pair;
+
+import lombok.Data;
+
+@Data
+public class Product implements CompoundRecipeComponent {
+	@Data
+	public static class Phase implements CompoundRecipeComponent {
+		List<Step> steps = new ArrayList<>();
+
+		@SuppressWarnings("unchecked")
+		@Override
+		public Collection<RecipeComponent> getComponents() {
+			return (Collection<RecipeComponent>)(List<?>) steps;
+		}
+	}
+	
+	@Data
+	public static class RecipePhase {
+		List<Pair<Phase, Rest>> additionalPhases = new ArrayList<>();
+		Phase phase;
+		Optional<Rest> rest = Optional.empty();
+		
+		void addComponents(Collection<RecipeComponent> comps) {
+			additionalPhases.forEach(pair -> {
+				comps.add(pair.getLeft());
+				comps.add(pair.getRight());
+			});
+			comps.add(phase);
+			rest.ifPresent(comps::add);
+		}
+	}
+	
+	String name;
+	RecipePhase prep;
+	RecipePhase cooking;
+	
+	@Override
+	public Collection<RecipeComponent> getComponents() {
+		Collection<RecipeComponent> comps = new ArrayList<>();
+		prep.addComponents(comps);
+		cooking.addComponents(comps);
+		return comps;
+	}
+}

+ 30 - 0
src/main/lombok/org/leumasjaffe/recipe/model/Recipe.java

@@ -0,0 +1,30 @@
+package org.leumasjaffe.recipe.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.swing.ImageIcon;
+
+import lombok.Data;
+
+/**
+ * Recipe handling logic::
+ * 	if (num components == 1) {
+ * 	  let c := components(0)
+ * 	  create ingredient listing c.ingredients
+ *    add product panel for c
+ *  } else {
+ *    for (each component c) {
+ *    	create ingredient listing c.title => c.ingredients
+ *      add product panel for c with heading c.title
+ *    }
+ *  }
+ */
+@Data
+public class Recipe {
+	String title;
+	String description;
+	Optional<ImageIcon> photo; // TODO JSONIZATION	
+	List<Product> products = new ArrayList<>();
+}

+ 8 - 0
src/main/lombok/org/leumasjaffe/recipe/model/RecipeComponent.java

@@ -0,0 +1,8 @@
+package org.leumasjaffe.recipe.model;
+
+import java.util.Collection;
+
+public interface RecipeComponent {
+	Collection<Ingredient> getIngredients();
+	Duration getDuration();
+}

+ 22 - 0
src/main/lombok/org/leumasjaffe/recipe/model/Rest.java

@@ -0,0 +1,22 @@
+package org.leumasjaffe.recipe.model;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import lombok.Data;
+
+@Data
+public class Rest implements RecipeComponent {
+	public enum Where {
+		FREEZER, REFRIGERATOR, ROOM_TEMPERATURE, WARM_PLACE
+	}
+	
+	Where where;
+	Duration duration;
+	
+	@Override
+	public Collection<Ingredient> getIngredients() {
+		return Collections.emptyList();
+	}
+
+}

+ 13 - 0
src/main/lombok/org/leumasjaffe/recipe/model/Step.java

@@ -0,0 +1,13 @@
+package org.leumasjaffe.recipe.model;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import lombok.Data;
+
+@Data
+public class Step implements RecipeComponent {
+	Set<Ingredient> ingredients = new HashSet<>();
+	Duration duration;
+	String instruction;
+}

+ 80 - 0
src/main/lombok/org/leumasjaffe/recipe/view/AutoGrowPanel.java

@@ -0,0 +1,80 @@
+package org.leumasjaffe.recipe.view;
+
+import java.awt.Component;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.IntFunction;
+import java.util.function.Supplier;
+
+import javax.swing.JPanel;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+import org.jdesktop.swingx.VerticalLayout;
+import org.leumasjaffe.event.AnyActionDocumentListener;
+
+import lombok.AllArgsConstructor;
+
+@SuppressWarnings("serial")
+public class AutoGrowPanel extends JPanel {
+	public static interface DocumentListenable {
+		void addDocumentListener(DocumentListener dl);
+		void removeDocumentListener(DocumentListener dl);
+		default void setListPosition(int zeroIndex) {}
+	}
+	
+	@AllArgsConstructor
+	private class DeleteOnEmpty implements AnyActionDocumentListener {
+		DocumentListenable content;
+		@Override public void update(DocumentEvent e) {
+			if (e.getDocument().getLength() == 0) {
+				content.removeDocumentListener(this);
+				int index = members.indexOf(content);
+				members.remove(index);
+				for (final int size = members.size(); index < size; ++index) {
+					members.get(index).setListPosition(index);
+				}
+				remove((Component) content);
+				getParent().getParent().validate();
+			}
+		}
+	}
+	
+	IntFunction<DocumentListenable> prod;
+	AnyActionDocumentListener grow;
+	List<DocumentListenable> members = new ArrayList<>();
+	
+	/**
+	 * @wbp.parser.constructor
+	 */
+	public AutoGrowPanel(Supplier<DocumentListenable> prod) {
+		this(i -> prod.get());
+	}
+	
+	public AutoGrowPanel(IntFunction<DocumentListenable> prod) {
+		this.prod = prod;
+		
+		this.grow = new AnyActionDocumentListener() {
+			@Override public void update(DocumentEvent e) {
+				getBack().removeDocumentListener(this);
+				getBack().addDocumentListener(new DeleteOnEmpty(getBack()));
+				extend();
+				getParent().getParent().validate();
+			}
+		};
+
+		setLayout(new VerticalLayout(5));
+		extend();
+	}
+
+	private void extend() {
+		members.add(prod.apply(members.size()));
+		add((Component) getBack());
+		getBack().addDocumentListener(grow);
+	}
+
+	private DocumentListenable getBack() {
+		return members.get(members.size() - 1);
+	}
+	
+}

+ 50 - 0
src/main/lombok/org/leumasjaffe/recipe/view/ImagePanel.java

@@ -0,0 +1,50 @@
+package org.leumasjaffe.recipe.view;
+
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+import javax.swing.JPanel;
+
+import lombok.Getter;
+import lombok.Synchronized;
+
+@SuppressWarnings("serial")
+public class ImagePanel extends JPanel {
+	private static final @Getter(lazy=true) BufferedImage placeholder = getDefaultImage();
+    private BufferedImage image;
+
+    public ImagePanel() {
+    	image = getPlaceholder();
+    	setPreferredSize(new Dimension(200, 200));
+    }
+    
+    public ImagePanel(String imagePath) {
+       try {                
+          image = ImageIO.read(new File(imagePath));
+       } catch (IOException ex) {
+          image = getPlaceholder();
+       }
+       setPreferredSize(new Dimension(200, 200));
+   }
+
+    @Synchronized
+    private static BufferedImage getDefaultImage() {
+    	try {
+    		return ImageIO.read(new File("src/main/resources/200.png"));
+    	} catch (IOException ex) {
+    		return null;
+    	}
+	}
+
+	@Override
+    protected void paintComponent(Graphics g) {
+    	if (image == null) return;
+        super.paintComponent(g);
+        g.drawImage(image, 0, 0, this); // see javadoc for more info on the parameters            
+    }
+
+}

+ 111 - 0
src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java

@@ -0,0 +1,111 @@
+package org.leumasjaffe.recipe.view;
+
+import javax.swing.JPanel;
+import java.awt.GridBagLayout;
+import javax.swing.JTextField;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.awt.event.ItemEvent;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import javax.swing.JToggleButton;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.NumberFormatter;
+
+import org.leumasjaffe.recipe.model.Ingredient;
+
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JFormattedTextField;
+import java.awt.Font;
+import javax.swing.JLabel;
+
+@SuppressWarnings("serial")
+public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
+	private JTextField txtName;
+	
+	public IngredientPanel() {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 100, 40, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JLabel label = new JLabel("\u2022");
+		GridBagConstraints gbc_label = new GridBagConstraints();
+		gbc_label.insets = new Insets(0, 0, 0, 5);
+		gbc_label.anchor = GridBagConstraints.EAST;
+		gbc_label.gridx = 0;
+		gbc_label.gridy = 0;
+		add(label, gbc_label);
+		
+		txtName = new JTextField();
+		txtName.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_txtName = new GridBagConstraints();
+		gbc_txtName.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtName.insets = new Insets(0, 0, 0, 5);
+		gbc_txtName.gridx = 1;
+		gbc_txtName.gridy = 0;
+		add(txtName, gbc_txtName);
+		txtName.setColumns(10);
+		
+		NumberFormatter fmtDone = new NumberFormatter(NumberFormat.getNumberInstance(Locale.getDefault()));
+		fmtDone.setMinimum(0.0);
+		fmtDone.setCommitsOnValidEdit(true);
+		JFormattedTextField txtAmount = new JFormattedTextField(fmtDone);
+		txtAmount.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_txtAmount = new GridBagConstraints();
+		gbc_txtAmount.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtAmount.insets = new Insets(0, 0, 0, 5);
+		gbc_txtAmount.gridx = 2;
+		gbc_txtAmount.gridy = 0;
+		add(txtAmount, gbc_txtAmount);
+		txtAmount.setColumns(4);
+		
+		JToggleButton tglbtnUnits = new JToggleButton("vol");
+		tglbtnUnits.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		tglbtnUnits.setMargin(new Insets(2, 5, 2, 5));
+		GridBagConstraints gbc_tglbtnUnits = new GridBagConstraints();
+		gbc_tglbtnUnits.insets = new Insets(0, 0, 0, 5);
+		gbc_tglbtnUnits.gridx = 3;
+		gbc_tglbtnUnits.gridy = 0;
+		add(tglbtnUnits, gbc_tglbtnUnits);
+		
+		JComboBox<Ingredient.Weight> weights = new JComboBox<>(new DefaultComboBoxModel<>(Ingredient.Weight.values()));
+		weights.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		JComboBox<Ingredient.Volume> volumes = new JComboBox<>(new DefaultComboBoxModel<>(Ingredient.Volume.values()));
+		volumes.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_comboBox = new GridBagConstraints();
+		gbc_comboBox.fill = GridBagConstraints.HORIZONTAL;
+		gbc_comboBox.gridx = 4;
+		gbc_comboBox.gridy = 0;
+		add(volumes, gbc_comboBox);
+//		add(weights, gbc_comboBox);
+
+		tglbtnUnits.addItemListener(e -> {
+			if (e.getStateChange() == ItemEvent.SELECTED) {
+				tglbtnUnits.setText("wgt");
+				remove(volumes);
+				add(weights, gbc_comboBox);
+			} else {
+				tglbtnUnits.setText("vol");
+				remove(weights);
+				add(volumes, gbc_comboBox);
+			}
+			repaint();
+		});
+	}
+
+	@Override
+	public void addDocumentListener(DocumentListener dl) {
+		this.txtName.getDocument().addDocumentListener(dl);
+	}
+
+	@Override
+	public void removeDocumentListener(DocumentListener dl) {
+		this.txtName.getDocument().removeDocumentListener(dl);
+	}
+
+}

+ 22 - 0
src/main/lombok/org/leumasjaffe/recipe/view/PhasePanel.java

@@ -0,0 +1,22 @@
+package org.leumasjaffe.recipe.view;
+
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+
+import org.leumasjaffe.recipe.model.Product;
+import org.leumasjaffe.recipe.model.Step;
+import org.jdesktop.swingx.VerticalLayout;
+
+@SuppressWarnings("serial")
+public class PhasePanel extends JPanel {
+
+	public PhasePanel(Product.Phase phase) {
+		setLayout(new VerticalLayout(5));
+		int i = 0;
+		for (Step comp : phase.getSteps()) {
+			add(new StepPanel(i++, comp));
+			add(new JSeparator());
+		}
+	}
+
+}

+ 47 - 0
src/main/lombok/org/leumasjaffe/recipe/view/ProductPanel.java

@@ -0,0 +1,47 @@
+package org.leumasjaffe.recipe.view;
+
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+import org.leumasjaffe.recipe.model.Product;
+import org.leumasjaffe.recipe.model.RecipeComponent;
+import org.leumasjaffe.recipe.model.Rest;
+import org.jdesktop.swingx.VerticalLayout;
+
+import javax.swing.JButton;
+import javax.swing.JSeparator;
+
+@SuppressWarnings("serial")
+public class ProductPanel extends JScrollPane {
+	private JPanel panelViewPort;
+	public ProductPanel(Product product) {
+		JPanel panelColumnHeader = new JPanel();
+		setColumnHeaderView(panelColumnHeader);
+		
+		JButton btnAddStep = new JButton("Add Step");
+		panelColumnHeader.add(btnAddStep);
+		
+		panelViewPort = new JPanel();
+		setViewportView(panelViewPort);
+		panelViewPort.setLayout(new VerticalLayout(5));
+		
+		for (RecipeComponent comp : product.getComponents()) {
+			if (comp instanceof Rest) {
+				addRest((Rest) comp);
+			} else if (comp instanceof Product.Phase) {
+				addPhase((Product.Phase) comp);
+			}
+		}
+	}
+	
+	void addPhase(final Product.Phase phase) {
+		panelViewPort.add(new PhasePanel(phase));
+		panelViewPort.add(new JSeparator());
+	}
+	
+	void addRest(final Rest rest) {
+		panelViewPort.add(new RestPanel(rest));
+		panelViewPort.add(new JSeparator());
+	}
+
+}

+ 42 - 0
src/main/lombok/org/leumasjaffe/recipe/view/RecipeFrame.java

@@ -0,0 +1,42 @@
+package org.leumasjaffe.recipe.view;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.swing.JFrame;
+import javax.swing.JTabbedPane;
+
+import org.leumasjaffe.recipe.model.Product;
+import org.leumasjaffe.recipe.model.Recipe;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+@SuppressWarnings("serial")
+public class RecipeFrame extends JFrame {
+	public RecipeFrame() {
+		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+		
+		JTabbedPane tabbedPane = new JTabbedPane();
+		setContentPane(tabbedPane);
+
+		SummaryPanel summaryPanel = new SummaryPanel();
+		tabbedPane.addTab("Summary", summaryPanel);
+		
+		ObjectMapper mapper = new ObjectMapper();
+		mapper.registerModule(new Jdk8Module());
+		try {
+			Recipe recipe = mapper.readValue(new File("src/test/resources/example.json"), Recipe.class);
+			for (Product comp : recipe.getProducts()) {
+				tabbedPane.addTab(comp.getName(), new ProductPanel(comp));
+			}
+		} catch (IOException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+
+		pack();
+		repaint();
+		setVisible(true);
+	}
+}

+ 44 - 0
src/main/lombok/org/leumasjaffe/recipe/view/RestPanel.java

@@ -0,0 +1,44 @@
+package org.leumasjaffe.recipe.view;
+
+import javax.swing.JPanel;
+
+import org.leumasjaffe.recipe.model.Rest;
+import java.awt.GridBagLayout;
+import javax.swing.JLabel;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+
+@SuppressWarnings("serial")
+public class RestPanel extends JPanel {
+
+	public RestPanel(Rest rest) {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 0.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JLabel lblRest = new JLabel("Rest");
+		GridBagConstraints gbc_lblRest = new GridBagConstraints();
+		gbc_lblRest.insets = new Insets(0, 0, 0, 5);
+		gbc_lblRest.gridx = 0;
+		gbc_lblRest.gridy = 0;
+		add(lblRest, gbc_lblRest);
+		
+		JLabel lblLocation = new JLabel(rest.getWhere().toString());
+		GridBagConstraints gbc_lblLocation = new GridBagConstraints();
+		gbc_lblLocation.insets = new Insets(0, 0, 0, 5);
+		gbc_lblLocation.gridx = 1;
+		gbc_lblLocation.gridy = 0;
+		add(lblLocation, gbc_lblLocation);
+		
+		JLabel lblDuration = new JLabel(rest.getDuration().toString());
+		GridBagConstraints gbc_lblDuration = new GridBagConstraints();
+		gbc_lblDuration.gridx = 2;
+		gbc_lblDuration.gridy = 0;
+		add(lblDuration, gbc_lblDuration);
+		// TODO Auto-generated constructor stub
+	}
+
+}

+ 105 - 0
src/main/lombok/org/leumasjaffe/recipe/view/StepPanel.java

@@ -0,0 +1,105 @@
+package org.leumasjaffe.recipe.view;
+
+import javax.swing.JPanel;
+import javax.swing.event.DocumentListener;
+
+import org.leumasjaffe.recipe.model.Step;
+
+import java.awt.GridBagLayout;
+
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+
+import javax.swing.JLabel;
+import javax.swing.JTextPane;
+import java.awt.Component;
+import javax.swing.Box;
+import java.awt.Dimension;
+
+@SuppressWarnings("serial")
+public class StepPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
+	private JLabel lblIndex;
+	private JTextPane txtpnInstructions;
+		
+	public StepPanel(int zeroIndex, Step step) {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JPanel panelLeft = new JPanel();
+		panelLeft.setPreferredSize(new Dimension(200, 50));
+		GridBagConstraints gbc_panelLeft = new GridBagConstraints();
+		gbc_panelLeft.insets = new Insets(0, 0, 0, 5);
+		gbc_panelLeft.fill = GridBagConstraints.BOTH;
+		gbc_panelLeft.gridx = 0;
+		gbc_panelLeft.gridy = 0;
+		add(panelLeft, gbc_panelLeft);
+		GridBagLayout gbl_panelLeft = new GridBagLayout();
+		gbl_panelLeft.columnWidths = new int[]{0, 0, 0, 0};
+		gbl_panelLeft.rowHeights = new int[]{0, 0, 0};
+		gbl_panelLeft.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE};
+		gbl_panelLeft.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		panelLeft.setLayout(gbl_panelLeft);
+		
+		lblIndex = new JLabel("");
+		GridBagConstraints gbc_lblIndex = new GridBagConstraints();
+		gbc_lblIndex.fill = GridBagConstraints.HORIZONTAL;
+		gbc_lblIndex.insets = new Insets(0, 0, 5, 5);
+		gbc_lblIndex.gridx = 0;
+		gbc_lblIndex.gridy = 0;
+		panelLeft.add(lblIndex, gbc_lblIndex);
+		
+		Component horizontalGlue = Box.createHorizontalGlue();
+		GridBagConstraints gbc_horizontalGlue = new GridBagConstraints();
+		gbc_horizontalGlue.insets = new Insets(0, 0, 5, 5);
+		gbc_horizontalGlue.gridx = 1;
+		gbc_horizontalGlue.gridy = 0;
+		panelLeft.add(horizontalGlue, gbc_horizontalGlue);
+		
+		JLabel lblDuration = new JLabel("Duration");
+		GridBagConstraints gbc_lblDuration = new GridBagConstraints();
+		gbc_lblDuration.insets = new Insets(0, 0, 5, 0);
+		gbc_lblDuration.gridx = 2;
+		gbc_lblDuration.gridy = 0;
+		panelLeft.add(lblDuration, gbc_lblDuration);
+		
+		AutoGrowPanel panelIngredients = new AutoGrowPanel(IngredientPanel::new);
+		GridBagConstraints gbc_panelIngredients = new GridBagConstraints();
+		gbc_panelIngredients.gridwidth = 3;
+		gbc_panelIngredients.insets = new Insets(0, 0, 0, 5);
+		gbc_panelIngredients.fill = GridBagConstraints.BOTH;
+		gbc_panelIngredients.gridx = 0;
+		gbc_panelIngredients.gridy = 1;
+		panelLeft.add(panelIngredients, gbc_panelIngredients);
+		
+		txtpnInstructions = new JTextPane();
+		txtpnInstructions.setPreferredSize(new Dimension(200, 30));
+		txtpnInstructions.setText("Instructions");
+		GridBagConstraints gbc_txtpnInstructions = new GridBagConstraints();
+		gbc_txtpnInstructions.fill = GridBagConstraints.BOTH;
+		gbc_txtpnInstructions.gridx = 1;
+		gbc_txtpnInstructions.gridy = 0;
+		add(txtpnInstructions, gbc_txtpnInstructions);
+		
+		setListPosition(zeroIndex);
+	}
+
+	@Override
+	public void addDocumentListener(DocumentListener dl) {
+		this.txtpnInstructions.getDocument().addDocumentListener(dl);
+	}
+
+	@Override
+	public void removeDocumentListener(DocumentListener dl) {
+		this.txtpnInstructions.getDocument().removeDocumentListener(dl);		
+	}
+	
+	@Override
+	public void setListPosition(int zeroIndex) {
+		this.lblIndex.setText("Step " + Integer.toString(zeroIndex + 1));
+		repaint();
+	}
+}

+ 86 - 0
src/main/lombok/org/leumasjaffe/recipe/view/SummaryPanel.java

@@ -0,0 +1,86 @@
+package org.leumasjaffe.recipe.view;
+
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.JTextPane;
+
+import org.jdesktop.swingx.VerticalLayout;
+
+@SuppressWarnings("serial")
+public class SummaryPanel extends JPanel {
+	JTextField txtTitle;
+	
+	public SummaryPanel() {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, 1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{1.0, 1.0, 1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JPanel panelHeader = new JPanel();
+		GridBagConstraints gbc_panelHeader = new GridBagConstraints();
+		gbc_panelHeader.gridwidth = 2;
+		gbc_panelHeader.insets = new Insets(0, 0, 5, 0);
+		gbc_panelHeader.fill = GridBagConstraints.BOTH;
+		gbc_panelHeader.gridx = 0;
+		gbc_panelHeader.gridy = 0;
+		add(panelHeader, gbc_panelHeader);
+		GridBagLayout gbl_panelHeader = new GridBagLayout();
+		gbl_panelHeader.columnWidths = new int[]{0, 0, 0};
+		gbl_panelHeader.rowHeights = new int[]{0, 0};
+		gbl_panelHeader.columnWeights = new double[]{1.0, 0.0, Double.MIN_VALUE};
+		gbl_panelHeader.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panelHeader.setLayout(gbl_panelHeader);
+		
+		txtTitle = new JTextField();
+		txtTitle.setText("Title");
+		GridBagConstraints gbc_txtTitle = new GridBagConstraints();
+		gbc_txtTitle.insets = new Insets(0, 0, 0, 5);
+		gbc_txtTitle.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtTitle.gridx = 0;
+		gbc_txtTitle.gridy = 0;
+		panelHeader.add(txtTitle, gbc_txtTitle);
+		txtTitle.setColumns(10);
+		
+		JLabel lblDuration = new JLabel("Duration");
+		GridBagConstraints gbc_lblDuration = new GridBagConstraints();
+		gbc_lblDuration.gridx = 1;
+		gbc_lblDuration.gridy = 0;
+		panelHeader.add(lblDuration, gbc_lblDuration);
+		
+		JPanel panelIngredients = new JPanel();
+		panelIngredients.setPreferredSize(new Dimension(200, 100));
+		GridBagConstraints gbc_panelIngredients = new GridBagConstraints();
+		gbc_panelIngredients.gridheight = 2;
+		gbc_panelIngredients.insets = new Insets(0, 0, 0, 5);
+		gbc_panelIngredients.fill = GridBagConstraints.BOTH;
+		gbc_panelIngredients.gridx = 0;
+		gbc_panelIngredients.gridy = 1;
+		add(panelIngredients, gbc_panelIngredients);
+		panelIngredients.setLayout(new VerticalLayout());
+		
+		JPanel panelPhoto = new ImagePanel();
+		GridBagConstraints gbc_panelPhoto = new GridBagConstraints();
+		gbc_panelPhoto.insets = new Insets(0, 0, 5, 0);
+		gbc_panelPhoto.fill = GridBagConstraints.BOTH;
+		gbc_panelPhoto.gridx = 1;
+		gbc_panelPhoto.gridy = 1;
+		add(panelPhoto, gbc_panelPhoto);
+		
+		JTextPane txtpnDescription = new JTextPane();
+		txtpnDescription.setPreferredSize(new Dimension(0, 100));
+		txtpnDescription.setText("Description");
+		GridBagConstraints gbc_txtpnDescription = new GridBagConstraints();
+		gbc_txtpnDescription.fill = GridBagConstraints.BOTH;
+		gbc_txtpnDescription.gridx = 1;
+		gbc_txtpnDescription.gridy = 2;
+		add(txtpnDescription, gbc_txtpnDescription);
+	}
+}

BIN
src/main/resources/200.png


+ 37 - 0
src/test/resources/example.json

@@ -0,0 +1,37 @@
+{
+  "title": "Peanut Chicken Curry",
+  "description": "A fusion style peanut-butter based chicken curry from Chef John of FoodWishes.com",
+  "products": [
+    {
+      "name": "Curry",
+      "prep": {
+        "phase": {
+          "steps": [
+            {
+              "ingredients": [
+                {
+                  "name": "onion",
+                  "value": 100,
+                  "unit": [ null, "g" ]
+                }
+              ],
+              "duration": {
+                "displayAs": "MINUTES",
+                "isApproximate": true,
+                "minSeconds": 300,
+                "maxSeconds": 600
+              },
+              "instruction": "Expode"
+            }
+          ]
+        }
+      },
+      "cooking": {
+        "phase": {
+          "steps": [
+          ]
+        }
+      }
+    }
+  ]
+}