diff --git a/extend.php b/extend.php index a147a1e..e2aaa9a 100644 --- a/extend.php +++ b/extend.php @@ -1,24 +1,31 @@ js(__DIR__.'/js/dist/forum.js')->css(__DIR__.'/less/forum.less'), + (new Extend\Frontend('admin')) + ->js(__DIR__.'/js/dist/admin.js') + ->css(__DIR__.'/less/admin.less'), + + (new Extend\Frontend('forum')) + ->js(__DIR__.'/js/dist/forum.js') + ->css(__DIR__.'/less/forum.less'), new Extend\Locales(__DIR__.'/locale'), + (new Extend\Settings) + ->default('acpl-mobile-tab.items', ['home', 'tags', 'notifications', 'session']) + ->serializeToForum('acplMobileTabItems', 'acpl-mobile-tab.items', function ($value) { + if (is_string($value)) { + return json_decode($value, true); + } + + return $value; + }), + (new Extend\Frontend('forum')) ->content(function (Document $document) { $document->meta['viewport'] = "{$document->meta['viewport']}, viewport-fit=cover"; diff --git a/js/.prettierrc.js b/js/.prettierrc.js new file mode 100644 index 0000000..5a63c91 --- /dev/null +++ b/js/.prettierrc.js @@ -0,0 +1,4 @@ +module.exports = { + ...require('@flarum/prettier-config'), + plugins: ['prettier-plugin-organize-imports'], +}; diff --git a/js/admin.ts b/js/admin.ts new file mode 100644 index 0000000..3e69ff3 --- /dev/null +++ b/js/admin.ts @@ -0,0 +1 @@ +export * from './src/admin'; diff --git a/js/dist/forum.js b/js/dist/forum.js deleted file mode 100644 index ee3dae5..0000000 --- a/js/dist/forum.js +++ /dev/null @@ -1,2 +0,0 @@ -(()=>{var o={n:e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return o.d(a,{a}),a},d:(e,a)=>{for(var t in a)o.o(a,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},o:(o,e)=>Object.prototype.hasOwnProperty.call(o,e)};(()=>{"use strict";const e=flarum.reg.get("core","forum/app");var a=o.n(e);const t=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","common/Application");var r=o.n(n);const s=flarum.reg.get("core","common/Component");var l=o.n(s);const c=flarum.reg.get("core","common/helpers/listItems");var i=o.n(c);const u=flarum.reg.get("core","common/utils/ItemList");var f=o.n(u);const d=flarum.reg.get("core","common/components/LinkButton");var b=o.n(d);const p=flarum.reg.get("core","common/components/Button");var g=o.n(p);const v=flarum.reg.get("core","forum/components/SessionDropdown");var h=o.n(v);const x=flarum.reg.get("core","common/components/Avatar");var M=o.n(x);class w extends(h()){getButtonContent(){const{user:o}=a().session;return[m(M(),{user:o})," ",m("span",{className:"Button-label"},a().translator.trans("acpl-mobile-tab.forum.profile"))]}}flarum.reg.add("acpl-mobile-tab","forum/components/MobileTabSessionDropdown",w);class y extends(l()){view(o){return m("nav",{className:"MobileTab"},m("ul",{className:"MobileTab-items"},i()(this.items().toArray())))}items(){const o=new(f());if(o.add("home",m(b(),{href:"/",icon:"fas fa-home"},a().translator.trans("acpl-mobile-tab.forum.home")),100),"/all"===a().routes.index.path?o.add("all",m(b(),{href:a().route("index"),icon:"fas fa-comments"},a().translator.trans("acpl-mobile-tab.forum.all_discussions")),90):"askvortsov-categories"in flarum.extensions?o.add("categories",m(b(),{href:a().route("categories"),icon:"fas fa-th-list"},a().translator.trans("acpl-mobile-tab.forum.categories")),90):"flarum-tags"in flarum.extensions&&o.add("tags",m(b(),{href:a().route("tags"),icon:"fas fa-tags"},a().translator.trans("acpl-mobile-tab.forum.tags")),90),a().session.user){const e=a().session.user.unreadNotificationCount();o.add("notifications",m(b(),{href:a().route("notifications"),icon:"fas fa-bell",title:a().translator.trans("acpl-mobile-tab.forum.notifications")},e?m("span",{className:"Bubble"},e):"",a().translator.trans("acpl-mobile-tab.forum.notifications")),80),o.add("session",m(w,null),70)}else o.add("logIn",m(g(),{icon:"fas fa-user",className:"Button Button--link",onclick:()=>a().modal.show(()=>flarum.reg.asyncModuleImport("flarum/forum/components/LogInModal"))},a().translator.trans("acpl-mobile-tab.forum.log_in")),70);return o}}flarum.reg.add("acpl-mobile-tab","forum/components/MobileTab",y);const B={MobileTab:y,MobileTabSessionDropdown:w};flarum.reg.add("acpl-mobile-tab","forum/components",B),a().initializers.add("acpl/mobile-tab",()=>{(0,t.extend)(r().prototype,"mount",()=>{const o=document.createElement("div");m.mount(document.body.appendChild(o),y)})})})(),module.exports={}})(); -//# sourceMappingURL=forum.js.map \ No newline at end of file diff --git a/js/dist/forum.js.map b/js/dist/forum.js.map deleted file mode 100644 index bf06952..0000000 --- a/js/dist/forum.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"forum.js","mappings":"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","sources":["webpack://@acpl/mobile-tab/webpack/bootstrap","webpack://@acpl/mobile-tab/webpack/runtime/compat get default export","webpack://@acpl/mobile-tab/webpack/runtime/define property getters","webpack://@acpl/mobile-tab/webpack/runtime/hasOwnProperty shorthand","webpack://@acpl/mobile-tab/external root \"flarum.reg.get('core', 'forum/app')\"","webpack://@acpl/mobile-tab/external root \"flarum.reg.get('core', 'common/extend')\"","webpack://@acpl/mobile-tab/external root \"flarum.reg.get('core', 'common/Application')\"","webpack://@acpl/mobile-tab/external root \"flarum.reg.get('core', 'common/Component')\"","webpack://@acpl/mobile-tab/external root \"flarum.reg.get('core', 'common/helpers/listItems')\"","webpack://@acpl/mobile-tab/external root \"flarum.reg.get('core', 'common/utils/ItemList')\"","webpack://@acpl/mobile-tab/external root \"flarum.reg.get('core', 'common/components/LinkButton')\"","webpack://@acpl/mobile-tab/external root \"flarum.reg.get('core', 'common/components/Button')\"","webpack://@acpl/mobile-tab/external root \"flarum.reg.get('core', 'forum/components/SessionDropdown')\"","webpack://@acpl/mobile-tab/external root \"flarum.reg.get('core', 'common/components/Avatar')\"","webpack://@acpl/mobile-tab/./src/forum/components/MobileTabSessionDropdown.tsx","webpack://@acpl/mobile-tab/./src/forum/components/MobileTab.tsx","webpack://@acpl/mobile-tab/./src/forum/components/index.ts","webpack://@acpl/mobile-tab/./src/forum/index.ts"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'forum/app');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/extend');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/Application');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/Component');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/helpers/listItems');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/utils/ItemList');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/LinkButton');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Button');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'forum/components/SessionDropdown');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Avatar');","import app from 'flarum/forum/app';\nimport SessionDropdown from 'flarum/forum/components/SessionDropdown';\nimport Avatar from 'flarum/common/components/Avatar';\nexport default class MobileTabSessionDropdown extends SessionDropdown {\n getButtonContent() {\n const {\n user\n } = app.session;\n return [m(Avatar, {\n user: user\n }), ' ',\n // The username can be long, so it is better to display \"Profile\"\n m(\"span\", {\n className: \"Button-label\"\n }, app.translator.trans('acpl-mobile-tab.forum.profile'))];\n }\n}\nflarum.reg.add('acpl-mobile-tab', 'forum/components/MobileTabSessionDropdown', MobileTabSessionDropdown);","import app from 'flarum/forum/app';\nimport Component from 'flarum/common/Component';\nimport listItems from 'flarum/common/helpers/listItems';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport LinkButton from 'flarum/common/components/LinkButton';\nimport Button from 'flarum/common/components/Button';\nimport MobileTabSessionDropdown from './MobileTabSessionDropdown';\nexport default class MobileTab extends Component {\n view(vnode) {\n return m(\"nav\", {\n className: \"MobileTab\"\n }, m(\"ul\", {\n className: \"MobileTab-items\"\n }, listItems(this.items().toArray())));\n }\n items() {\n const items = new ItemList();\n items.add('home', m(LinkButton, {\n href: \"/\",\n icon: \"fas fa-home\"\n }, app.translator.trans('acpl-mobile-tab.forum.home')), 100);\n if (app.routes.index.path === '/all') {\n items.add('all', m(LinkButton, {\n href: app.route('index'),\n icon: \"fas fa-comments\"\n }, app.translator.trans('acpl-mobile-tab.forum.all_discussions')), 90);\n } else if ('askvortsov-categories' in flarum.extensions) {\n items.add('categories', m(LinkButton, {\n href: app.route('categories'),\n icon: \"fas fa-th-list\"\n }, app.translator.trans('acpl-mobile-tab.forum.categories')), 90);\n } else if ('flarum-tags' in flarum.extensions) {\n items.add('tags', m(LinkButton, {\n href: app.route('tags'),\n icon: \"fas fa-tags\"\n }, app.translator.trans('acpl-mobile-tab.forum.tags')), 90);\n }\n if (app.session.user) {\n const unread = app.session.user.unreadNotificationCount();\n // The default Flarum component opens as a dropdown on mobile if the drawer is not open\n items.add('notifications', m(LinkButton, {\n href: app.route('notifications'),\n icon: \"fas fa-bell\",\n title: app.translator.trans('acpl-mobile-tab.forum.notifications')\n }, unread ? m(\"span\", {\n className: \"Bubble\"\n }, unread) : '', app.translator.trans('acpl-mobile-tab.forum.notifications')), 80);\n items.add('session', m(MobileTabSessionDropdown, null), 70);\n } else {\n items.add('logIn', m(Button, {\n icon: \"fas fa-user\",\n className: \"Button Button--link\",\n onclick: () => app.modal.show(() => flarum.reg.asyncModuleImport('flarum/forum/components/LogInModal'))\n }, app.translator.trans('acpl-mobile-tab.forum.log_in')), 70);\n }\n return items;\n }\n}\nflarum.reg.add('acpl-mobile-tab', 'forum/components/MobileTab', MobileTab);","import MobileTab from './MobileTab';\nimport MobileTabSessionDropdown from './MobileTabSessionDropdown';\nconst components = {\n MobileTab,\n MobileTabSessionDropdown\n};\nexport default components;\nflarum.reg.add('acpl-mobile-tab', 'forum/components', components);","import app from 'flarum/forum/app';\nimport { extend } from 'flarum/common/extend';\nimport Application from 'flarum/common/Application';\nimport MobileTab from './components/MobileTab';\napp.initializers.add('acpl/mobile-tab', () => {\n //@ts-ignore - missing 'mount' types\n extend(Application.prototype, 'mount', () => {\n const mTab = document.createElement('div');\n m.mount(document.body.appendChild(mTab), MobileTab);\n });\n});\nexport * from './components';"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","flarum","reg","MobileTabSessionDropdown","getButtonContent","user","m","className","trans","add","MobileTab","view","vnode","this","items","toArray","href","icon","index","path","extensions","unread","unreadNotificationCount","title","onclick","show","asyncModuleImport","components","extend","mTab","document","createElement","mount","body","appendChild"],"sourceRoot":""} \ No newline at end of file diff --git a/js/package-lock.json b/js/package-lock.json index d2cb988..2214b06 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -5,13 +5,16 @@ "packages": { "": { "name": "@acpl/mobile-tab", + "dependencies": { + "sortablejs": "^1.15.6" + }, "devDependencies": { "@flarum/prettier-config": "^1.0.0", + "@types/sortablejs": "^1.15.9", "flarum-tsconfig": "^2.0.0", "flarum-webpack-config": "^3.0.2", - "husky": "^9.1.7", - "lint-staged": "^16.2.6", "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "typescript": "^5.9.3", "webpack": "^5.102.1", "webpack-cli": "^6.0.1" @@ -1932,6 +1935,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sortablejs": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/throttle-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz", @@ -2271,48 +2281,6 @@ "ajv": "^6.9.1" } }, - "node_modules/ansi-escapes": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", - "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/babel-loader": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", @@ -2451,19 +2419,6 @@ "node": "*" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browserslist": { "version": "4.27.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", @@ -2537,39 +2492,6 @@ "node": ">=6.0" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -2593,14 +2515,11 @@ "license": "MIT" }, "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } + "license": "MIT" }, "node_modules/common-path-prefix": { "version": "3.0.0", @@ -2691,13 +2610,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -2735,19 +2647,6 @@ "node": ">=4" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2835,13 +2734,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, - "license": "MIT" - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -2893,19 +2785,6 @@ "node": ">= 4.9.1" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-cache-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", @@ -3025,19 +2904,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -3098,22 +2964,6 @@ "dev": true, "license": "MIT" }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -3239,32 +3089,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -3367,49 +3191,6 @@ "node": ">=0.10.0" } }, - "node_modules/lint-staged": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.6.tgz", - "integrity": "sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^14.0.1", - "listr2": "^9.0.5", - "micromatch": "^4.0.8", - "nano-spawn": "^2.0.0", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.8.1" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/loader-runner": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", @@ -3475,26 +3256,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3512,20 +3273,6 @@ "dev": true, "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3549,19 +3296,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -3589,19 +3323,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nano-spawn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", - "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" - } - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -3616,22 +3337,6 @@ "dev": true, "license": "MIT" }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -3718,32 +3423,6 @@ "dev": true, "license": "ISC" }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/pkg-dir": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", @@ -3766,6 +3445,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3776,6 +3456,23 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-organize-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.3.0.tgz", + "integrity": "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0 || 3" + }, + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3921,30 +3618,6 @@ "node": ">=8" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4041,19 +3714,6 @@ "node": ">=8" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -4069,22 +3729,11 @@ "node": ">= 10" } }, - "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } + "node_modules/sortablejs": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", + "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", + "license": "MIT" }, "node_modules/source-map": { "version": "0.6.1", @@ -4107,49 +3756,6 @@ "source-map": "^0.6.0" } }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -4305,26 +3911,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -4341,6 +3927,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4711,42 +4298,6 @@ "dev": true, "license": "MIT" }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", @@ -4776,19 +4327,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yocto-queue": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", diff --git a/js/package.json b/js/package.json index 61ac014..cff2239 100644 --- a/js/package.json +++ b/js/package.json @@ -1,12 +1,16 @@ { "name": "@acpl/mobile-tab", "private": true, - "prettier": "@flarum/prettier-config", + "dependencies": { + "sortablejs": "^1.15.6" + }, "devDependencies": { "@flarum/prettier-config": "^1.0.0", + "@types/sortablejs": "^1.15.9", "flarum-tsconfig": "^2.0.0", "flarum-webpack-config": "^3.0.2", "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "typescript": "^5.9.3", "webpack": "^5.102.1", "webpack-cli": "^6.0.1" @@ -15,8 +19,8 @@ "dev": "webpack --mode development --watch", "build": "webpack --mode production", "analyze": "cross-env ANALYZER=true npm run build", - "format": "prettier --write src", - "format-check": "prettier --check src", + "format": "prettier --write src ../less ../locale", + "format-check": "prettier --check src ../less ../locale", "clean-typings": "npx rimraf dist-typings && mkdir dist-typings", "build-typings": "npm run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && npm run post-build-typings", "post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'", diff --git a/js/src/admin/MobileTabItemsRegistryAdmin.tsx b/js/src/admin/MobileTabItemsRegistryAdmin.tsx new file mode 100644 index 0000000..3cbbc8e --- /dev/null +++ b/js/src/admin/MobileTabItemsRegistryAdmin.tsx @@ -0,0 +1,13 @@ +import app from 'flarum/admin/app'; +import MobileTabItemsRegistry from '../common/MobileTabItemsRegistry'; +import MobileTabItem from '../common/components/MobileTabItem'; + +export default class MobileTabItemsRegistryAdmin extends MobileTabItemsRegistry { + items() { + const items = super.items(); + + items.add('session', {app.translator.trans('acpl-mobile-tab.admin.item.session')}); + + return items; + } +} diff --git a/js/src/admin/components/MobileTabSettingsPage.tsx b/js/src/admin/components/MobileTabSettingsPage.tsx new file mode 100644 index 0000000..850f39c --- /dev/null +++ b/js/src/admin/components/MobileTabSettingsPage.tsx @@ -0,0 +1,140 @@ +import app from 'flarum/admin/app'; +import ExtensionPage, { ExtensionPageAttrs } from 'flarum/admin/components/ExtensionPage'; +import FormSection from 'flarum/admin/components/FormSection'; +import FormSectionGroup from 'flarum/admin/components/FormSectionGroup'; +import listItems from 'flarum/common/helpers/listItems'; +import ItemList from 'flarum/common/utils/ItemList'; +import { Children, VnodeDOM } from 'mithril'; +import Sortable from 'sortablejs'; +import { MobileTabRegistryItem } from '../../common/types'; +import MobileTabItemsRegistryAdmin from '../MobileTabItemsRegistryAdmin'; + +export default class MobileTabSettingsPage extends ExtensionPage { + protected itemsSettingKey = 'acpl-mobile-tab.items'; + protected sortableKey = 'acpl-mobile-tab'; + protected sortableAvailableItems!: Sortable; + protected sortableEnabledItems!: Sortable; + /** + * Sortablejs directly manipulates the DOM, which can confuse Mithril's diffing. + * Changing this key forces a full re-render of the list, ensuring a clean sync. + */ + protected forcedRefreshKey = 0; + + get activeKeys(): string[] { + const raw = this.setting(this.itemsSettingKey)(); + if (Array.isArray(raw)) return raw; + + try { + return raw ? JSON.parse(raw) : []; + } catch { + return []; + } + } + + set activeKeys(value: string[]) { + this.setting(this.itemsSettingKey)(JSON.stringify(value)); + } + + content(vnode: VnodeDOM) { + return ( +
+
+ + {this.availableItemsContent()} + {this.enabledItemsContent()} + + {this.submitButton()} +
+
+ ); + } + + availableItemsContent(): Children { + return ( + + + + ); + } + + enabledItemsContent(): Children { + return ( + + + + ); + } + + availableItems() { + const registeredItems = new MobileTabItemsRegistryAdmin().items(); + + this.activeKeys.forEach((key: string) => { + if (registeredItems.has(key)) registeredItems.remove(key); + }); + + return registeredItems; + } + + enabledItems() { + const registeredItems = new MobileTabItemsRegistryAdmin().items(); + const enabledItems = new ItemList(); + + this.activeKeys.forEach((key: string) => { + if (registeredItems.has(key)) { + enabledItems.add(key, registeredItems.get(key)); + } + }); + + return enabledItems; + } + + getSortableItemKey(event: Sortable.SortableEvent) { + const element = event.item; + const match = Array.from(element.classList).find((className) => className.startsWith('item-')); + if (!match) return; + return match.replace('item-', ''); + } + + onListCreate() { + this.sortableAvailableItems = new Sortable(this.element.querySelector('.MobileTabAvailableItems-list')!, { + group: this.sortableKey, + animation: 150, + sort: false, + }); + + this.sortableEnabledItems = new Sortable(this.element.querySelector('.MobileTabPreview-items')!, { + group: this.sortableKey, + animation: 120, + onAdd: (event) => { + const key = this.getSortableItemKey(event); + if (!key) return; + const activeKeys = [...this.activeKeys]; + this.activeKeys = [...activeKeys, key]; + this.forcedRefreshKey++; + m.redraw(); + }, + onSort: (event) => { + if (event.oldIndex == null || event.newIndex == null) return; + if (event.oldIndex === event.newIndex) return; + if (event.from !== event.to) return; + + const current = [...this.activeKeys]; + + const [moved] = current.splice(event.oldIndex, 1); + current.splice(event.newIndex, 0, moved); + + this.activeKeys = current; + m.redraw(); + }, + onRemove: (event) => { + const key = this.getSortableItemKey(event); + const activeKeys = [...this.activeKeys]; + this.activeKeys = activeKeys.filter((item) => item !== key); + this.forcedRefreshKey++; + m.redraw(); + }, + }); + } +} diff --git a/js/src/admin/extend.ts b/js/src/admin/extend.ts new file mode 100644 index 0000000..5b9c6a0 --- /dev/null +++ b/js/src/admin/extend.ts @@ -0,0 +1,4 @@ +import Extend from 'flarum/common/extenders'; +import MobileTabSettingsPage from './components/MobileTabSettingsPage'; + +export default [new Extend.Admin().page(MobileTabSettingsPage)]; diff --git a/js/src/admin/index.ts b/js/src/admin/index.ts new file mode 100644 index 0000000..6d2293d --- /dev/null +++ b/js/src/admin/index.ts @@ -0,0 +1 @@ +export { default as extend } from './extend'; diff --git a/js/src/common/MobileTabItemsRegistry.tsx b/js/src/common/MobileTabItemsRegistry.tsx new file mode 100644 index 0000000..0c4638b --- /dev/null +++ b/js/src/common/MobileTabItemsRegistry.tsx @@ -0,0 +1,46 @@ +import app from 'flarum/common/app'; +import ItemList from 'flarum/common/utils/ItemList'; +import MobileTabItem from './components/MobileTabItem'; +import { MobileTabRegistryItem } from './types'; + +export default class MobileTabItemsRegistry { + items() { + const itemList = new ItemList(); + + itemList.add( + 'home', + + {app.translator.trans('acpl-mobile-tab.lib.item.home')} + + ); + + itemList.add( + 'all_discussions', + + {app.translator.trans('acpl-mobile-tab.lib.item.all_discussions')} + + ); + + if (app.session.user) { + const unread = app.session.user.unreadNotificationCount(); + itemList.add( + 'notifications', + + {unread ? {unread} : ''} + {app.translator.trans('acpl-mobile-tab.lib.item.notifications')} + + ); + } + + if ('flarum-tags' in flarum.extensions) { + itemList.add( + 'tags', + + {app.translator.trans('acpl-mobile-tab.lib.item.tags')} + + ); + } + + return itemList; + } +} diff --git a/js/src/common/components/MobileTabItem.tsx b/js/src/common/components/MobileTabItem.tsx new file mode 100644 index 0000000..70c0c89 --- /dev/null +++ b/js/src/common/components/MobileTabItem.tsx @@ -0,0 +1,18 @@ +import Component from 'flarum/common/Component'; +import { IButtonAttrs } from 'flarum/common/components/Button'; +import LinkButton from 'flarum/common/components/LinkButton'; +import { Vnode } from 'mithril'; + +export interface MobileTabItemAttrs extends IButtonAttrs { + href: string; +} + +export default class MobileTabItem extends Component { + view(vnode: Vnode) { + return ( + + {vnode.children} + + ); + } +} diff --git a/js/src/common/types.ts b/js/src/common/types.ts new file mode 100644 index 0000000..0399766 --- /dev/null +++ b/js/src/common/types.ts @@ -0,0 +1,3 @@ +import { Children } from 'mithril'; + +export type MobileTabRegistryItem = Children; diff --git a/js/src/forum/MobileTabItemsRegistryForum.tsx b/js/src/forum/MobileTabItemsRegistryForum.tsx new file mode 100644 index 0000000..006b50a --- /dev/null +++ b/js/src/forum/MobileTabItemsRegistryForum.tsx @@ -0,0 +1,23 @@ +import Button from 'flarum/common/components/Button'; +import app from 'flarum/forum/app'; +import MobileTabItemsRegistry from '../common/MobileTabItemsRegistry'; +import MobileTabSessionDropdown from './components/MobileTabSessionDropdown'; + +export default class MobileTabItemsRegistryForum extends MobileTabItemsRegistry { + items() { + const items = super.items(); + + if (app.session.user) { + items.add('session', ); + } else { + items.add( + 'session', + + ); + } + + return items; + } +} diff --git a/js/src/forum/components/MobileTab.tsx b/js/src/forum/components/MobileTab.tsx index 49f45e1..ca59de9 100644 --- a/js/src/forum/components/MobileTab.tsx +++ b/js/src/forum/components/MobileTab.tsx @@ -1,13 +1,11 @@ -import type { Children, Vnode } from 'mithril'; -import app from 'flarum/forum/app'; import type { ComponentAttrs } from 'flarum/common/Component'; import Component from 'flarum/common/Component'; import listItems from 'flarum/common/helpers/listItems'; -import ItemList from 'flarum/common/utils/ItemList'; -import LinkButton from 'flarum/common/components/LinkButton'; -import Button from 'flarum/common/components/Button'; +import type { Children, Vnode } from 'mithril'; -import MobileTabSessionDropdown from './MobileTabSessionDropdown'; +import ItemList from 'flarum/common/utils/ItemList'; +import app from 'flarum/forum/app'; +import MobileTabItemsRegistryForum from '../MobileTabItemsRegistryForum'; export default class MobileTab extends Component { view(vnode: Vnode): Children { @@ -18,65 +16,16 @@ export default class MobileTab extends Component { ); } - items(): ItemList { - const items = new ItemList(); - - items.add( - 'home', - - {app.translator.trans('acpl-mobile-tab.forum.home')} - , - 100 - ); + items() { + const registeredItems = new MobileTabItemsRegistryForum().items(); - if (app.routes.index.path === '/all') { - items.add( - 'all', - - {app.translator.trans('acpl-mobile-tab.forum.all_discussions')} - , - 90 - ); - } else if ('askvortsov-categories' in flarum.extensions) { - items.add( - 'categories', - - {app.translator.trans('acpl-mobile-tab.forum.categories')} - , - 90 - ); - } else if ('flarum-tags' in flarum.extensions) { - items.add( - 'tags', - - {app.translator.trans('acpl-mobile-tab.forum.tags')} - , - 90 - ); - } - - if (app.session.user) { - const unread = app.session.user.unreadNotificationCount(); - // The default Flarum component opens as a dropdown on mobile if the drawer is not open - items.add( - 'notifications', - - {unread ? {unread} : ''} - {app.translator.trans('acpl-mobile-tab.forum.notifications')} - , - 80 - ); - - items.add('session', , 70); - } else { - items.add( - 'logIn', - , - 70 - ); - } + const settings = app.forum.attribute('acplMobileTabItems'); + const items = new ItemList(); + settings.forEach((item: string) => { + if (registeredItems.has(item)) { + items.add(item, registeredItems.get(item)); + } + }); return items; } diff --git a/js/src/forum/components/MobileTabSessionDropdown.tsx b/js/src/forum/components/MobileTabSessionDropdown.tsx index 923e68d..a023d9c 100644 --- a/js/src/forum/components/MobileTabSessionDropdown.tsx +++ b/js/src/forum/components/MobileTabSessionDropdown.tsx @@ -1,6 +1,6 @@ +import Avatar from 'flarum/common/components/Avatar'; import app from 'flarum/forum/app'; import SessionDropdown from 'flarum/forum/components/SessionDropdown'; -import Avatar from 'flarum/common/components/Avatar'; export default class MobileTabSessionDropdown extends SessionDropdown { getButtonContent() { @@ -10,7 +10,7 @@ export default class MobileTabSessionDropdown extends SessionDropdown { , ' ', // The username can be long, so it is better to display "Profile" - {app.translator.trans('acpl-mobile-tab.forum.profile')}, + {app.translator.trans('acpl-mobile-tab.lib.item.profile')}, ]; } } diff --git a/js/src/forum/index.ts b/js/src/forum/index.ts index 5eff7bd..b56b68a 100644 --- a/js/src/forum/index.ts +++ b/js/src/forum/index.ts @@ -1,6 +1,6 @@ -import app from 'flarum/forum/app'; -import { extend } from 'flarum/common/extend'; import Application from 'flarum/common/Application'; +import { extend } from 'flarum/common/extend'; +import app from 'flarum/forum/app'; import MobileTab from './components/MobileTab'; diff --git a/less/admin.less b/less/admin.less new file mode 100644 index 0000000..2501836 --- /dev/null +++ b/less/admin.less @@ -0,0 +1,22 @@ +@import "./components/MobileTab"; + +.MobileTabPreview { + position: static; + + @media @tablet-up { + display: block; + } +} + +.MobileTabAvailableItems-list { + list-style: none; + padding-left: 0; + gap: 1em; + + display: grid; + grid-template-columns: repeat(auto-fit, minmax(84px, 1fr)); + + > li { + flex-basis: 33%; + } +} diff --git a/less/components/MobileTab.less b/less/components/MobileTab.less new file mode 100644 index 0000000..688cb59 --- /dev/null +++ b/less/components/MobileTab.less @@ -0,0 +1,95 @@ +:root { + --mobile-tab-height: 54px; + --mobile-tab-bg: var(--body-bg); + --mobile-tab-zindex: var(--zindex-header); + --mobile-tab-shadow: 0 2px 6px var(--shadow-color); + --mobile-tab-item-color: var(--header-control-color); + --mobile-tab-item-active-color: var(--primary-color); +} + +.MobileTab { + @media @tablet-up { + display: none; + } + + position: fixed; + bottom: 0; + left: 0; + width: 100%; + height: ~"calc(var(--mobile-tab-height) + env(safe-area-inset-bottom, 0))"; + background: var(--mobile-tab-bg); + box-shadow: var(--mobile-tab-shadow); + z-index: var(--mobile-tab-zindex); + isolation: isolate; + + &-items { + display: flex; + justify-content: space-between; + align-items: center; + list-style: none; + height: 100%; + margin: 0 auto; + padding: 0 0 env(safe-area-inset-bottom, 0); + text-align: center; + + > li { + flex-basis: 100%; + + > .ButtonGroup { + width: 100%; + } + + > .LinkButton, + > .Button, + > .ButtonGroup > .Button { + position: relative; + color: var(--mobile-tab-item-color); + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + width: 100%; + height: 100%; + padding: 5px 0; + line-height: 1; + -webkit-tap-highlight-color: color-mix( + in srgb, + var(--control-bg) 40%, + transparent + ); + + .Button-icon { + font-size: 20px; + } + + .Button-label { + font-size: 9px; + line-height: 1em; + margin: 0; + } + + &:hover, + &:active { + text-decoration: none; + background-color: transparent; + box-shadow: none; + } + } + + > [active="true"] { + color: var(--primary-color); + } + } + + .SessionDropdown .Avatar { + --size: 20px; + margin: 0; + box-sizing: border-box; + } + + .Bubble { + top: 6px; + left: 52%; + } + } +} diff --git a/less/forum.less b/less/forum.less index 688cb59..f4d04d3 100644 --- a/less/forum.less +++ b/less/forum.less @@ -1,95 +1 @@ -:root { - --mobile-tab-height: 54px; - --mobile-tab-bg: var(--body-bg); - --mobile-tab-zindex: var(--zindex-header); - --mobile-tab-shadow: 0 2px 6px var(--shadow-color); - --mobile-tab-item-color: var(--header-control-color); - --mobile-tab-item-active-color: var(--primary-color); -} - -.MobileTab { - @media @tablet-up { - display: none; - } - - position: fixed; - bottom: 0; - left: 0; - width: 100%; - height: ~"calc(var(--mobile-tab-height) + env(safe-area-inset-bottom, 0))"; - background: var(--mobile-tab-bg); - box-shadow: var(--mobile-tab-shadow); - z-index: var(--mobile-tab-zindex); - isolation: isolate; - - &-items { - display: flex; - justify-content: space-between; - align-items: center; - list-style: none; - height: 100%; - margin: 0 auto; - padding: 0 0 env(safe-area-inset-bottom, 0); - text-align: center; - - > li { - flex-basis: 100%; - - > .ButtonGroup { - width: 100%; - } - - > .LinkButton, - > .Button, - > .ButtonGroup > .Button { - position: relative; - color: var(--mobile-tab-item-color); - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - width: 100%; - height: 100%; - padding: 5px 0; - line-height: 1; - -webkit-tap-highlight-color: color-mix( - in srgb, - var(--control-bg) 40%, - transparent - ); - - .Button-icon { - font-size: 20px; - } - - .Button-label { - font-size: 9px; - line-height: 1em; - margin: 0; - } - - &:hover, - &:active { - text-decoration: none; - background-color: transparent; - box-shadow: none; - } - } - - > [active="true"] { - color: var(--primary-color); - } - } - - .SessionDropdown .Avatar { - --size: 20px; - margin: 0; - box-sizing: border-box; - } - - .Bubble { - top: 6px; - left: 52%; - } - } -} +@import "./components/MobileTab"; diff --git a/locale/en.yml b/locale/en.yml index 58fac30..166ce5c 100644 --- a/locale/en.yml +++ b/locale/en.yml @@ -1,9 +1,17 @@ acpl-mobile-tab: + admin: + available_items: "Available items" + active_items: "Active items" + item: + session: "Session" forum: - home: "Home" - all_discussions: "Discussions" - tags: "Tags" - categories: "Categories" - notifications: "Notifications" - log_in: "Log In" - profile: "Profile" + + lib: + item: + home: "Home" + all_discussions: "Discussions" + tags: "Tags" + categories: "Categories" + notifications: "Notifications" + log_in: "Log In" + profile: "Profile"