2018-10-18 01:50:25 +08:00
|
|
|
var Undo = {
|
|
|
|
index: 0,
|
|
|
|
history: [],
|
|
|
|
initEdit: function(aspects) {
|
|
|
|
//Before
|
|
|
|
if (aspects && Undo.current_save) {
|
|
|
|
//This "before" is the same as the "after" of the previous step
|
|
|
|
if (Objector.equalKeys(aspects, Undo.current_save.aspects) && aspects.cubes !== selected) {
|
|
|
|
//return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Undo.current_save = new Undo.save(aspects)
|
|
|
|
},
|
|
|
|
finishEdit: function(action, aspects) {
|
2019-02-04 04:09:35 +08:00
|
|
|
if (!Undo.current_save) return;
|
2018-12-16 23:18:20 +08:00
|
|
|
aspects = aspects || Undo.current_save.aspects
|
2018-10-18 01:50:25 +08:00
|
|
|
//After
|
2019-02-04 04:09:35 +08:00
|
|
|
Blockbench.dispatchEvent('finish_edit', {aspects})
|
2018-10-18 01:50:25 +08:00
|
|
|
var entry = {
|
|
|
|
before: Undo.current_save,
|
2018-12-16 23:18:20 +08:00
|
|
|
post: new Undo.save(aspects),
|
2018-10-18 01:50:25 +08:00
|
|
|
action: action
|
|
|
|
}
|
|
|
|
Undo.current_save = entry.post
|
|
|
|
|
|
|
|
if (Undo.history.length-1 > Undo.index) {
|
|
|
|
Undo.history.length = Undo.index+1
|
|
|
|
}
|
|
|
|
|
|
|
|
Undo.history.push(entry)
|
|
|
|
|
|
|
|
if (Undo.history.length > settings.undo_limit.value) {
|
|
|
|
Undo.history.shift()
|
|
|
|
}
|
|
|
|
Undo.index = Undo.history.length
|
2018-12-16 23:18:20 +08:00
|
|
|
if (!aspects || !aspects.keep_saved) {
|
|
|
|
Prop.project_saved = false;
|
|
|
|
}
|
2019-02-04 04:09:35 +08:00
|
|
|
Blockbench.dispatchEvent('finished_edit', {aspects})
|
2019-04-08 00:53:33 +08:00
|
|
|
if (EditSession.active) {
|
|
|
|
EditSession.sendEdit(entry)
|
|
|
|
}
|
2018-10-18 01:50:25 +08:00
|
|
|
},
|
2019-01-09 22:54:35 +08:00
|
|
|
cancelEdit: function() {
|
|
|
|
if (!Undo.current_save) return;
|
|
|
|
outlines.children.length = 0
|
|
|
|
Undo.loadSave(Undo.current_save, new Undo.save(Undo.current_save.aspects))
|
|
|
|
delete Undo.current_save;
|
|
|
|
},
|
2019-04-08 00:53:33 +08:00
|
|
|
undo: function(remote) {
|
2018-10-18 01:50:25 +08:00
|
|
|
if (Undo.history.length <= 0 || Undo.index < 1) return;
|
|
|
|
|
|
|
|
Prop.project_saved = false;
|
|
|
|
Undo.index--;
|
|
|
|
|
|
|
|
var entry = Undo.history[Undo.index]
|
|
|
|
Undo.loadSave(entry.before, entry.post)
|
2019-04-08 00:53:33 +08:00
|
|
|
if (EditSession.active && remote !== true) {
|
|
|
|
EditSession.sendAll('command', 'undo')
|
|
|
|
}
|
2018-10-18 01:50:25 +08:00
|
|
|
console.log('Undo: '+entry.action)
|
2019-02-04 04:09:35 +08:00
|
|
|
Blockbench.dispatchEvent('undo', {entry})
|
2018-10-18 01:50:25 +08:00
|
|
|
},
|
2019-04-08 00:53:33 +08:00
|
|
|
redo: function(remote) {
|
2018-10-18 01:50:25 +08:00
|
|
|
if (Undo.history.length <= 0) return;
|
|
|
|
if (Undo.index >= Undo.history.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Prop.project_saved = false;
|
|
|
|
Undo.index++;
|
|
|
|
|
|
|
|
var entry = Undo.history[Undo.index-1]
|
|
|
|
Undo.loadSave(entry.post, entry.before)
|
2019-04-08 00:53:33 +08:00
|
|
|
if (EditSession.active && remote !== true) {
|
|
|
|
EditSession.sendAll('command', 'redo')
|
|
|
|
}
|
2018-10-18 01:50:25 +08:00
|
|
|
console.log('Redo: '+entry.action)
|
2019-02-04 04:09:35 +08:00
|
|
|
Blockbench.dispatchEvent('redo', {entry})
|
2018-10-18 01:50:25 +08:00
|
|
|
},
|
2019-04-08 00:53:33 +08:00
|
|
|
remoteEdit: function(entry) {
|
|
|
|
Undo.loadSave(entry.post, entry.before, 'session')
|
|
|
|
|
|
|
|
if (entry.save_history !== false) {
|
|
|
|
delete Undo.current_save;
|
|
|
|
Undo.history.push(entry)
|
|
|
|
if (Undo.history.length > settings.undo_limit.value) {
|
|
|
|
Undo.history.shift()
|
|
|
|
}
|
|
|
|
Undo.index = Undo.history.length
|
|
|
|
Prop.project_saved = false;
|
|
|
|
Blockbench.dispatchEvent('finished_edit', {remote: true})
|
|
|
|
}
|
|
|
|
},
|
2018-10-18 01:50:25 +08:00
|
|
|
getItemByUUID: function(list, uuid) {
|
|
|
|
if (!list || typeof list !== 'object' || !list.length) {return false;}
|
|
|
|
var i = 0;
|
|
|
|
while (i < list.length) {
|
|
|
|
if (list[i].uuid === uuid) {
|
|
|
|
return list[i]
|
|
|
|
}
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
save: function(aspects) {
|
|
|
|
var scope = this;
|
|
|
|
this.aspects = aspects
|
|
|
|
|
|
|
|
if (aspects.selection) {
|
|
|
|
this.selection = []
|
|
|
|
selected.forEach(function(obj) {
|
|
|
|
scope.selection.push(obj.uuid)
|
|
|
|
})
|
|
|
|
if (selected_group) {
|
|
|
|
this.selection_group = selected_group.uuid
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-03 02:37:06 +08:00
|
|
|
if (aspects.cubes) {
|
2018-10-18 01:50:25 +08:00
|
|
|
this.cubes = {}
|
|
|
|
aspects.cubes.forEach(function(obj) {
|
2019-04-08 00:53:33 +08:00
|
|
|
var copy = new Cube(obj)
|
2018-10-18 01:50:25 +08:00
|
|
|
if (aspects.uv_only) {
|
|
|
|
copy = {
|
|
|
|
uv_offset: copy.uv_offset,
|
|
|
|
faces: copy.faces,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
copy.uuid = obj.uuid
|
2019-04-08 00:53:33 +08:00
|
|
|
delete copy.parent;
|
2018-10-18 01:50:25 +08:00
|
|
|
scope.cubes[obj.uuid] = copy
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-12-03 02:37:06 +08:00
|
|
|
if (aspects.outliner) {
|
2018-10-18 01:50:25 +08:00
|
|
|
this.outliner = compileGroups(true)
|
|
|
|
}
|
|
|
|
|
2018-12-03 02:37:06 +08:00
|
|
|
if (aspects.group) {
|
2018-10-18 01:50:25 +08:00
|
|
|
this.group = aspects.group.getChildlessCopy()
|
|
|
|
this.group.uuid = aspects.group.uuid
|
|
|
|
}
|
|
|
|
|
2018-12-03 02:37:06 +08:00
|
|
|
if (aspects.textures) {
|
2018-10-18 01:50:25 +08:00
|
|
|
this.textures = {}
|
|
|
|
aspects.textures.forEach(function(t) {
|
|
|
|
var tex = t.getUndoCopy(aspects.bitmap)
|
|
|
|
scope.textures[t.uuid] = tex
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aspects.settings) {
|
|
|
|
this.settings = aspects.settings
|
|
|
|
}
|
|
|
|
|
2018-12-03 02:37:06 +08:00
|
|
|
if (aspects.resolution) {
|
2018-10-18 01:50:25 +08:00
|
|
|
this.resolution = {
|
|
|
|
width: Project.texture_width,
|
|
|
|
height: Project.texture_height
|
|
|
|
}
|
|
|
|
}
|
2018-12-03 02:37:06 +08:00
|
|
|
|
2019-04-08 00:53:33 +08:00
|
|
|
if (aspects.animations) {
|
|
|
|
this.animations = {}
|
|
|
|
aspects.animations.forEach(a => {
|
|
|
|
scope.animations[a.uuid] = a.undoCopy();
|
|
|
|
})
|
2018-12-03 02:37:06 +08:00
|
|
|
}
|
|
|
|
if (aspects.keyframes && Animator.selected && Animator.selected.getBoneAnimator()) {
|
|
|
|
this.keyframes = {
|
|
|
|
animation: Animator.selected.uuid,
|
|
|
|
bone: Animator.selected.getBoneAnimator().uuid
|
|
|
|
}
|
|
|
|
aspects.keyframes.forEach(kf => {
|
|
|
|
scope.keyframes[kf.uuid] = kf.undoCopy()
|
|
|
|
})
|
|
|
|
}
|
2018-12-27 21:03:04 +08:00
|
|
|
|
|
|
|
if (aspects.display_slots) {
|
|
|
|
scope.display_slots = {}
|
|
|
|
aspects.display_slots.forEach(slot => {
|
2019-01-09 22:54:35 +08:00
|
|
|
if (display[slot]) {
|
|
|
|
scope.display_slots[slot] = display[slot].copy()
|
|
|
|
} else {
|
|
|
|
scope.display_slots[slot] = null
|
|
|
|
}
|
2018-12-27 21:03:04 +08:00
|
|
|
})
|
|
|
|
}
|
2018-10-18 01:50:25 +08:00
|
|
|
},
|
2019-04-08 00:53:33 +08:00
|
|
|
loadSave: function(save, reference, mode) {
|
|
|
|
var is_session = mode === 'session';
|
2018-10-18 01:50:25 +08:00
|
|
|
if (save.cubes) {
|
|
|
|
for (var uuid in save.cubes) {
|
|
|
|
if (save.cubes.hasOwnProperty(uuid)) {
|
|
|
|
var data = save.cubes[uuid]
|
2019-01-09 22:54:35 +08:00
|
|
|
var obj = elements.findInArray('uuid', uuid)
|
2018-10-18 01:50:25 +08:00
|
|
|
if (obj) {
|
|
|
|
for (var face in obj.faces) {
|
2019-02-04 04:09:35 +08:00
|
|
|
obj.faces[face].reset()
|
2018-10-18 01:50:25 +08:00
|
|
|
}
|
|
|
|
obj.extend(data)
|
|
|
|
Canvas.adaptObjectPosition(obj)
|
|
|
|
Canvas.adaptObjectFaces(obj)
|
|
|
|
Canvas.updateUV(obj)
|
|
|
|
} else {
|
2019-03-10 05:06:35 +08:00
|
|
|
obj = new Cube(data, uuid).init()
|
2018-10-18 01:50:25 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (var uuid in reference.cubes) {
|
|
|
|
if (reference.cubes.hasOwnProperty(uuid) && !save.cubes.hasOwnProperty(uuid)) {
|
2019-01-09 22:54:35 +08:00
|
|
|
var obj = elements.findInArray('uuid', uuid)
|
2018-10-18 01:50:25 +08:00
|
|
|
if (obj) {
|
2019-04-08 00:53:33 +08:00
|
|
|
obj.remove()
|
2018-10-18 01:50:25 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
loadOutlinerDraggable()
|
|
|
|
Canvas.updateVisibility()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (save.outliner) {
|
|
|
|
selected_group = undefined
|
|
|
|
parseGroups(save.outliner)
|
2019-04-08 00:53:33 +08:00
|
|
|
if (is_session) {
|
|
|
|
function iterate(arr) {
|
|
|
|
arr.forEach((obj) => {
|
|
|
|
delete obj.isOpen;
|
|
|
|
if (obj.children) {
|
|
|
|
iterate(obj.children)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
iterate(save.outliner)
|
|
|
|
}
|
2018-10-18 01:50:25 +08:00
|
|
|
if (Blockbench.entity_mode) {
|
|
|
|
Canvas.updateAllPositions()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-08 00:53:33 +08:00
|
|
|
if (save.selection_group && !is_session) {
|
2018-10-18 01:50:25 +08:00
|
|
|
selected_group = undefined
|
|
|
|
var sel_group = TreeElements.findRecursive('uuid', save.selection_group)
|
|
|
|
if (sel_group) {
|
|
|
|
sel_group.select()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-08 00:53:33 +08:00
|
|
|
if (save.selection && !is_session) {
|
2018-10-18 01:50:25 +08:00
|
|
|
selected.length = 0;
|
|
|
|
elements.forEach(function(obj) {
|
|
|
|
if (save.selection.includes(obj.uuid)) {
|
|
|
|
selected.push(obj)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if (save.group) {
|
|
|
|
var group = TreeElements.findRecursive('uuid', save.group.uuid)
|
|
|
|
if (group) {
|
2019-04-08 00:53:33 +08:00
|
|
|
if (is_session) {
|
|
|
|
delete save.group.isOpen;
|
|
|
|
}
|
2018-10-18 01:50:25 +08:00
|
|
|
group.extend(save.group)
|
|
|
|
if (Blockbench.entity_mode) {
|
|
|
|
group.forEachChild(function(obj) {
|
|
|
|
if (obj.type === 'cube') {
|
|
|
|
Canvas.adaptObjectPosition(obj)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (save.textures) {
|
|
|
|
Painter.current = {}
|
|
|
|
for (var uuid in save.textures) {
|
|
|
|
if (reference.textures[uuid]) {
|
|
|
|
var tex = Undo.getItemByUUID(textures, uuid)
|
|
|
|
if (tex) {
|
2019-04-08 00:53:33 +08:00
|
|
|
var require_reload = tex.mode !== save.textures[uuid].mode;
|
2018-10-18 01:50:25 +08:00
|
|
|
tex.extend(save.textures[uuid]).updateMaterial()
|
2019-04-08 00:53:33 +08:00
|
|
|
if (require_reload || reference.textures[uuid] === true) {
|
|
|
|
tex.load()
|
|
|
|
} else {
|
|
|
|
tex.updateMaterial()
|
|
|
|
}
|
2018-10-18 01:50:25 +08:00
|
|
|
}
|
|
|
|
} else {
|
2019-04-08 00:53:33 +08:00
|
|
|
var tex = new Texture(save.textures[uuid], uuid)
|
|
|
|
tex.load().add(false)
|
2018-10-18 01:50:25 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for (var uuid in reference.textures) {
|
|
|
|
if (!save.textures[uuid]) {
|
|
|
|
var tex = Undo.getItemByUUID(textures, uuid)
|
|
|
|
if (tex) {
|
|
|
|
textures.splice(textures.indexOf(tex), 1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Canvas.updateAllFaces()
|
|
|
|
}
|
|
|
|
if (save.settings) {
|
|
|
|
for (var key in save.settings) {
|
|
|
|
settings[key].value = save.settings[key]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (save.resolution) {
|
|
|
|
Project.texture_width = save.resolution.width
|
|
|
|
Project.texture_height = save.resolution.height
|
|
|
|
}
|
2018-12-03 02:37:06 +08:00
|
|
|
|
2019-04-08 00:53:33 +08:00
|
|
|
if (save.animations) {
|
|
|
|
for (var uuid in save.animations) {
|
2018-12-03 02:37:06 +08:00
|
|
|
|
2019-04-08 00:53:33 +08:00
|
|
|
var animation = reference.animations[uuid] ? Undo.getItemByUUID(Animator.animations, uuid) : null;
|
|
|
|
if (!animation) {
|
|
|
|
animation = new Animation()
|
|
|
|
animation.uuid = uuid
|
|
|
|
}
|
|
|
|
animation.extend(save.animations[uuid]).add(false)
|
|
|
|
if (save.animations[uuid].selected) {
|
|
|
|
animation.select()
|
|
|
|
}
|
2018-12-03 02:37:06 +08:00
|
|
|
}
|
2019-04-08 00:53:33 +08:00
|
|
|
for (var uuid in reference.animations) {
|
|
|
|
if (!save.animations[uuid]) {
|
|
|
|
var animation = Undo.getItemByUUID(Animator.animations, uuid)
|
|
|
|
if (animation) {
|
|
|
|
animation.remove(false)
|
|
|
|
}
|
|
|
|
}
|
2018-12-03 02:37:06 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-08 00:53:33 +08:00
|
|
|
if (save.keyframes) {
|
|
|
|
var animation = Animator.selected;
|
|
|
|
if (!animation || animation.uuid !== save.keyframes.animation) {
|
2018-12-03 02:37:06 +08:00
|
|
|
animation = Animator.animations.findInArray('uuid', save.keyframes.animation)
|
2019-04-08 00:53:33 +08:00
|
|
|
if (animation.select && Animator.open && is_session) {
|
2019-01-09 22:54:35 +08:00
|
|
|
animation.select()
|
|
|
|
}
|
2018-12-03 02:37:06 +08:00
|
|
|
}
|
2019-04-08 00:53:33 +08:00
|
|
|
if (animation) {
|
|
|
|
var bone = Animator.selected.getBoneAnimator();
|
|
|
|
if (!bone || bone.uuid !== save.keyframes.bone) {
|
|
|
|
for (var uuid in Animator.selected.bones) {
|
|
|
|
if (uuid === save.keyframes.bone) {
|
|
|
|
bone = Animator.selected.bones[uuid]
|
|
|
|
if (bone.select && Animator.open && is_session) {
|
|
|
|
bone.select()
|
|
|
|
}
|
|
|
|
}
|
2019-01-09 22:54:35 +08:00
|
|
|
}
|
|
|
|
}
|
2019-04-08 00:53:33 +08:00
|
|
|
if (bone) {
|
2018-12-03 02:37:06 +08:00
|
|
|
|
2019-04-08 00:53:33 +08:00
|
|
|
function getKeyframe(uuid) {
|
|
|
|
var i = 0;
|
|
|
|
while (i < Timeline.keyframes.length) {
|
|
|
|
if (Timeline.keyframes[i].uuid === uuid) {
|
|
|
|
return Timeline.keyframes[i];
|
|
|
|
}
|
|
|
|
i++;
|
|
|
|
}
|
2018-12-03 02:37:06 +08:00
|
|
|
}
|
2019-04-08 00:53:33 +08:00
|
|
|
var added = 0;
|
|
|
|
for (var uuid in save.keyframes) {
|
|
|
|
if (uuid.length === 36 && save.keyframes.hasOwnProperty(uuid)) {
|
|
|
|
var data = save.keyframes[uuid]
|
|
|
|
var kf = getKeyframe(uuid)
|
|
|
|
if (kf) {
|
|
|
|
kf.extend(data)
|
|
|
|
} else {
|
|
|
|
kf = new Keyframe(data)
|
|
|
|
kf.parent = bone;
|
|
|
|
kf.uuid = uuid;
|
|
|
|
Timeline.keyframes.push(kf)
|
|
|
|
added++;
|
|
|
|
}
|
|
|
|
}
|
2018-12-03 02:37:06 +08:00
|
|
|
}
|
2019-04-08 00:53:33 +08:00
|
|
|
for (var uuid in reference.keyframes) {
|
|
|
|
if (uuid.length === 36 && reference.keyframes.hasOwnProperty(uuid) && !save.keyframes.hasOwnProperty(uuid)) {
|
|
|
|
var kf = getKeyframe(uuid)
|
|
|
|
if (kf) {
|
|
|
|
kf.remove()
|
|
|
|
}
|
|
|
|
}
|
2018-12-03 02:37:06 +08:00
|
|
|
}
|
2019-04-08 00:53:33 +08:00
|
|
|
if (added) {
|
|
|
|
Vue.nextTick(Timeline.update)
|
|
|
|
}
|
|
|
|
updateKeyframeSelection()
|
|
|
|
Animator.preview()
|
2018-12-03 02:37:06 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-12-27 21:03:04 +08:00
|
|
|
|
|
|
|
if (save.display_slots) {
|
|
|
|
for (var slot in save.display_slots) {
|
2019-01-09 22:54:35 +08:00
|
|
|
var data = save.display_slots[slot]
|
|
|
|
|
|
|
|
if (!display[slot] && data) {
|
2018-12-27 21:03:04 +08:00
|
|
|
display[slot] = new DisplaySlot()
|
2019-01-09 22:54:35 +08:00
|
|
|
} else if (data === null && display[slot]) {
|
|
|
|
display[slot].default()
|
2018-12-27 21:03:04 +08:00
|
|
|
}
|
2019-01-09 22:54:35 +08:00
|
|
|
display[slot].extend(data).update()
|
2018-12-27 21:03:04 +08:00
|
|
|
}
|
|
|
|
}
|
2019-03-10 05:06:35 +08:00
|
|
|
if (open_dialog == 'uv_dialog') {
|
|
|
|
for (var key in uv_dialog.editors) {
|
|
|
|
if (uv_dialog.editors[key]) {
|
|
|
|
uv_dialog.editors[key].loadData()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-10-18 01:50:25 +08:00
|
|
|
updateSelection()
|
|
|
|
}
|
2018-12-27 21:03:04 +08:00
|
|
|
}
|
|
|
|
Undo.save.prototype.addTexture = function(texture) {
|
|
|
|
if (!this.textures) return;
|
|
|
|
if (this.aspects.textures.safePush(texture)) {
|
|
|
|
this.textures[texture.uuid] = texture.getUndoCopy(this.aspects.bitmap)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
BARS.defineActions(function() {
|
|
|
|
|
|
|
|
new Action({
|
|
|
|
id: 'undo',
|
|
|
|
icon: 'undo',
|
|
|
|
category: 'edit',
|
2019-03-10 05:06:35 +08:00
|
|
|
condition: () => (!open_dialog || open_dialog === 'uv_dialog'),
|
|
|
|
work_in_dialog: true,
|
2018-12-27 21:03:04 +08:00
|
|
|
keybind: new Keybind({key: 90, ctrl: true}),
|
2019-03-10 05:06:35 +08:00
|
|
|
click: Undo.undo
|
2018-12-27 21:03:04 +08:00
|
|
|
})
|
|
|
|
new Action({
|
|
|
|
id: 'redo',
|
|
|
|
icon: 'redo',
|
|
|
|
category: 'edit',
|
2019-03-10 05:06:35 +08:00
|
|
|
condition: () => (!open_dialog || open_dialog === 'uv_dialog'),
|
|
|
|
work_in_dialog: true,
|
2018-12-27 21:03:04 +08:00
|
|
|
keybind: new Keybind({key: 89, ctrl: true}),
|
2019-03-10 05:06:35 +08:00
|
|
|
click: Undo.redo
|
2018-12-27 21:03:04 +08:00
|
|
|
})
|
|
|
|
})
|