changeset 2:b8a19781f79b

shipment logging
author Nenue
date Thu, 13 Oct 2016 09:08:38 -0400
parents 232617b8bcd5
children c006ce87a147
files ClassPlan.lua ClassPlan.xml
diffstat 2 files changed, 545 insertions(+), 100 deletions(-) [+]
line wrap: on
line diff
--- a/ClassPlan.lua	Thu Oct 13 05:32:43 2016 -0400
+++ b/ClassPlan.lua	Thu Oct 13 09:08:38 2016 -0400
@@ -1,3 +1,11 @@
+local wipe = table.wipe
+local pairs, ipairs = pairs, ipairs
+local GetTime = GetTime
+local blockTemplate = {
+  point = 'TOPLEFT',
+  relativePoint ='TOPLEFT',
+}
+
 SLASH_CLASSPLAN1 = "/classplan"
 SLASH_CLASSPLAN2 = "/cp"
 SlashCmdList.CLASSPLAN = function(args)
@@ -10,27 +18,85 @@
 end
 
 ClassOrderPlanCore = {
+  freeBlocks = {},
   blocks = {},
-  playerFirst = true,
-  timers = {}
+  shipmentBlocks = {},
+  freeShipmentBlocks = {},
+  sortedShipments = {},
+  sortedMissions = {},
+  timers = {},
+  shipments = {},
+  playerFirst = false,
+  templates = setmetatable({}, {
+    __newindex = function(t,k ,v)
+      if type(v) == 'table' then
+        setmetatable(v, {__index = blockTemplate})
+        rawset(t,k,v)
+      end
+    end
+  })
 }
-ClassPlanBlockMixin = {
-  followers = {},
-  blocks = {},
-}
-local core, block = ClassOrderPlanCore, ClassPlanBlockMixin
+ClassPlanBlockMixin = {}
+ClassPlanShipmentMixin = setmetatable({}, {__index = ClassPlanBlockMixin})
+local core, block, shipment = ClassOrderPlanCore, ClassPlanBlockMixin, ClassPlanShipmentMixin
 local print = DEVIAN_WORKSPACE and function(...) print('ClassPlan', ...) end or nop
 
+
+
 function core:OnLoad ()
   self:RegisterUnitEvent('UNIT_PORTRAIT_UPDATE', 'player')
-  self:RegisterEvent('GARRISON_MISSION_STARTED')
-  self:RegisterEvent('GARRISON_MISSION_FINISHED')
   self:RegisterEvent('PLAYER_LOGIN')
   self:RegisterEvent('PLAYER_ENTERING_WORLD')
+  self:RegisterEvent('PLAYER_REGEN_ENABLED')
+
+  self:RegisterEvent('GARRISON_MISSION_LIST_UPDATE')
+  self:RegisterEvent('GARRISON_MISSION_FINISHED')
+  self:RegisterEvent("GARRISON_LANDINGPAGE_SHIPMENTS");
+  self:RegisterEvent("GARRISON_SHIPMENT_RECEIVED");
+  self:RegisterEvent("GARRISON_TALENT_UPDATE");
+  self:RegisterEvent("GARRISON_TALENT_COMPLETE");
 end
 
+core.templates.ClassPlanBlock = {
+  SetItemData = function(block, data)
+    block.isComplete = data.isComplete
+    block.missionEndTime = data.missionEndTime
+  end
+}
+
+core.templates.ClassPlanShipment = {
+
+  parent = false,
+  point = 'TOPRIGHT',
+  relativePoint ='TOPRIGHT',
+  SetItemData = function(block, data)
+    block.icon = data.icon
+    block.shipmentCapacity = data.shipmentCapacity
+    block.shipmentsReady = data.shipmentsReady
+    block.shipmentsTotal = data.shipmentsTotal
+    block.creationTime = data.creationTime
+    block.duration = data.duration
+    block.itemID = data.itemID
+    block.itemQuality = data.itemQuality
+    --[[
+        icon = texture,
+        shipmentCapacity = shipmentCapacity,
+        shipmentsReady = shipmentsReady,
+        shipmentsTotal = shipmentsTotal,
+        creationTime = creationTime,
+        duration = duration,
+        timeleftString = timeleftString,
+        itemName = itemName,
+        itemIcon = itemIcon,
+        itemQuality = itemQuality,
+        itemID = itemID
+
+    --]]
+  end
+}
 
 function core:OnEvent (event, ...)
+  print(event)
   if event == 'UNIT_PORTRAIT_UPDATE' then
     SetPortraitTexture(self.portrait, 'player')
   elseif event == 'PLAYER_LOGIN' then
@@ -48,166 +114,461 @@
         end
         self.profile = self.data[self.profileKey]
 
+        self.profile.shipments = self.profile.shipments or {}
+        self.profile.missions = self.profile.missions or {}
+        self.profile.classColor = RAID_CLASS_COLORS[select(2, UnitClass('player'))]
+
+        C_Garrison.RequestLandingPageShipmentInfo();
         self.initialized = true
-        print('initialized')
       end
     end
+  elseif event == 'GARRISON_LANDINGPAGE_SHIPMENTS' or event == 'GARRISON_TALENT_UPDATE' then
+    self:UpdateShipments()
+  elseif event == 'PLAYER_REGEN_ENABLED' or event == 'GARRISON_MISSION_FINISHED' or event == 'GARRISON_TALENT_COMPLETE' or event == 'GARRISON_SHIPMENT_RECEIVED' then
+    self:UpdateNotifications()
   else
-    self:Refresh ()
+    self:UpdateItems ()
   end
 end
 
-function core:UpdateList()
-  self.sortedMissions = self.sortedMissions or {}
-  table.wipe(self.sortedMissions)
+function core:UpdateNotifications()
+end
 
+function core:RefreshItems(sortedItems, templateName)
+  self.blocks[templateName] = self.blocks[templateName] or {}
+  local blocks = self.blocks[templateName]
+  local template = self.templates[templateName] or {
+    parent = self.portrait,
+    point = 'TOPLEFT',
+    relativePoint ='TOPRIGHT',
+  }
+
+  local lastProfile
+  local numItems = #sortedItems
+  for i, data in ipairs(sortedItems) do
+    local block = blocks[i]
+
+    if not block then
+      block = CreateFrame('Frame', nil, self, templateName)
+      block:SetID(i)
+      template.numBlocks = (template.numBlocks or 0) + 1
+
+      if template.lastBlock then
+        block:SetPoint('TOPLEFT', template.lastBlock, 'BOTTOMLEFT', 0, 0)
+      else
+        block:SetPoint(template.point, self[template.parent] or self, template.relativePoint, 0, 0)
+      end
+      template.lastBlock = block
+      blocks[i] = block
+    end
+
+    if template.SetItemData then
+      template.SetItemData(block, data)
+    end
+
+
+    block.lastProfile = lastProfile
+    block:Refresh(data)
+    block:Show()
+    lastProfile = data.profileKey
+  end
+
+  for i = numItems + 1, template.numBlocks do
+    if blocks[i] then
+      blocks[i]:Hide()
+    end
+  end
+end
+
+function core:Refresh()
+  if self.isStale then
+    self:SortLists()
+  end
+  self.isStale = nil
+
+  self:RefreshItems(self.sortedMissions, 'ClassPlanBlock')
+  self:RefreshItems(self.sortedShipments, 'ClassPlanShipment')
+
+
+  local posX = self.data.posX or 0
+  local posY = self.data.posY or -24
+  local point = self.point or 'TOP'
+  local relativePoint = self.point or 'TOP'
+  self:SetPoint(point, UIParent, relativePoint, posX, posY)
+
+
+end
+
+function core:OnUpdate()
+  if self.fadeTimer and self.fadeTimer < GetTime() then
+    self:Hide()
+  end
+end
+
+function core:OnShow()
+  if self.isStale then
+    print('updating items on show')
+    self:Refresh()
+  end
+
+end
+
+function core:SortLists()
+
+  wipe(self.sortedShipments)
+  wipe(self.sortedMissions)
   for name, profile in pairs(self.data) do
     local isMine = (profile == self.profile)
     for index, data in pairs(profile.missions) do
 
       data.classColor = profile.classColor or {r = 0.7, g = 0.7, b =0.7}
-
       data.profileKey = name
       data.isMine = (profile == self.profile)
       tinsert(self.sortedMissions, data)
     end
+
+    if not profile.shipments then
+      profile.shipments = {}
+      profile.shipment = nil
+    end
+
+    for index, data in pairs(profile.shipments) do
+      data.classColor = profile.classColor or {r = 0.7, g = 0.7, b =0.7}
+      data.profileKey = name
+      data.isMine = (profile == self.profile)
+      tinsert(self.sortedShipments, data)
+    end
   end
 
-  for i, v in ipairs(self.sortedMissions) do
-    print(i, v.missionEndTime, v.name)
-  end
-
-
-  table.sort(self.sortedMissions, function(a,b)
+  table.sort(self.sortedMissions, function (a,b)
     local result = false
     if not a or not b then
       result = true
     else
-
       if (a.isMine ~= b.isMine) and self.playerFirst then
         result =  a.isMine
       else
         if (not b.missionEndTime) or (not a.missionEndTime) then
           print('missing article', b.missionEndTime, a.missionEndTime)
         end
-
         result = ( b.missionEndTime > a.missionEndTime)
       end
     end
 
-    print('cmp', (b and (b.missionEndTime .. ' ' .. tostring(b.isMine)) or '-'), '>', (a and (a.missionEndTime .. ' ' .. tostring(a.isMine)) or '-'), result, n)
+    --print('cmp', (b and (b.missionEndTime .. ' ' .. tostring(b.isMine)) or '-'), '>', (a and (a.missionEndTime .. ' ' .. tostring(a.isMine)) or '-'), result, n)
     return result
   end)
-  self.isStale = nil
 
-  local lastProfile
-  local numItems = #self.sortedMissions
-  for i, data in ipairs(self.sortedMissions) do
-    local block = self.blocks[i]
-    if not block then
-      block = CreateFrame('Frame', nil, self, 'ClassPlanBlock')
-      block:SetID(i)
-      self.numBlocks = (self.numBlocks or 0) + 1
+end
 
-      if self.lastBlock then
-        block:SetPoint('TOPLEFT', self.lastBlock, 'BOTTOMLEFT', 0, 0)
-      else
-        block:SetPoint('TOPLEFT', self.portrait, 'TOPRIGHT', 0, 0)
+function core:UpdateShipments()
+  print('|cFF0088FFShipments|r:', self.profileKey)
+  if not self.profile then
+    return
+  end
+  wipe(self.shipments)
+
+
+  local garrisonType = LE_GARRISON_TYPE_7_0
+  local buildings = C_Garrison.GetBuildings(garrisonType);
+  local shipmentIndex = 0
+  print('Buildings:')
+  for i = 1, #buildings do
+    local name, texture, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString, itemName, itemIcon, itemQuality, itemID = C_Garrison.GetLandingPageShipmentInfo(buildingID);
+    print(buildings[i], name, creationTime, duration)
+    tinsert(self.shipments,
+      {
+        shipmentType = 'Work Order',
+        name = name,
+        icon = texture,
+        shipmentCapacity = shipmentCapacity,
+        shipmentsReady = shipmentsReady,
+        shipmentsTotal = shipmentsTotal,
+        creationTime = creationTime,
+        duration = duration,
+        timeleftString = timeleftString,
+        itemName = itemName,
+        itemIcon = itemIcon,
+        itemQuality = itemQuality,
+        itemID = itemID
+      })
+  end
+
+  print('Follower:')
+  local followerShipments = C_Garrison.GetFollowerShipments(garrisonType);
+  for i = 1, #followerShipments do
+    local name, texture, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString, _, _, _, _, followerID = C_Garrison.GetLandingPageShipmentInfoByContainerID(followerShipments[i]);
+    print(followerShipments[i], name, creationTime, duration)
+    tinsert(self.shipments,
+      {
+        shipmentType = '',
+        name = name,
+        icon = texture,
+        shipmentCapacity = shipmentCapacity,
+        shipmentsReady = shipmentsReady,
+        shipmentsTotal = shipmentsTotal,
+        creationTime = creationTime,
+        duration = duration,
+        timeleftString = timeleftString,
+        followerID = followerID,
+      })
+  end
+
+  print('Loose:')
+  local looseShipments = C_Garrison.GetLooseShipments(garrisonType)
+  for i = 1, #looseShipments do
+    local name, texture, shipmentCapacity, shipmentsReady, shipmentsTotal, creationTime, duration, timeleftString = C_Garrison.GetLandingPageShipmentInfoByContainerID(looseShipments[i]);
+    print(looseShipments[i], name, creationTime, duration)
+    tinsert(self.shipments,
+      {
+        shipmentType = '',
+        name = name,
+        icon = texture,
+        shipmentCapacity = shipmentCapacity,
+        shipmentsReady = shipmentsReady,
+        shipmentsTotal = shipmentsTotal,
+        creationTime = creationTime,
+        duration = duration,
+        timeleftString = timeleftString,
+      })
+  end
+
+  local talentTrees = C_Garrison.GetTalentTrees(garrisonType, select(3, UnitClass("player")));
+  -- this is a talent that has completed, but has not been seen in the talent UI yet.
+  local completeTalentID = C_Garrison.GetCompleteTalent(garrisonType);
+  print('Talents:')
+  if (talentTrees) then
+    for treeIndex, tree in ipairs(talentTrees) do
+      for talentIndex, talent in ipairs(tree) do
+        local showTalent = false;
+        if (talent.isBeingResearched) then
+          showTalent = true;
+        end
+        if (talent.id == completeTalentID) then
+          showTalent = true;
+        end
+        if (showTalent) then
+          print(talent.name)
+          talent.creationTime = talent.researchStartTime
+          talent.duration = talent.researchDuration
+          talent.shipmentType = 'Talent: '
+          tinsert(self.shipments, talent)
+        end
       end
-      self.lastBlock = block
-      self.blocks[i] = block
     end
+  end
 
-    local r,g,b = 1, 1, 1
-    if data.isRare then
-      r,g,b = 0.1, 0.4, 1
+  self.profile.shipments = self.profile.shipments or {}
+  if #self.shipments >= 1 then
+
+    wipe(self.profile.shipments)
+    for index, shipment in ipairs(self.shipments) do
+      tinsert(self.profile.shipments, shipment)
     end
-    if data.isMine then
-      block.Icon:SetVertexColor(0,1,0,1)
-    else
-      block.Icon:SetVertexColor(1,1,1)
-    end
+    self.isStale = true
+  end
 
-
-    --block.missionData = data
-    block.missionID = data.missionID
-    block.missionEndTime = data.missionEndTime
-    block.Icon:SetAtlas(data.typeAtlas, false)
-    block.Label:SetText(data.name)
-    block.Label:SetTextColor(r, g, b)
-
-    if lastProfile ~= data.profileKey then
-      block.Owner:SetText(data.profileKey)
-      block.Owner:SetTextColor(data.classColor.r, data.classColor.g, data.classColor.b)
-    else
-      block.Owner:SetText(nil)
-    end
-    block.Background:SetColorTexture(data.classColor.r, data.classColor.g, data.classColor.b, 0.5)
-
-    block:Show()
-    lastProfile = data.profileKey
-  end
-  for i = numItems + 1, self.numBlocks do
-    if self.blocks[i] then
-      self.blocks[i]:Hide()
-    end
+  if self:IsVisible() then
+    self:Refresh()
   end
 end
 
-function core:OnShow()
-  if self.isStale then
-    print('updating items on show')
-    self:UpdateList()
-  end
-end
-
-function core:Refresh ()
+function core:UpdateItems ()
   if not self.profile then
     return
   end
-
   self.items = C_Garrison.GetLandingPageItems(LE_GARRISON_TYPE_7_0)
 
-  self.profile.missions = self.profile.missions or {}
 
-  self.profile.classColor = RAID_CLASS_COLORS[select(2, UnitClass('player'))]
 
 
-  print('|cFF0088FFLocal Scoop|r:', self.profileKey)
+  print('|cFF0088FFLandingPageItems|r:', self.profileKey)
   if #self.items >= 1 then
-    table.wipe(self.profile.missions)
+    wipe(self.profile.missions)
     for index, data in ipairs(self.items) do
       print('', data.name)
       print('  |cFF00FF00', data.timeLeft .. '|r', date("%A %I:%m %p", data.missionEndTime))
       tinsert(self.profile.missions, data)
     end
+    print('items update pending')
+    self.isStale = true
   end
 
   if self:IsVisible() then
-    self:UpdateList()
+    self:Refresh()
+  end
+end
+
+function block:OnComplete()
+  self.isComplete = true
+  self:Refresh()
+end
+
+local GetTimeLeftString = function(timeLeft)
+
+  local days = floor(timeLeft/(24*3600))
+  local hours = floor(mod(timeLeft, (24*3600)) / 3600)
+  local minutes = floor(mod(timeLeft, 3600) / 60)
+  local seconds = mod(timeLeft, 60)
+  if days >= 1 then
+    return (days .. 'd' .. ' ') .. ((hours > 0) and (hours .. 'h ') or '')
   else
-    print('items update pending')
-    self.isStale = true
+    return ((hours > 0) and (hours .. 'h ') or '') .. ((minutes > 0) and (minutes .. ' min') or '')
   end
 end
 
 function block:OnUpdate()
+  if self.isComplete then
+    return
+  end
+
   if self.missionEndTime then
     local timeLeft = self.missionEndTime - time()
     if timeLeft < 0 then
-      self.TimeLeft:SetText('Complete!')
+      self:OnComplete()
     else
-      local days = floor(timeLeft/(24*3600))
-      local hours = floor(mod(timeLeft, (24*3600)) / 3600)
-      local minutes = floor(mod(timeLeft, 3600) / 60)
-      local seconds = mod(timeLeft, 60)
-      self.TimeLeft:SetText(
-        ((days > 0) and (days .. ' d') or '') ..
-            ((hours > 0) and (' '.. hours .. ' hr') or '')..
-            ((minutes > 0) and (' ' .. minutes .. ' min' or ''))
-        , 1,1,1)
+      self.TimeLeft:SetText(GetTimeLeftString(timeLeft))
+
+      if timeLeft > 3600 then
+        self.TimeLeft:SetTextColor(1,1,1)
+      else
+        self.TimeLeft:SetTextColor(1,1,0)
+      end
+
     end
   else
     self.TimeLeft:SetText(self.missionEndTime)
   end
+end
+
+local SetClassColors = function(self, data)
+
+  if self.lastProfile ~= data.profileKey then
+    self.Owner:SetText(data.profileKey)
+    self.Owner:SetTextColor(data.classColor.r, data.classColor.g, data.classColor.b)
+  else
+    self.Owner:SetText(nil)
+  end
+  self.Background:SetColorTexture(data.classColor.r, data.classColor.g, data.classColor.b,
+    (data.isComplete and 0.5 or 0.1))
+end
+
+function block:Refresh(data)
+  data = data or self.data
+  self.data = data
+
+  local r,g,b = 1, 1, 1
+  if data.isRare then
+    r,g,b = 0.1, 0.4, 1
+  end
+
+
+  --self.missionData = data
+  self.Label:SetText(data.name)
+  self.Label:SetTextColor(r, g, b)
+
+  if #data.rewards >= 1 then
+    self.Icon:SetTexture(data.rewards[1].icon or GetItemIcon(data.rewards[1].itemID))
+    self.rewardInfo = data.rewards[1]
+  else
+    self.Icon:SetAtlas(data.typeAtlas, false)
+  end
+
+  SetClassColors(self, data)
+
+  if self.isComplete then
+    self.TimeLeft:SetText('Complete!')
+  end
+end
+
+
+function block:OnEnter()
+  if self.rewardInfo and self.rewardInfo.itemID then
+    GameTooltip:SetOwner(self, 'ANCHOR_LEFT')
+    GameTooltip:SetItemByID(self.rewardInfo.itemID)
+    GameTooltip:Show()
+  end
+end
+function block:OnLeave()
+  if GameTooltip:IsOwned(self) then
+    GameTooltip:Hide()
+  end
+end
+
+
+
+function shipment:Refresh(data)
+  data = data or self.data
+  self.Icon:SetTexture(data.icon)
+  self.data = data
+
+  self.Name:SetText(data.shipmentType .. data.name)
+  self.Count:SetText(data.shipmentsReady)
+
+  self.Done:SetShown(data.shipmentsReady and (data.shipmentsReady >= 1))
+
+  if ( data.shipmentsReady == data.shipmentsTotal ) then
+    self.Swipe:SetCooldownUNIX(0, 0);
+    self.Done:Show();
+    if not data.isBeingResearched then
+      data.isComplete = true
+    end
+  else
+    self.Swipe:SetCooldownUNIX(data.creationTime or 0 , data.duration or 0);
+  end
+
+
+
+  SetClassColors(self, data)
+end
+function shipment:UpdateShipment()
+
+  local data = self.data
+  if data.shipmentsTotal  then
+    local timeLeft = data.creationTime + data.duration - time()
+    if timeLeft < 0 then
+      local numReady = floor((1*timeLeft) / data.duration)
+      data.shipmentsReady = data.shipmentsReady + numReady
+      data.creationTime = data.creationTime + (numReady * data.duration)
+      self:Refresh()
+    end
+  end
+end
+function shipment:OnUpdate()
+  local data = self.data
+  if (data.shipmentsReady and data.shipmentsTotal) and (data.shipmentsReady ~= data.shipmentsTotal) then
+    local timeLeft = data.creationTime + data.duration - time()
+    if timeLeft < 0 then
+      self:UpdateShipment()
+      return
+    end
+
+    self.TimeLeft:SetText('Next: '.. GetTimeLeftString(timeLeft) .. ' |cFFFFFF00'..data.shipmentsTotal..' orders|r')
+
+
+  elseif data.isBeingResearched then
+    self.TimeLeft:SetText(GetTimeLeftString(data.researchStartTime + data.researchDuration - time()))
+  else
+    self.TimeLeft:SetText('Complete!')
+  end
+
+end
+
+function shipment:OnEnter()
+  local data = self.data
+  if ( data.shipmentsReady and data.shipmentsTotal ) then
+    GameTooltip:SetOwner(self, 'ANCHOR_LEFT')
+    GameTooltip:AddLine(data.shipmentsReady .. ' of '.. data.shipmentsTotal)
+    GameTooltip:Show()
+  end
+end
+
+function shipment:OnLeave()
+  if GameTooltip:IsOwned(self) then
+    GameTooltip:Hide()
+  end
+end
+
+function shipment:OnClick(button)
+  --todo: trigger cleanup script for dead shipment data
 end
\ No newline at end of file
--- a/ClassPlan.xml	Thu Oct 13 05:32:43 2016 -0400
+++ b/ClassPlan.xml	Thu Oct 13 09:08:38 2016 -0400
@@ -3,8 +3,11 @@
 
   <Script file="ClassPlan.lua" />
 
+  <Font name="ClassPlanFont" font="Interface\AddOns\Veneer\Font\ArchivoNarrow-Regular.ttf" height="14" outline="NORMAL" virtual="true" />
+  <Font name="ClassPlanNumberFont" font="Interface\AddOns\Veneer\Font\ArchivoNarrow-Bold.ttf" height="14" outline="NORMAL" virtual="true" />
+
   <Frame name="ClassOrderPlan" mixin="ClassOrderPlanCore" parent="UIParent" hidden="true">
-    <Size x="500" y="40" />
+    <Size x="600" y="40" />
     <Anchors>
       <Anchor point="TOP" />
     </Anchors>
@@ -31,6 +34,8 @@
     <Scripts>
       <OnUpdate method="OnUpdate" />
       <OnShow method="OnShow" />
+      <OnEnter method="OnEnter" />
+      <OnLeave method="OnLeave" />
     </Scripts>
     <Layers>
       <Layer level="BACKGROUND">
@@ -47,22 +52,101 @@
         </Texture>
       </Layer>
       <Layer level="OVERLAY">
-        <FontString name="$parentLabel" inherits="GameFontNormal" parentKey="Label" text="base text">
+        <FontString name="$parentLabel" inherits="ClassPlanFont" parentKey="Label" text="base text">
           <Anchors>
             <Anchor point="TOPLEFT" relativePoint="TOPRIGHT" relativeKey="$parent.Icon" x="4" y="0" />
           </Anchors>
         </FontString>
-        <FontString name="$parentTimeLeft" inherits="GameFontNormalSmall" parentKey="TimeLeft" text="base text">
+        <FontString name="$parentTimeLeft" inherits="ClassPlanNumberFont" parentKey="TimeLeft" text="base text">
           <Anchors>
             <Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT" relativeKey="$parent.Label" x="4" y="-2" />
           </Anchors>
         </FontString>
-        <FontString name="$parentOwner" inherits="GameFontNormalSmall" parentKey="Owner" text="base text">
+        <FontString name="$parentOwner" inherits="ClassPlanFont" parentKey="Owner" text="base text">
+          <Anchors>
+            <Anchor point="TOPRIGHT" />
+          </Anchors>
+        </FontString>
+      </Layer>
+      <Layer level="HIGHLIGHT">
+        <Texture setAllPoints="true" alphaMode="ADD">
+          <Color a="1" r="0.1" g="0.1" b="0.1" />
+        </Texture>
+      </Layer>
+    </Layers>
+  </Frame>
+
+  <Frame name="ClassPlanShipment" mixin="ClassPlanShipmentMixin" virtual="true">
+    <Scripts>
+      <OnUpdate method="OnUpdate" />
+      <OnShow method="OnShow" />
+      <OnEnter method="OnEnter" />
+      <OnLeave method="OnLeave" />
+    </Scripts>
+    <Size x="200" y="32" />
+    <Layers>
+      <Layer level="BACKGROUND">
+        <Texture parentKey="Background" setAllPoints="true" />
+      </Layer>
+      <Layer level="BACKGROUND" textureSubLevel="1">
+        <Texture parentKey="Icon" alpha="0.5" desaturated="true">
+          <Size x="30" y="30"/>
+          <Anchors>
+            <Anchor point="LEFT"/>
+          </Anchors>
+        </Texture>
+      </Layer>
+      <Layer level="BORDER">
+        <Texture parentKey="Done" atlas="GarrLanding-ShipmentCompleteGlow">
+          <Size x="32" y="32" />
+          <Anchors>
+            <Anchor point="LEFT"/>
+          </Anchors>
+        </Texture>
+      </Layer>
+      <Layer level="ARTWORK">
+        <FontString parentKey="Name" inherits="ClassPlanFont" justifyV="TOP" justifyH="CENTER">
+          <Anchors>
+            <Anchor point="TOPLEFT" relativePoint="TOPRIGHT" relativeKey="$parent.Icon" x="2" y="-2"/>
+          </Anchors>
+          <Color r=".75" g=".75" b=".73"/>
+        </FontString>
+      </Layer>
+      <Layer level="OVERLAY">
+        <FontString parentKey="TimeLeft" inherits="ClassPlanFont" justifyH="CENTER">
+          <Anchors>
+            <Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT" relativeKey="$parent.Name" x="0" y="-2"/>
+          </Anchors>
+        </FontString>
+        <FontString parentKey="Count" inherits="WorldPlanFont" justifyH="CENTER">
+          <Anchors>
+            <Anchor point="BOTTOM" relativeKey="$parent.Icon" x="0" y="0"/>
+          </Anchors>
+        </FontString>
+        <FontString name="$parentOwner" inherits="WorldPlanFont" parentKey="Owner" text="base text">
           <Anchors>
             <Anchor point="TOPRIGHT" />
           </Anchors>
         </FontString>
       </Layer>
     </Layers>
+    <Frames>
+      <Cooldown parentKey="Swipe" reverse="true" hideCountdownNumbers="true">
+        <Size x="32" y="32"/>
+        <Anchors>
+          <Anchor point="LEFT"/>
+        </Anchors>
+        <SwipeTexture file="Interface\Garrison\GarrLanding-TradeskillTimerFill"/>
+        <Scripts>
+          <OnCooldownStart>
+            self:Show()
+          </OnCooldownStart>
+          <OnCoolDownDone>
+            -- update via event
+            C_Garrison.RequestLandingPageShipmentInfo();
+          </OnCoolDownDone>
+        </Scripts>
+      </Cooldown>
+    </Frames>
   </Frame>
 </Ui>
\ No newline at end of file