diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9e179ef
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+.DS_Store
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+
+.marginalia
+
+js/
+build/
+coverage/
\ No newline at end of file
diff --git a/Makefile b/Makefile
index f14fa66..4806724 100644
--- a/Makefile
+++ b/Makefile
@@ -1,51 +1,52 @@
-app_name=gadgetbridge
-project_dir=$(CURDIR)/../$(app_name)
-build_dir=$(CURDIR)/build/artifacts
-appstore_dir=$(build_dir)/appstore
-source_dir=$(build_dir)/source
-sign_dir=$(build_dir)/sign
-package_name=$(app_name)
-cert_dir=$(HOME)/.nextcloud/certificates
-version+=1.0.0
+all: dev-setup lint build-js-production test
-all: appstore
+# Dev env management
+dev-setup: clean clean-dev npm-init
-release: appstore create-tag
+npm-init:
+ yarn ci
-create-tag:
- git tag -s -a v$(version) -m "Tagging the $(version) release."
- git push origin v$(version)
+npm-update:
+ yarn update
+# Building
+build-js:
+ yarn run dev
+
+build-js-production:
+ yarn run build
+
+watch-js:
+ yarn run watch
+
+# Testing
+test:
+ yarn run test
+
+test-watch:
+ yarn run test:watch
+
+test-coverage:
+ yarn run test:coverage
+
+# Linting
+lint:
+ yarn run lint
+
+lint-fix:
+ yarn run lint:fix
+
+# Style linting
+stylelint:
+ yarn run stylelint
+
+stylelint-fix:
+ yarn run stylelint:fix
+
+# Cleaning
clean:
- rm -rf $(build_dir)
- rm -rf node_modules
-
-appstore: clean
- mkdir -p $(sign_dir)
- rsync -a \
- --exclude=/build \
- --exclude=/docs \
- --exclude=/l10n/templates \
- --exclude=/l10n/.tx \
- --exclude=/tests \
- --exclude=/.git \
- --exclude=/screenshots \
- --exclude=/.github \
- --exclude=/l10n/l10n.pl \
- --exclude=/CONTRIBUTING.md \
- --exclude=/issue_template.md \
- --exclude=/README.md \
- --exclude=/.gitattributes \
- --exclude=/.gitignore \
- --exclude=/.scrutinizer.yml \
- --exclude=/.travis.yml \
- --exclude=/.drone.yml \
- --exclude=/Makefile \
- $(project_dir)/ $(sign_dir)/$(app_name)
- tar -czf $(build_dir)/$(app_name)-$(version).tar.gz \
- -C $(sign_dir) $(app_name)
- @if [ -f $(cert_dir)/$(app_name).key ]; then \
- echo "Signing packageā¦"; \
- openssl dgst -sha512 -sign $(cert_dir)/$(app_name).key $(build_dir)/$(app_name)-$(version).tar.gz | openssl base64; \
- fi
+ rm -f js/*
+
+clean-dev:
+ rm -rf node_modules
\ No newline at end of file
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 75d26a7..c9a26a0 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -18,7 +18,7 @@
GadgetBridge
-
+
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000..7a5d71e
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ plugins: ['@babel/plugin-syntax-dynamic-import'],
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ modules: false
+ }
+ ]
+ ]
+}
diff --git a/js/gadgetbridge.js b/js/gadgetbridge.js
index fb244f4..a0ae04c 100644
--- a/js/gadgetbridge.js
+++ b/js/gadgetbridge.js
@@ -1,226 +1,58734 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ }
+/******/ };
+/******/
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = function(exports) {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 8|1: behave like require
+/******/ __webpack_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = __webpack_require__(value);
+/******/ if(mode & 8) return value;
+/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ var ns = Object.create(null);
+/******/ __webpack_require__.r(ns);
+/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ return ns;
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "/js/";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = "./src/main.js");
+/******/ })
+/************************************************************************/
+/******/ ({
+
+/***/ "./node_modules/@nextcloud/dialogs/dist/filepicker.js":
+/*!************************************************************!*\
+ !*** ./node_modules/@nextcloud/dialogs/dist/filepicker.js ***!
+ \************************************************************/
+/*! no static exports found */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+__webpack_require__(/*! core-js/modules/es.object.to-string */ "./node_modules/core-js/modules/es.object.to-string.js");
+
+__webpack_require__(/*! core-js/modules/es.promise */ "./node_modules/core-js/modules/es.promise.js");
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.getFilePickerBuilder = getFilePickerBuilder;
+exports.FilePickerBuilder = exports.FilePicker = exports.FilePickerType = void 0;
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+///
+var FilePickerType;
+exports.FilePickerType = FilePickerType;
+
+(function (FilePickerType) {
+ FilePickerType[FilePickerType["Choose"] = 1] = "Choose";
+ FilePickerType[FilePickerType["Move"] = 2] = "Move";
+ FilePickerType[FilePickerType["Copy"] = 3] = "Copy";
+ FilePickerType[FilePickerType["CopyMove"] = 4] = "CopyMove";
+})(FilePickerType || (exports.FilePickerType = FilePickerType = {}));
+
+var FilePicker = /*#__PURE__*/function () {
+ function FilePicker(title, multiSelect, mimeTypeFilter, modal, type, directoriesAllowed, path) {
+ _classCallCheck(this, FilePicker);
+
+ this.title = title;
+ this.multiSelect = multiSelect;
+ this.mimeTypeFiler = mimeTypeFilter;
+ this.modal = modal;
+ this.type = type;
+ this.directoriesAllowed = directoriesAllowed;
+ this.path = path;
+ }
+
+ _createClass(FilePicker, [{
+ key: "pick",
+ value: function pick() {
+ var _this = this;
+
+ return new Promise(function (res, rej) {
+ OC.dialogs.filepicker(_this.title, res, _this.multiSelect, _this.mimeTypeFiler, _this.modal, _this.type, _this.path, {
+ allowDirectoryChooser: _this.directoriesAllowed
+ });
+ });
+ }
+ }]);
+
+ return FilePicker;
+}();
+
+exports.FilePicker = FilePicker;
+
+var FilePickerBuilder = /*#__PURE__*/function () {
+ function FilePickerBuilder(title) {
+ _classCallCheck(this, FilePickerBuilder);
+
+ this.multiSelect = false;
+ this.mimeTypeFiler = [];
+ this.modal = true;
+ this.type = FilePickerType.Choose;
+ this.directoriesAllowed = false;
+ this.title = title;
+ }
+
+ _createClass(FilePickerBuilder, [{
+ key: "setMultiSelect",
+ value: function setMultiSelect(ms) {
+ this.multiSelect = ms;
+ return this;
+ }
+ }, {
+ key: "addMimeTypeFilter",
+ value: function addMimeTypeFilter(filter) {
+ this.mimeTypeFiler.push(filter);
+ return this;
+ }
+ }, {
+ key: "setMimeTypeFilter",
+ value: function setMimeTypeFilter(filter) {
+ this.mimeTypeFiler = filter;
+ return this;
+ }
+ }, {
+ key: "setModal",
+ value: function setModal(modal) {
+ this.modal = modal;
+ return this;
+ }
+ }, {
+ key: "setType",
+ value: function setType(type) {
+ this.type = type;
+ return this;
+ }
+ }, {
+ key: "allowDirectories",
+ value: function allowDirectories() {
+ var allow = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
+ this.directoriesAllowed = allow;
+ return this;
+ }
+ }, {
+ key: "startAt",
+ value: function startAt(path) {
+ this.path = path;
+ return this;
+ }
+ }, {
+ key: "build",
+ value: function build() {
+ return new FilePicker(this.title, this.multiSelect, this.mimeTypeFiler, this.modal, this.type, this.directoriesAllowed, this.path);
+ }
+ }]);
+
+ return FilePickerBuilder;
+}();
+
+exports.FilePickerBuilder = FilePickerBuilder;
+
+function getFilePickerBuilder(title) {
+ return new FilePickerBuilder(title);
+}
+//# sourceMappingURL=filepicker.js.map
+
+/***/ }),
+
+/***/ "./node_modules/@nextcloud/dialogs/dist/index.js":
+/*!*******************************************************!*\
+ !*** ./node_modules/@nextcloud/dialogs/dist/index.js ***!
+ \*******************************************************/
+/*! no static exports found */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+Object.defineProperty(exports, "FilePicker", {
+ enumerable: true,
+ get: function get() {
+ return _filepicker.FilePicker;
+ }
+});
+Object.defineProperty(exports, "FilePickerBuilder", {
+ enumerable: true,
+ get: function get() {
+ return _filepicker.FilePickerBuilder;
+ }
+});
+Object.defineProperty(exports, "getFilePickerBuilder", {
+ enumerable: true,
+ get: function get() {
+ return _filepicker.getFilePickerBuilder;
+ }
+});
+Object.defineProperty(exports, "showMessage", {
+ enumerable: true,
+ get: function get() {
+ return _toast.showMessage;
+ }
+});
+Object.defineProperty(exports, "showSuccess", {
+ enumerable: true,
+ get: function get() {
+ return _toast.showSuccess;
+ }
+});
+Object.defineProperty(exports, "showWarning", {
+ enumerable: true,
+ get: function get() {
+ return _toast.showWarning;
+ }
+});
+Object.defineProperty(exports, "showInfo", {
+ enumerable: true,
+ get: function get() {
+ return _toast.showInfo;
+ }
+});
+Object.defineProperty(exports, "showError", {
+ enumerable: true,
+ get: function get() {
+ return _toast.showError;
+ }
+});
+
+var _filepicker = __webpack_require__(/*! ./filepicker */ "./node_modules/@nextcloud/dialogs/dist/filepicker.js");
+
+var _toast = __webpack_require__(/*! ./toast */ "./node_modules/@nextcloud/dialogs/dist/toast.js");
+//# sourceMappingURL=index.js.map
+
+/***/ }),
+
+/***/ "./node_modules/@nextcloud/dialogs/dist/toast.js":
+/*!*******************************************************!*\
+ !*** ./node_modules/@nextcloud/dialogs/dist/toast.js ***!
+ \*******************************************************/
+/*! no static exports found */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+__webpack_require__(/*! core-js/modules/es.symbol */ "./node_modules/core-js/modules/es.symbol.js");
+
+__webpack_require__(/*! core-js/modules/es.array.filter */ "./node_modules/core-js/modules/es.array.filter.js");
+
+__webpack_require__(/*! core-js/modules/es.array.for-each */ "./node_modules/core-js/modules/es.array.for-each.js");
+
+__webpack_require__(/*! core-js/modules/es.object.assign */ "./node_modules/core-js/modules/es.object.assign.js");
+
+__webpack_require__(/*! core-js/modules/es.object.get-own-property-descriptor */ "./node_modules/core-js/modules/es.object.get-own-property-descriptor.js");
+
+__webpack_require__(/*! core-js/modules/es.object.get-own-property-descriptors */ "./node_modules/core-js/modules/es.object.get-own-property-descriptors.js");
+
+__webpack_require__(/*! core-js/modules/es.object.keys */ "./node_modules/core-js/modules/es.object.keys.js");
+
+__webpack_require__(/*! core-js/modules/web.dom-collections.for-each */ "./node_modules/core-js/modules/web.dom-collections.for-each.js");
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.showMessage = showMessage;
+exports.showError = showError;
+exports.showWarning = showWarning;
+exports.showInfo = showInfo;
+exports.showSuccess = showSuccess;
+
+var _toastifyJs = _interopRequireDefault(__webpack_require__(/*! toastify-js */ "./node_modules/toastify-js/src/toastify.js"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
+
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var ToastType = function ToastType() {
+ _classCallCheck(this, ToastType);
+};
+
+ToastType.ERROR = 'toast-error';
+ToastType.WARNING = 'toast-warning';
+ToastType.INFO = 'toast-info';
+ToastType.SUCCESS = 'toast-success';
+ToastType.PERMANENT = 'toast-error';
+
+/**
+ * Show a toast message
+ *
+ * @param text Message to be shown in the toast, any HTML is removed by default
+ * @param options
+ */
+function showMessage(text, options) {
+ var _options$type;
+
+ options = Object.assign({
+ timeout: 7,
+ isHTML: false,
+ type: undefined,
+ // An undefined selector defaults to the body element
+ selector: undefined,
+ onRemove: function onRemove() {},
+ onClick: function onClick() {},
+ close: true
+ }, options);
+
+ if (!options.isHTML) {
+ // fime mae sure that text is extracted
+ var element = document.createElement('div');
+ element.innerHTML = text;
+ text = element.innerText;
+ }
+
+ var classes = (_options$type = options.type) !== null && _options$type !== void 0 ? _options$type : '';
+ var toast = (0, _toastifyJs.default)({
+ text: text,
+ duration: options.timeout === null || options.timeout === undefined ? null : options.timeout * 1000,
+ callback: options.onRemove,
+ onClick: options.onClick,
+ close: options.close,
+ gravity: 'top',
+ selector: options.selector,
+ position: 'right',
+ backgroundColor: '',
+ className: 'toast ' + classes
+ });
+ toast.showToast();
+ return toast;
+}
/**
- * @copyright (c) 2017 Joas Schilling
+ * Show a toast message with error styling
*
- * @author Joas Schilling
+ * @param text Message to be shown in the toast, any HTML is removed by default
+ * @param options
+ */
+
+
+function showError(text, options) {
+ return showMessage(text, _objectSpread(_objectSpread({}, options), {}, {
+ type: ToastType.ERROR
+ }));
+}
+/**
+ * Show a toast message with warning styling
*
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
+ * @param text Message to be shown in the toast, any HTML is removed by default
+ * @param options
*/
-(function(OC, OCA, _) {
- OCA = OCA || {};
- OCA.GadgetBridge = {
- databaseFileId: 0,
- selectedDevice: 0,
- lastRawKind: 0,
+function showWarning(text, options) {
+ return showMessage(text, _objectSpread(_objectSpread({}, options), {}, {
+ type: ToastType.WARNING
+ }));
+}
+/**
+ * Show a toast message with info styling
+ *
+ * @param text Message to be shown in the toast, any HTML is removed by default
+ * @param options
+ */
- _deviceTemplate: null,
- _deviceHTML: '' +
- '' +
- '' +
- '' +
- '{{NAME}} ({{IDENTIFIER}})' +
- '' +
- '',
- initialise: function() {
- $('#import-data').on('click', _.bind(this._importButtonOnClick, this));
- this._deviceTemplate = Handlebars.compile(this._deviceHTML);
- this._selectedDatabase($('#app-content').attr('data-database-id'), $('#app-content').attr('data-database-path'));
- },
+function showInfo(text, options) {
+ return showMessage(text, _objectSpread(_objectSpread({}, options), {}, {
+ type: ToastType.INFO
+ }));
+}
+/**
+ * Show a toast message with success styling
+ *
+ * @param text Message to be shown in the toast, any HTML is removed by default
+ * @param options
+ */
- _importButtonOnClick: function(e) {
- e.preventDefault();
- OCdialogs.filepicker(
- t('gadgetbridge', 'Choose a file to import'),
- _.bind(this._filePickerCallback, this)
- )
- },
- _selectedDatabase: function(id, path) {
- this.databaseFileId = id;
- this.databaseFilePath = path;
- $('.settings-caption').text(this.databaseFilePath);
- $('#app-content').attr('data-database-id', this.databaseFileId);
- $('#app-content').attr('data-database-path', this.databaseFilePath);
+function showSuccess(text, options) {
+ return showMessage(text, _objectSpread(_objectSpread({}, options), {}, {
+ type: ToastType.SUCCESS
+ }));
+}
+//# sourceMappingURL=toast.js.map
- if (this.databaseFileId > 0) {
- this._loadDevices();
- }
- },
+/***/ }),
- _filePickerCallback: function(path) {
- var self = this;
-
- $.ajax({
- url: OC.linkToOCS('apps/gadgetbridge/api/v1', 2) + 'database',
- type: 'POST',
- beforeSend: function (request) {
- request.setRequestHeader('Accept', 'application/json');
- },
- data: {
- path: path
- },
- success: function(result) {
- self._selectedDatabase(
- result.ocs.data.fileId,
- path.substring(1) // Remove leading slash
- );
- },
- error: function() {
- OC.Notification. showTemporary(t('gadgetbridge', 'The selected file is not a readable Gadgetbridge database'));
- }
- });
- },
+/***/ "./node_modules/@nextcloud/dialogs/styles/toast.scss":
+/*!***********************************************************!*\
+ !*** ./node_modules/@nextcloud/dialogs/styles/toast.scss ***!
+ \***********************************************************/
+/*! no static exports found */
+/***/ (function(module, exports, __webpack_require__) {
- _loadDevices: function() {
- var self = this;
- $.ajax({
- url: OC.linkToOCS('apps/gadgetbridge/api/v1', 2) + this.databaseFileId + '/devices',
- beforeSend: function (request) {
- request.setRequestHeader('Accept', 'application/json');
- },
- success: function(result) {
- // TODO Remove previous devices
-
-
- var singleDeviceDatabase = result.ocs.data.length === 1;
-
- _.each(result.ocs.data, function(device) {
- var $device = $(self._deviceTemplate(device));
- $device.on('click', function() {
- self.selectedDevice = $(this).attr('data-device-id');
- $(this).addClass('active');
- if (self.selectedDevice !== $(this).attr('data-device-id')) {
- self._loadDevice(moment().format('YYYY/MM/DD/HH/mm'));
- }
- });
- $('#app-navigation ul').append($device);
-
- if (singleDeviceDatabase) {
- self.selectedDevice = device._id;
- self._loadDevice(moment().format('YYYY/MM/DD/HH/mm'));
- $device.addClass('active');
- }
- });
- },
- error: function() {
- OC.Notification. showTemporary(t('gadgetbridge', 'The selected file is not a readable Gadgetbridge database'));
- }
- });
- },
+// style-loader: Adds some css to the DOM by adding a