diff --git a/area.js b/area.js index 85fa316..4bdb9a5 100644 --- a/area.js +++ b/area.js @@ -1129,14 +1129,7 @@ var DrawingArea = new Lang.Class({ } content += "\n"; - let filename = `${Me.metadata['svg-file-name']} ${Files.getDateString()}.svg`; - let dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); - let path = GLib.build_filenamev([dir, filename]); - if (GLib.file_test(path, GLib.FileTest.EXISTS)) - return false; - let success = GLib.file_set_contents(path, content); - - if (success) { + if (Files.saveSvg(content)) { // pass the parent (bgContainer) to Flashspot because coords of this are relative let flashspot = new Screenshot.Flashspot(this.get_parent()); flashspot.fire(); @@ -1149,7 +1142,7 @@ var DrawingArea = new Lang.Class({ } }, - _saveAsJson: function(name, notify, callback) { + _saveAsJson: function(json, notify, callback) { // stop drawing or writing if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { this._stopWriting(); @@ -1157,46 +1150,31 @@ var DrawingArea = new Lang.Class({ this._stopDrawing(); } - let json = new Files.Json({ name }); - let oldContents; - - if (name == Me.metadata['persistent-file-name']) { - let oldContents = json.contents; - // do not create a file to write just an empty array - if (!oldContents && this.elements.length == 0) - return; - } - // do not use "content = JSON.stringify(this.elements, null, 2);", neither "content = JSON.stringify(this.elements);" // do compromise between disk usage and human readability - let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; - - if (name == Me.metadata['persistent-file-name'] && contents == oldContents) - return; + let contents = this.elements.length ? `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]` : '[]'; GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { json.contents = contents; if (notify) - this.emit('show-osd', Files.Icons.SAVE, name, "", -1, false); - if (name != Me.metadata['persistent-file-name']) { - this.jsonName = name; - this.lastJsonContents = contents; - } + this.emit('show-osd', Files.Icons.SAVE, json.name, "", -1, false); + if (!json.isPersistent) + this.currentJson = json; if (callback) callback(); }); }, saveAsJsonWithName: function(name, callback) { - this._saveAsJson(name, false, callback); + this._saveAsJson(Files.Jsons.getNamed(name), false, callback); }, saveAsJson: function() { - this._saveAsJson(Files.getDateString(), true); + this._saveAsJson(Files.Jsons.getDated(), true); }, savePersistent: function() { - this._saveAsJson(Me.metadata['persistent-file-name']); + this._saveAsJson(Files.Jsons.getPersistent()); }, syncPersistent: function() { @@ -1208,7 +1186,7 @@ var DrawingArea = new Lang.Class({ }, - _loadJson: function(name, notify) { + _loadJson: function(json, notify) { // stop drawing or writing if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { this._stopWriting(); @@ -1218,56 +1196,45 @@ var DrawingArea = new Lang.Class({ this.elements = []; this.currentElement = null; - let contents = (new Files.Json({ name })).contents; - if (!contents) + if (!json.contents) return; - this.elements.push(...JSON.parse(contents).map(object => { + this.elements.push(...JSON.parse(json.contents).map(object => { if (object.image) object.image = new Files.Image(object.image); return new Elements.DrawingElement(object); })); if (notify) - this.emit('show-osd', Files.Icons.OPEN, name, "", -1, false); - if (name != Me.metadata['persistent-file-name']) { - this.jsonName = name; - this.lastJsonContents = contents; - } + this.emit('show-osd', Files.Icons.OPEN, json.name, "", -1, false); + if (!json.isPersistent) + this.currentJson = json; }, _loadPersistent: function() { - this._loadJson(Me.metadata['persistent-file-name']); + this._loadJson(Files.Jsons.getPersistent()); }, - loadJson: function(name, notify) { - this._loadJson(name, notify); + loadJson: function(json, notify) { + this._loadJson(json, notify); this._redisplay(); }, loadPreviousJson: function() { - let names = Files.getJsons().map(json => json.name); - - if (!names.length) - return; - - let previousName = names[this.jsonName && names.indexOf(this.jsonName) != names.length - 1 ? names.indexOf(this.jsonName) + 1 : 0]; - this.loadJson(previousName, true); + let json = Files.Jsons.getPrevious(this.currentJson || null); + if (json) + this.loadJson(json, true); }, loadNextJson: function() { - let names = Files.getJsons().map(json => json.name); - - if (!names.length) - return; - - let nextName = names[this.jsonName && names.indexOf(this.jsonName) > 0 ? names.indexOf(this.jsonName) - 1 : names.length - 1]; - this.loadJson(nextName, true); + let json = Files.Jsons.getNext(this.currentJson || null); + if (json) + this.loadJson(json, true); }, get drawingContentsHasChanged() { let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; - return contents != this.lastJsonContents; + return contents != (this.currentJson && this.currentJson.contents); } }); diff --git a/extension.js b/extension.js index 95c1dd7..ca34b88 100644 --- a/extension.js +++ b/extension.js @@ -524,6 +524,8 @@ const AreaManager = new Lang.Class({ Main.wm.removeKeybinding('toggle-modal'); Main.wm.removeKeybinding('erase-drawings'); this.removeAreas(); + Files.Images.disable(); + Files.Jsons.disable(); if (this.indicator) this.indicator.disable(); } diff --git a/files.js b/files.js index 15c2b91..a443c8b 100644 --- a/files.js +++ b/files.js @@ -1,5 +1,5 @@ /* jslint esversion: 6 */ -/* exported Icons, Image, Images, Json, getJsons, getDateString */ +/* exported Icons, Image, Images, Json, Jsons, getDateString, saveSvg */ /* * Copyright 2019 Abakkk @@ -205,6 +205,12 @@ var Images = { _clipboardImages: [], _upToDate: false, + disable: function() { + this._images = []; + this._clipboardImages = []; + this._upToDate = false; + }, + _clipboardImagesContains: function(file) { return this._clipboardImages.some(image => image.file.equal(file)); }, @@ -265,7 +271,7 @@ var Images = { let file = this.enumerator.get_child(info); if (info.get_content_type().indexOf('image') == 0 && !clipboardImagesContains(file)) { - let image = oldImages.find(image => image.file.equal(file)) || new ImageWithGicon({ file, info }); + let image = oldImages.find(oldImage => oldImage.file.equal(file)) || new ImageWithGicon({ file, info }); newImages.push(image); return { value: image, done: false }; } else { @@ -287,7 +293,7 @@ var Images = { getPrevious: function(currentImage) { let images = this.getSorted(); - let index = currentImage ? images.findIndex(image => image.file.equal(currentImage.file)) : 0; + let index = currentImage ? images.findIndex(image => image.file.equal(currentImage.file)) : -1; return images[index <= 0 ? images.length - 1 : index - 1] || null; }, @@ -333,6 +339,10 @@ var Json = new Lang.Class({ this[key] = params[key]; }, + get isPersistent() { + return this.name == Me.metadata['persistent-file-name']; + }, + toString: function() { return this.displayName || this.name; }, @@ -342,10 +352,10 @@ var Json = new Lang.Class({ }, get file() { - if (!this._file && this.name) + if (!this._file) this._file = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], `${this.name}.json`])); - return this._file || null; + return this._file; }, set file(file) { @@ -353,62 +363,167 @@ var Json = new Lang.Class({ }, get contents() { - let success_, contents; - try { - [success_, contents] = this.file.load_contents(null); - if (contents instanceof Uint8Array) - contents = ByteArray.toString(contents); - } catch(e) { - return null; + if (this._contents === undefined) { + try { + [, this._contents] = this.file.load_contents(null); + if (this._contents instanceof Uint8Array) + this._contents = ByteArray.toString(this._contents); + } catch(e) { + this._contents = null; + } } - return contents; + + return this._contents; }, set contents(contents) { + if (this.isPersistent && (this.contents == contents || !this.contents && contents == '[]')) + return; + try { this.file.replace_contents(contents, null, false, Gio.FileCreateFlags.NONE, null); } catch(e) { this.file.get_parent().make_directory_with_parents(null); this.file.replace_contents(contents, null, false, Gio.FileCreateFlags.NONE, null); } + + this._contents = contents; } }); -var getJsons = function() { - let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); +var Jsons = { + _jsons: [], + _upToDate: false, - let enumerator; - try { - enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); - } catch(e) { - return []; - } - - let jsons = []; - let fileInfo = enumerator.next_file(null); - while (fileInfo) { - if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${Me.metadata['persistent-file-name']}.json`) { - let file = enumerator.get_child(fileInfo); - jsons.push(new Json({ - file, - name: fileInfo.get_name().slice(0, -5), - displayName: fileInfo.get_display_name().slice(0, -5), - // fileInfo.get_modification_date_time: Gio 2.62+ - modificationUnixTime: fileInfo.get_attribute_uint64('time::modified') - })); + disable: function() { + if (this._monitor) { + this._monitor.disconnect(this._monitorHandler); + this._monitor.cancel(); } - fileInfo = enumerator.next_file(null); + + delete this._monitor; + delete this._persistent; + + this._jsons = []; + this._upToDate = false; + }, + + _updateMonitor: function() { + if (this._monitor) + return; + + let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); + this._monitor = directory.monitor(Gio.FileMonitorFlags.NONE, null); + this._monitorHandler = this._monitor.connect('changed', (monitor, file) => { + if (file.get_basename() != `${Me.metadata['persistent-file-name']}.json` && file.get_basename().indexOf('.goutputstream')) + this.reset(); + }); + }, + + [Symbol.iterator]: function() { + if (this._upToDate) + return this._jsons[Symbol.iterator](); + + this._updateMonitor(); + this._upToDate = true; + let newJsons = this._jsons = []; + + return { + get enumerator() { + if (this._enumerator === undefined) { + try { + let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); + this._enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); + } catch(e) { + this._enumerator = null; + } + } + + return this._enumerator; + }, + + next: function() { + if (!this.enumerator || this.enumerator.is_closed()) + return { done: true }; + + let info = this.enumerator.next_file(null); + if (!info) { + this.enumerator.close(null); + return this.next(); + } + + let file = this.enumerator.get_child(info); + + if (info.get_content_type().indexOf('json') != -1 && info.get_name() != `${Me.metadata['persistent-file-name']}.json`) { + let json = new Json({ + file, name: info.get_name().slice(0, -5), + displayName: info.get_display_name().slice(0, -5), + // info.get_modification_date_time: Gio 2.62+ + modificationUnixTime: info.get_attribute_uint64('time::modified') + }); + + newJsons.push(json); + return { value: json, done: false }; + } else { + return this.next(); + } + } + }; + }, + + getSorted: function() { + return [...this].sort((a, b) => b.modificationUnixTime - a.modificationUnixTime); + }, + + getNext: function(currentJson) { + let jsons = this.getSorted(); + let index = currentJson ? jsons.findIndex(json => json.name == currentJson.name) : -1; + return jsons[index == jsons.length - 1 ? 0 : index + 1] || null; + }, + + getPrevious: function(currentJson) { + let jsons = this.getSorted(); + let index = currentJson ? jsons.findIndex(json => json.name == currentJson.name) : -1; + return jsons[index <= 0 ? jsons.length - 1 : index - 1] || null; + }, + + getPersistent: function() { + if (!this._persistent) + this._persistent = new Json({ name: Me.metadata['persistent-file-name'] }); + + return this._persistent; + }, + + getDated: function() { + return new Json({ name: getDateString() }); + }, + + getNamed: function(name) { + return [...this].find(json => json.name == name) || new Json({ name }); + }, + + reset: function() { + this._upToDate = false; } - enumerator.close(null); - - jsons.sort((a, b) => { - return b.modificationUnixTime - a.modificationUnixTime; - }); - - return jsons; }; var getDateString = function() { let date = GLib.DateTime.new_now_local(); return `${date.format("%F")} ${date.format("%X")}`; }; + +var saveSvg = function(content) { + let filename = `${Me.metadata['svg-file-name']} ${getDateString()}.svg`; + let dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); + let path = GLib.build_filenamev([dir, filename]); + let file = Gio.File.new_for_path(path); + if (file.query_exists(null)) + return false; + + try { + return file.replace_contents(content, null, false, Gio.FileCreateFlags.NONE, null); + } catch(e) { + return false; + } +}; + diff --git a/menu.js b/menu.js index bc10dfa..c460073 100644 --- a/menu.js +++ b/menu.js @@ -562,10 +562,10 @@ var DrawingMenu = new Lang.Class({ }, _updateDrawingNameMenuItem: function() { - getActor(this.drawingNameMenuItem).visible = this.area.jsonName ? true : false; - if (this.area.jsonName) { + getActor(this.drawingNameMenuItem).visible = this.area.currentJson ? true : false; + if (this.area.currentJson) { let prefix = this.area.drawingContentsHasChanged ? "* " : ""; - this.drawingNameMenuItem.label.set_text(`${prefix}${this.area.jsonName}`); + this.drawingNameMenuItem.label.set_text(`${prefix}${this.area.currentJson.name}`); this.drawingNameMenuItem.label.get_clutter_text().set_use_markup(true); } }, @@ -574,7 +574,7 @@ var DrawingMenu = new Lang.Class({ let item = new PopupMenu.PopupSubMenuMenuItem(_("Open drawing"), true); this.openDrawingSubMenuItem = item; this.openDrawingSubMenu = item.menu; - item.setSensitive(Boolean(Files.getJsons().length)); + item.setSensitive(Boolean(Files.Jsons.getSorted().length)); item.icon.set_gicon(icon); item.menu.itemActivated = item.menu.close; @@ -591,10 +591,10 @@ var DrawingMenu = new Lang.Class({ _populateOpenDrawingSubMenu: function() { this.openDrawingSubMenu.removeAll(); - let jsons = Files.getJsons(); + let jsons = Files.Jsons.getSorted(); jsons.forEach(json => { let subItem = this.openDrawingSubMenu.addAction(`${String(json)}`, () => { - this.area.loadJson(json.name); + this.area.loadJson(json); this._updateDrawingNameMenuItem(); this._updateSaveDrawingSubMenuItemSensitivity(); });