Compare commits

...

204 Commits

Author SHA1 Message Date
DanyLE
59fb302e18 feat(GridView): allows navigation by arrow keys
All checks were successful
AntOS front-end / Build-AntOS-Frontend (push) Successful in 35s
2024-07-28 19:05:34 +02:00
DanyLE
4bbc6c770a feat(ListView): allows list navigation using keyboard 2024-07-28 12:53:49 +02:00
DanyLE
bfeef223a8 improve: allow changing year in calendar widget
All checks were successful
AntOS front-end / Build-AntOS-Frontend (push) Successful in 34s
2024-07-27 11:50:04 +02:00
Dany LE
a554a678d8
feat(ci): upload artifact on success
All checks were successful
AntOS front-end / Build-AntOS-Frontend (push) Successful in 41s
2024-07-22 18:28:19 +02:00
29702459b4 refactor(ci): use gitea action instead of Jenkins 2024-07-17 11:41:47 +02:00
73f925ac8c feat: allow to resize on any edge of the window
All checks were successful
gitea-sync/antos-frontend/pipeline/head This commit looks good
2024-06-25 16:27:10 +02:00
147353327b various changes:
All checks were successful
gitea-sync/antos-frontend/pipeline/head This commit looks good
- ESC (alone) key is now considered as function key for binding (global or application)
- Improve app launcher event handling
- Convention: Global announcement events are in upper case, local window events is in lower case
2024-06-24 17:54:10 +02:00
DanyLE
9fa3ab92bf fix: PushNotification service show/hide animation bug
All checks were successful
gitea-sync/antos-frontend/pipeline/head This commit looks good
2024-04-30 16:30:33 +02:00
DanyLE
45b019725d fix: add missing Setting handle to git
All checks were successful
gitea-sync/antos-frontend/pipeline/head This commit looks good
2024-03-21 13:15:56 +01:00
DanyLE
b99668f4f2 feat: Setting displays versions info
All checks were successful
gitea-sync/antos-frontend/pipeline/head This commit looks good
2024-03-18 17:06:53 +01:00
DanyLE
04da2a9d39 fix: Allow passing Global version number and build ID to frontend build
All checks were successful
gitea-sync/antos-frontend/pipeline/head This commit looks good
2024-03-17 21:18:11 +01:00
DanyLE
8c20cfec5e doc: update README.md 2024-03-17 21:18:11 +01:00
DanyLE
24788a421c refactor: remove unused files 2024-03-17 21:18:11 +01:00
DanyLE
e479fe43c9 fix: init npm before installing packages 2024-03-17 21:18:11 +01:00
DanyLE
4a46104710 feat: add install_dev to Makefile to install dependencies before build 2024-03-17 21:18:11 +01:00
DanyLE
5605d6c35a fix: simulate contextmenu on mobile device 2024-03-17 21:18:11 +01:00
DanyLE
6ac2429dba fix: list view item shall propagate click event to parent 2024-03-17 21:18:11 +01:00
DanyLE
297d471c1d fix: dblclick event does not fire on mobile device (IOS) 2024-03-17 21:18:11 +01:00
DanyLE
d95f9382f3 fix: page scale problem on mobile 2024-03-17 21:18:11 +01:00
DanyLE
d00fd3bd82 fix: invalid background settings 2024-03-17 21:18:11 +01:00
DanyLE
cbeab0d0f2 style: update file UI style 2024-03-17 21:18:11 +01:00
DanyLE
a86da11532 fix: minor UI bugs on File and Setting apps 2024-03-17 21:18:11 +01:00
DanyLE
984cdae438 fix: remove debug message 2024-03-17 21:18:11 +01:00
DanyLE
3e201bfbba fix: use row/column as common directives for all UI horizontal/vertical direction 2024-03-17 21:18:11 +01:00
DanyLE
3d09a20512 update: antos API declaration 2024-03-17 21:18:11 +01:00
DanyLE
87dad4eded update: use latest UI API on system applications 2024-03-17 21:18:11 +01:00
DanyLE
e6bf4d5352 feat: add APIs that support responsive UI on antos tags 2024-03-17 21:18:11 +01:00
DanyLE
a5257bf108 fix: use CSS variable to define color palette for UI theme 2024-03-17 21:18:11 +01:00
DanyLE
eac84a3ab8 fix: encode URI component when get file from VFS API 2024-03-17 21:18:11 +01:00
DanyLE
6523fafe91 fix: upload API only submit a task when files are selected 2024-03-17 21:18:11 +01:00
DanyLE
acd36a7a29 cleanup code 2024-03-17 21:18:11 +01:00
DanyLE
add5ef77c8 feat: use a separated setting file for each application instead of a single system setting files 2024-03-17 21:18:11 +01:00
DanyLE
4d59b104b9 feat: Introduce API.Task API that allow to track promise object via AntOS announcement system 2024-03-17 21:18:11 +01:00
Dany LE
0ac886f644 Update Jenkinsfile 2024-03-17 21:18:11 +01:00
Dany LE
f52e9a38d2 Update Jenkinsfile 2024-03-17 21:18:11 +01:00
Dany LE
a74cf39152 fix: clean up repo before build 2024-03-17 21:18:11 +01:00
Dany LE
082a85644b fix(Jenkinsfile): use typescript 5.0 as typedoc 0.24 depends on it 2024-03-17 21:18:11 +01:00
DanyLE
a00acf4bbf fix: support passing arguments when pushing a service 2024-03-17 21:18:11 +01:00
DanyLE
bff2c94fa9 remove support for VDB, applications that used SQLite database can now use API provided by the libsqlite package (on MarketPlace) 2024-03-17 21:18:11 +01:00
DanyLE
0f2ab549e8 fix: doc generation use latest typedoc version 2024-03-17 21:18:11 +01:00
DanyLE
cd6a6c373a fix: operator not permit on newer version of typescript 2024-03-17 21:18:11 +01:00
DanyLE
56ca945b0c fix: jquery delaration file compatibility with current typescript sdk 2024-03-17 21:18:11 +01:00
DanyLE
82f35f791e fix: extractZip bug introduced by last commit 2024-03-17 21:18:11 +01:00
DanyLE
b30a2bb44c fix: creating missing directories from file paths when they are not specified in zip meta-data 2024-03-17 21:18:11 +01:00
DanyLE
2c64dfe00d fix: calendar tag displays wrong date at the final week of month in some case 2024-03-17 21:18:11 +01:00
DanyLE
1d1218acbd gridview: allow to update row data 2024-03-17 21:18:11 +01:00
DanyLE
0d8daa36e8 safer way to attach element to data via getter 2024-03-17 21:18:11 +01:00
DanyLE
d72a4c954b Remove old menu element, use stackmenu instead 2024-03-17 21:18:11 +01:00
DanyLE
5f92e41021 ListView: add API to scroll the list to top/bottom 2024-03-17 21:18:11 +01:00
DanyLE
d3540d7575 fix: label shall only allow to show text instead of html content 2024-03-17 21:18:11 +01:00
DanyLE
60137fb4fb support icoclass_end in Label and Button HTML attribute 2024-03-17 21:18:11 +01:00
DanyLE
7196f9ff57 Update style for GridView, FileView and ListView 2024-03-17 21:18:11 +01:00
DanyLE
1063ae9c4f Only break-word in notification tag 2024-03-17 21:18:11 +01:00
DanyLE
649c7d942a Update Label, Button and ListView
- Label and Button now can set icon on both left and right side of the text
- Fix ListView dropdown bug, and allow the dropdown list to positioned correctly based on its nearest anchored element
2024-03-17 21:18:11 +01:00
DanyLE
b490ae9d42 fix: Dialog scheme not found when it is defined outside of the dialogs namespace 2024-03-17 21:18:11 +01:00
DanyLE
ae91401965 allow to specify user data in some low level VFS interface API 2024-03-17 21:18:11 +01:00
DanyLE
d482d2cad4 add support for drop custom event when drag is enable on an HTMLElement 2024-03-17 21:18:11 +01:00
DanyLE
b2ec7cc8db Add custom dragging event support for all HTMLElement 2024-03-17 21:18:11 +01:00
DanyLE
db006345a9 sportlight only focus on searchbar when on desktop 2024-03-17 21:18:11 +01:00
DanyLE
bf793ec204 Clean up code 2024-03-17 21:18:11 +01:00
DanyLE
cb9ccb576f Hide application when use click on active application dock item 2024-03-17 21:18:11 +01:00
DanyLE
ea160c2ccb fix: prevent scroll on desktop
When focusing on a window which overflows the desktop,
the desktop scrolls automatically to bottom,
even when `overflow: hiddle` is set on CSS.

This tricky hack prevents this to happen
2024-03-17 21:18:11 +01:00
DanyLE
edb427d6c3 fix: notification style 2024-03-17 21:18:11 +01:00
DanyLE
be72a62156 cleanup system services package 2024-03-17 21:18:11 +01:00
DanyLE
77b89c44f7 Fix: style + typo 2024-03-17 21:18:11 +01:00
DanyLE
242df06a28 Rework on Notification API + some sytem packages
- Rename Syslog to SystemReport
- All services previously on SystemReport now moved to the dedicated SystemServices Packages
- Rework on a more versatile notification GUI and API
- Applications now can display a local toast message instead of pushing a global notification message
2024-03-17 21:18:11 +01:00
DanyLE
c52e4b649e fix: window menu display bug 2024-03-17 21:18:11 +01:00
DanyLE
30c63748b0 Hide spotlight when an application is selected on appdock 2024-03-17 21:18:11 +01:00
DanyLE
84cfc87ce0 Re introduce the vboxchange, hboxchange events as many applications use it 2024-03-17 21:18:11 +01:00
DanyLE
f21a958ea0 Change color theme of the startup menu 2024-03-17 21:18:11 +01:00
DanyLE
81d13c1601 Update favicon to new color 2024-03-17 21:18:11 +01:00
DanyLE
9f07a86901 Update favicon 2024-03-17 21:18:11 +01:00
Dany LE
7b3072576c Update Makefile 2024-03-17 21:18:11 +01:00
Dany LE
be73a3c7ae Update Jenkinsfile 2024-03-17 21:18:11 +01:00
Dany LE
038823a7cb Update README.md 2024-03-17 21:18:11 +01:00
DanyLE
70301d8817 Update Jenkinsfile 2024-03-17 21:18:11 +01:00
DanyLE
9d1c66fe50 Fix make file 2024-03-17 21:18:11 +01:00
DanyLE
6948b0e339 Fix make file 2024-03-17 21:18:11 +01:00
DanyLE
b35812bb43 Update icons + add documentation build in Jenkinsfile 2024-03-17 21:18:11 +01:00
DanyLE
11fb8c97af Add favicon to the page 2024-03-17 21:18:11 +01:00
DanyLE
d9314fc829 Add official AntOS icon 2024-03-17 21:18:11 +01:00
Dany LE
722e672947 Update README.md 2024-03-17 21:18:11 +01:00
Dany LE
93b58c7aa2 Add files via upload 2024-03-17 21:18:11 +01:00
Dany LE
9e7c7e6d78 Update README.md 2024-03-17 21:18:11 +01:00
Dany LE
25e9efff46 Add files via upload 2024-03-17 21:18:11 +01:00
Dany LE
9baee31c01 Add files via upload 2024-03-17 21:18:11 +01:00
DanyLE
de7f027940 update README 2024-03-17 21:18:11 +01:00
DanyLE
870b1ec105 update Syslog application + README 2024-03-17 21:18:11 +01:00
DanyLE
03cee726ed UI improvement + use lastest boostrap icon
- Update bootstrap icons to latest
- Redesign system tray for services monitoring
- Improve UI + bug fix on default packages
2024-03-17 21:18:11 +01:00
DanyLE
303fc9ba20 fix minor bug on appdock contextmenu handling 2024-03-17 21:18:11 +01:00
DanyLE
243dfa7a89 Update dark theme 2024-03-17 21:18:11 +01:00
DanyLE
bc77329294 Improve UI 2024-03-17 21:18:11 +01:00
DanyLE
e1c1895070 Improve Firefox support + fix list view drag and drop bug 2024-03-17 21:18:11 +01:00
DanyLE
0b80a29d00 improve file icon view 2024-03-17 21:18:11 +01:00
DanyLE
b026b96bf2 Redesign the login form, preload all web font fonts on front page 2024-03-17 21:18:11 +01:00
DanyLE
3c25d8b52e Support pinned app in dock + remove old pinned apps UI 2024-03-17 21:18:11 +01:00
DanyLE
b5863702cb Improve application dock:
- Stack all instances of the same application to one single dock button
- Make the dock scrollable by mouse wheel or touch
2024-03-17 21:18:11 +01:00
DanyLE
2ae390ad4b generate 2.0.0 release archive 2024-03-17 21:18:11 +01:00
DanyLE
2e887851c5 Add input tag and update all base dialogs to support mobile devices 2024-03-17 21:18:11 +01:00
DanyLE
1a6ece4e7c Add stack panel component + redesign MarketPlace UI
- Continue improve UI elements
- Add stack panel UI tag
- Redesign MarketPlace UI to support mobile device
2024-03-17 21:18:11 +01:00
DanyLE
d5d6a16a85 Update version to 2.0.0-a 2024-03-17 21:18:11 +01:00
DanyLE
cd294f58a6 Rework on AntOS core to provide support to both mobile and desktop devices (experimental):
- Redesign the core UI API and tags to support Mobile devices
- Add new StackMenu tag
- Support touch events handling on touch devices
- Redesign File and Setting to work on mobile
- Improve Anouncement API
- Rework on default themes
2024-03-17 21:18:11 +01:00
Dany LE
13969511a5
Update README.md
Some checks failed
gitea-sync/antos/pipeline/head This commit looks good
gitea-sync/antos-frontend/pipeline/head There was a failure building this commit
2022-12-31 13:22:52 +01:00
Dany LE
dd56643e01
Update README.md
All checks were successful
gitea-sync/antos/pipeline/head This commit looks good
2022-12-31 13:16:40 +01:00
Dany LE
576051aca0
Update README.md
All checks were successful
gitea-sync/antos/pipeline/head This commit looks good
2022-12-31 13:15:20 +01:00
Dany LE
fec05d115f
Update README.md
All checks were successful
gitea-sync/antos/pipeline/head This commit looks good
2022-12-31 13:10:32 +01:00
Dany LE
5313f0b224
add screenshot
All checks were successful
gitea-sync/antos/pipeline/head This commit looks good
2022-12-31 13:09:42 +01:00
Dany LE
25d1c5fd47
Update README.md
All checks were successful
gitea-sync/antos/pipeline/head This commit looks good
2022-12-31 12:18:14 +01:00
Dany LE
2620d2ccb6
add build status
All checks were successful
gitea-sync/antos/pipeline/head This commit looks good
2022-10-05 21:32:40 +02:00
Dany LE
d1fdb47ca2
Update Jenkinsfile
All checks were successful
gitea-sync/antos/pipeline/head This commit looks good
2022-09-29 19:17:19 +02:00
Dany LE
31d928c977
Delete _config.yml
All checks were successful
gitea-sync/antos/pipeline/head This commit looks good
2022-09-16 15:31:42 +02:00
Dany LE
50e74cef5f
Delete .travis.yml
All checks were successful
antos-devel/pipeline/head This commit looks good
gitea-sync/antos/pipeline/head This commit looks good
2022-09-16 13:26:12 +02:00
Dany LE
cbb8948dbf
Generate build script per branch
All checks were successful
antos-devel/pipeline/head This commit looks good
2022-09-16 13:18:56 +02:00
Dany LE
c377a80d0d
export dts to build dir 2022-09-16 12:44:02 +02:00
Dany LE
f466cf1200
Update change logs on v1.2.1 2022-09-16 12:41:38 +02:00
Dany LE
53867c9d03
Separate build based on current branch name 2022-09-16 11:53:15 +02:00
Dany LE
4cd271cc51
Merge pull request #45 from lxsang/1.2.1
release v1.2.1

* 9b5da17 - App name now can differ from pkgname
* b381294 - fix: fix icon display problem when application is installed, remove all related settings when an application is uinstalled
* b6c90e5 - update image path in readme
* 14b72ef - Fix dragndrop bug on Fileview (grid mod)
* c96919e - fix: correct jenkins build demo stage
* 1cf7181 - Fix fileview status incorrect, add more build stage to Jenkinsfile
* 255f9dc - update readme file, and include it to delivery
* d08b33a - fix ar generation problem: with new version format
* da5bbda - Allow to set version number and build ID to the current Antos build
* 699c697 - update login form style
* 2fd4bb5 - Bug fix + improvement
* 6cbb463 - Fileview: list view display modified date instead of mime
* f7081ae - Include current Antos version to login screen
* 5d17c42 - Makefile read current version from gcode
* 583a0c0 - update version number in code
* c0603cd - Minor style fixes on menus and dropdown list
* 8b029c2 - fix minor visual bug on grid view, list view and tree view
* 86bcaf9 - visual bug fix on label: inline block by default
* 61de957 - Visual improvements
* 52af4b6 - fix visualize bug after style changes
* e63cae1 - style improvement on Label, FileView, GridView, system menu  and app Panel
* f97a45b - Add more control to mem file + bug fix on File
* fdcc5ce - allow to create memory-based temporal VFS file system
* 81d78aa - robusify VFS mem file handling
* d109d6a - fix: file name display inconsitent between views
* c26e27d - Fix multiple dialogs focus bug
* 8b23ebe - Loading animation is now based on the current context (global or application context)
* 2cdd8f9 - support dnd and grid sort
* 079af3b - fix type conversion error in gridview tag
* a6d725e - User a custom tag to manage the desktop instead of GUI
* 0624f42 - API improvement & bug fix: - subscribed global event doesnt unsubcribed when process is killed - process killall API doesnt work as expected - improve core API
* 3a24df1 - update announcement system
* e345a61 - update bootstrap icons to v.1.7.1
* b3d38cc - allow multiple files upload in single request
* 66e96cc - update VFS API
* 86a94a8 - update GUI API
* 27ac7c0 - Minor bug fix on desktop handling
* 99e0d02 - enable setting blur overlay window
* 52709d5 - improve Window GUI API
* 9c06d88 - AntOS load automatically custom VFS handles if available
* c23cb1b - Improve core API: - improve OS exit API - improve VFS API
2022-09-16 11:47:05 +02:00
DanyLE
9b5da170e7 App name now can differ from pkgname 2022-08-25 01:20:51 +02:00
DanyLE
b381294064 fix: fix icon display problem when application is installed, remove all related settings when an application is uinstalled 2022-08-24 14:45:11 +02:00
Dany LE
a0ee2d1090
Update Jenkinsfile 2022-08-23 18:20:25 +02:00
Dany LE
b6c90e56cd
update image path in readme 2022-08-22 21:27:57 +02:00
Dany LE
5f29d5b382
Update README.md 2022-08-22 12:59:33 +02:00
Dany LE
b806f20bdb
change image path 2022-08-22 12:58:50 +02:00
Dany LE
19a8610f1c
Update Jenkinsfile 2022-08-22 09:52:10 +02:00
DanyLE
14b72ef425 Fix dragndrop bug on Fileview (grid mod) 2022-08-19 11:15:21 +02:00
Dany LE
c96919ea12 fix: correct jenkins build demo stage 2022-08-04 13:47:55 +02:00
Dany LE
1cf718117f Fix fileview status incorrect, add more build stage to Jenkinsfile 2022-08-04 13:40:02 +02:00
Dany LE
e76a3f4ef1
Update Jenkinsfile 2022-08-02 11:59:21 +02:00
Dany LE
bb8991c1a1
Update Jenkinsfile 2022-08-02 11:57:43 +02:00
Dany LE
558ef1b3ef
Update Jenkinsfile 2022-08-02 11:56:56 +02:00
Dany LE
ef95e55d50
Update README.md 2022-08-01 18:18:31 +02:00
Dany LE
4a2f4483d6
Update README.md 2022-08-01 18:12:27 +02:00
Dany LE
a03feba430
Update README.md 2022-08-01 18:08:13 +02:00
Dany LE
5014cc730f
Update README.md 2022-08-01 17:15:43 +02:00
Dany LE
b3a4dd4bd1
Update README.md 2022-08-01 17:09:08 +02:00
Dany LE
1e230c9f9d
Update Jenkinsfile 2022-08-01 16:48:52 +02:00
Dany LE
2a4a68b902
Create Jenkinsfile 2022-08-01 16:38:54 +02:00
Dany LE
969222b687
Update README.md 2022-07-29 02:02:30 +02:00
DanyLE
76b16f54a3 regen ar 2022-07-22 20:35:54 +02:00
DanyLE
255f9dc285 update readme file, and include it to delivery 2022-07-22 20:35:21 +02:00
DanyLE
b26a62464d regen archive 2022-07-22 20:19:46 +02:00
DanyLE
d08b33a0b9 fix ar generation problem: with new version format 2022-07-22 20:19:10 +02:00
DanyLE
09e12437a8 fix ar generation problem: with new version format 2022-07-22 20:17:47 +02:00
DanyLE
e4ccfedbd8 regen archive 2022-07-22 20:13:45 +02:00
DanyLE
da5bbdab60 Allow to set version number and build ID to the current Antos build 2022-07-22 20:13:08 +02:00
DanyLE
699c697344 update login form style 2022-07-22 18:04:56 +02:00
DanyLE
2fd4bb5c96 Bug fix + improvement:
- About dialog now can display package README file
- Fix: double click handling on grid view
- add README to packages
- support source map in release version
2022-07-19 20:57:12 +02:00
DanyLE
6cbb463f6f Fileview: list view display modified date instead of mime 2022-07-17 22:21:10 +02:00
DanyLE
f7081ae48a Include current Antos version to login screen 2022-07-17 15:05:59 +02:00
DanyLE
5d17c429f7 Makefile read current version from gcode 2022-07-17 14:29:39 +02:00
DanyLE
583a0c0349 update version number in code 2022-07-17 14:20:56 +02:00
DanyLE
c0603cd47d Minor style fixes on menus and dropdown list 2022-07-17 13:10:02 +02:00
DanyLE
66af4d63f6 regen archive 2022-07-05 20:02:26 +02:00
DanyLE
8b029c2e0a fix minor visual bug on grid view, list view and tree view 2022-07-05 20:02:01 +02:00
DanyLE
86bcaf9dea visual bug fix on label: inline block by default 2022-07-05 15:25:19 +02:00
DanyLE
084c377bcf visual bug fix on label 2022-07-05 15:10:17 +02:00
DanyLE
61de95788c Visual improvements 2022-07-05 15:04:18 +02:00
DanyLE
52af4b6940 fix visualize bug after style changes 2022-07-05 14:41:34 +02:00
DanyLE
e63cae1550 style improvement on Label, FileView, GridView, system menu and app Panel 2022-07-05 13:22:51 +02:00
DanyLE
b2ce9ec978 Minor improvement
- Use arrow up key on File widget to navigate to parent directory
- Application will trigger a "launched" event when opened
2022-07-04 19:59:34 +02:00
DanyLE
f97a45be15 Add more control to mem file + bug fix on File 2022-07-02 21:36:07 +02:00
DanyLE
fdcc5ce784 allow to create memory-based temporal VFS file system 2022-07-02 18:52:50 +02:00
Dany LE
81d78aa8e5 robusify VFS mem file handling 2022-07-02 16:28:21 +02:00
DanyLE
5734ce8ca9 regen ar 2022-05-30 12:18:17 +02:00
DanyLE
7f0ae264fe regen ar 2022-05-30 12:14:10 +02:00
DanyLE
d109d6af39 fix: file name display inconsitent between views 2022-05-30 12:13:29 +02:00
DanyLE
c26e27d7ec Fix multiple dialogs focus bug 2022-05-24 17:35:27 +02:00
DanyLE
c2e72a067e regen ar 2022-05-24 15:41:25 +02:00
DanyLE
8b23ebeeff Loading animation is now based on the current context (global or application context) 2022-05-24 15:39:58 +02:00
DanyLE
deae7bbc57 regen archive 2022-03-31 09:27:30 +02:00
DanyLE
9c5272757a update archive 2022-03-29 18:26:04 +02:00
DanyLE
2cdd8f9a43 support dnd and grid sort 2022-03-29 18:25:07 +02:00
Dany LE
079af3b0ce fix type conversion error in gridview tag 2022-02-23 14:33:21 +01:00
Dany LE
6af1260918 regen release for test 2021-11-26 18:25:37 +01:00
Dany LE
a1d26a6069 regen release 2021-11-26 18:24:09 +01:00
Dany LE
dc2a7c36d8 regen release 2021-11-26 18:22:42 +01:00
Dany LE
15f8de8910 regen release 2021-11-26 18:17:44 +01:00
Dany LE
f912e57e3b regen release 2021-11-26 18:14:10 +01:00
Dany LE
a6d725ea71 User a custom tag to manage the desktop instead of GUI 2021-11-26 00:03:01 +01:00
Dany LE
0624f421f7 API improvement & bug fix:
- subscribed global event doesnt unsubcribed when process is killed
- process killall API doesnt work as expected
- improve core API
2021-11-25 01:13:16 +01:00
Dany LE
64094c29d5 update 2021-11-24 23:42:55 +01:00
Dany LE
d208aea1b9 update 2021-11-24 23:37:06 +01:00
Dany LE
149566ec5d update 2021-11-24 23:21:23 +01:00
Dany LE
3c67b1c134 fix bug 2021-11-24 22:37:49 +01:00
Dany LE
3a24df169c update announcement system 2021-11-24 22:15:25 +01:00
Dany LE
e345a61269 update bootstrap icons to v.1.7.1 2021-11-21 19:45:02 +01:00
Dany LE
b3d38ccb05 allow multiple files upload in single request 2021-11-21 19:19:09 +01:00
Dany LE
66e96cc0db update VFS API 2021-10-24 20:56:18 +02:00
Dany LE
86a94a817a update GUI API 2021-10-24 20:37:19 +02:00
Dany LE
27ac7c06fc Minor bug fix on desktop handling 2021-10-24 19:44:21 +02:00
Dany LE
99e0d02581 enable setting blur overlay window 2021-10-24 18:34:08 +02:00
Dany LE
52709d5da4 improve Window GUI API 2021-10-24 00:27:17 +02:00
Xuan Sang LE
b399958f69
Update README.md 2021-10-15 18:17:26 +02:00
lxsang
aab242c1f7 cleanup 2021-10-12 22:36:20 +02:00
lxsang
043214c001 cleanup 2021-10-12 22:35:47 +02:00
lxsang
9c06d88713 AntOS load automatically custom VFS handles if available 2021-10-12 22:26:40 +02:00
lxsang
5b125c288d update drone file 2021-10-12 22:04:33 +02:00
Xuan Sang LE
8af672748f
Update .drone.yml 2021-10-12 22:04:10 +02:00
Xuan Sang LE
facf51fe8c
Update .drone.yml 2021-10-12 22:03:42 +02:00
Xuan Sang LE
90b6783568
Update .drone.yml 2021-10-12 22:02:27 +02:00
lxsang
c23cb1bfa8 Improve core API:
- improve OS exit API
- improve VFS API
2021-10-12 21:58:57 +02:00
Xuan Sang LE
e6e727246d
Update README.md 2021-06-13 12:41:13 +02:00
Xuan Sang LE
89add530bc
Update README.md 2021-06-13 12:39:17 +02:00
Xuan Sang LE
27ef66eca8
Update README.md 2021-06-13 12:39:02 +02:00
173 changed files with 18088 additions and 13854 deletions

View File

@ -1,20 +0,0 @@
---
kind: pipeline
type: exec
name: default
platform:
os: linux
arch: amd64
clone:
disable: true
steps:
- name: download
commands:
- cd /opt/www/htdocs/os && wget https://github.com/lxsang/antos/raw/next-1.2.0/release/antos-1.2.0.tar.gz
- name: build
commands:
- cd /opt/www/htdocs/os && tar xvzf antos-1.2.0.tar.gz
- rm /opt/www/htdocs/os/antos-1.2.0.tar.gz
trigger:
branch:
- next-1.2.0

22
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,22 @@
name: AntOS front-end
run-name: build Ant-OS front end
on: [push]
jobs:
Build-AntOS-Frontend:
runs-on: ci-tools
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Building AntOS
run: |
ls ${{ gitea.workspace }}
mkdir build
BUILDDIR=$(realpath build) make install_dev main
- name: Save artifact
uses: actions/upload-artifact@v3
with:
name: build-AntOS
path: build/
retention-days: 5
- run: echo "Build status ${{ job.status }}."

View File

@ -1,14 +0,0 @@
language: javascript
node_js:
- 10.21.0
install:
- npm install terser
- npm install uglifycss
- npm install typescript@3.9.3
- npm install jest @types/jest ts-jest
- npm install @types/jquery
script:
- tsc -v
- make
- make test
- make release

View File

@ -4,11 +4,15 @@ BUILDDIR?=/opt/www/htdocs/os
DOCDIR?=/opt/www/htdocs/doc/antos
BLUE=\033[1;34m
NC=\033[0m
TSC=./node_modules/typescript/bin/tsc
UGLIFYJS=./node_modules/terser/bin/terser
UGLIFYCSS=./node_modules/uglifycss/uglifycss
VERSION=1.2.0
VERSION?=2.0.3-b
BUILDID?=master
GSED=sed
UNAME_S := $(shell uname -s)
UNAME_S := $(shell uname -s)cd
ifeq ($(UNAME_S),Darwin)
GSED=gsed
endif
@ -22,7 +26,6 @@ tags = dist/core/tags/tag.js \
dist/core/tags/ListViewTag.js \
dist/core/tags/SwitchTag.js \
dist/core/tags/NSpinnerTag.js \
dist/core/tags/MenuTag.js \
dist/core/tags/GridViewTag.js \
dist/core/tags/TabBarTag.js \
dist/core/tags/TabContainerTag.js \
@ -34,14 +37,18 @@ tags = dist/core/tags/tag.js \
dist/core/tags/FileViewTag.js \
dist/core/tags/OverlayTag.js \
dist/core/tags/AppDockTag.js \
dist/core/tags/SystemPanelTag.js
dist/core/tags/SystemPanelTag.js \
dist/core/tags/DesktopTag.js \
dist/core/tags/StackMenuTag.js \
dist/core/tags/StackPanelTag.js \
dist/core/tags/InputTag.js \
dist/core/tags/NotificationTag.js
javascripts= dist/core/core.js \
dist/core/settings.js \
dist/core/handles/RemoteHandle.js \
dist/core/Announcerment.js \
dist/core/vfs.js \
dist/core/db.js \
dist/core/BaseModel.js \
dist/core/BaseApplication.js \
dist/core/BaseService.js \
@ -54,10 +61,11 @@ javascripts= dist/core/core.js \
antfx = $(tags) \
dist/core/Announcerment.js
packages = Syslog Files MarketPlace Setting NotePad
packages = SystemServices SystemReport Files MarketPlace Setting NotePad
main: initd build_javascripts build_themes libs build_packages languages
- cp src/index.html $(BUILDDIR)/
- cp README.md $(BUILDDIR)/
initd:
- mkdir -p $(BUILDDIR)
@ -68,7 +76,7 @@ lite: build_javascripts build_themes build_packages
ts:
-rm -rf dist
tsc -p tsconfig.json
$(TSC) -p tsconfig.json
cat `find dist/core/ -name "*.d.ts"` > d.ts/antos.d.ts
rm `find dist/ -name "*.d.ts"`
cat d.ts/core.d.ts d.ts/jquery.d.ts d.ts/antos.d.ts > /tmp/corelib.d.ts
@ -85,7 +93,7 @@ standalone_tags: ts
rm "$${f}";\
done
echo "var Ant=this;" >> dist/afx.js
terser dist/afx.js --compress --mangle --output $(BUILDDIR)/afx.js
$(UGLIFYJS) dist/afx.js --compress --mangle --output $(BUILDDIR)/afx.js
# standalone theme
@for f in src/themes/system/afx-*.css; do \
@ -101,7 +109,7 @@ standalone_tags: ts
(cat "$${f}"; echo) >> $(BUILDDIR)/afx.css; \
fi;\
done
# uglifycss --output $(BUILDDIR)/afx.css $(BUILDDIR)/afx.css
# $(UGLIFYCSS) --output $(BUILDDIR)/afx.css $(BUILDDIR)/afx.css
rm -r dist/core
build_javascripts: ts
@ -113,6 +121,7 @@ build_javascripts: ts
(cat "$${f}"; echo) >> dist/antos.js;\
rm "$${f}";\
done
echo 'OS.VERSION.version_string = "$(VERSION)-$(BUILDID)";' >> dist/antos.js
cp dist/antos.js $(BUILDDIR)/scripts/
echo "if(exports){ exports.__esModule = true;exports.OS = OS; }" >> dist/antos.js
rm -r dist/core
@ -137,8 +146,10 @@ build_themes: antos_light antos_dark
-rm -rf $(BUILDDIR)/resources/themes/system/*
-mkdir -p $(BUILDDIR)/resources/themes/system
cp -r src/themes/system/fonts $(BUILDDIR)/resources/themes/system
cp -r src/themes/system/icons $(BUILDDIR)/resources/themes/system
cp -r src/themes/system/wp $(BUILDDIR)/resources/themes/system
for f in src/themes/system/*.css; do (cat "$${f}"; echo) >> $(BUILDDIR)/resources/themes/system/system.css;done
for f in src/themes/default/*.css; do (cat "$${f}"; echo) >> $(BUILDDIR)/resources/themes/system/antos.css;done
antos_light:
@echo "$(BLUE)Building themes name: antos-light$(NC)"
@ -173,27 +184,34 @@ package:
pkgar:
read -r -p "Enter package name: " PKG;\
echo $$PKG | make package &&\
test -f $(BUILDDIR)/packages/$$PKG/main.js && terser $(BUILDDIR)/packages/$$PKG/main.js --compress --mangle --output $(BUILDDIR)/packages/$$PKG/main.js;\
test -f $(BUILDDIR)/packages/$$PKG/main.css && uglifycss --output $(BUILDDIR)/packages/$$PKG/main.css $(BUILDDIR)/packages/$$PKG/main.css;\
test -f $(BUILDDIR)/packages/$$PKG/main.js && $(UGLIFYJS) $(BUILDDIR)/packages/$$PKG/main.js --compress --mangle --output $(BUILDDIR)/packages/$$PKG/main.js;\
test -f $(BUILDDIR)/packages/$$PKG/main.css && $(UGLIFYCSS) --output $(BUILDDIR)/packages/$$PKG/main.css $(BUILDDIR)/packages/$$PKG/main.css;\
cd $(BUILDDIR)/packages/$$PKG && zip -r "$$PKG.zip" ./ ; \
cd ../../ && (test -d repo/$$PKG || mkdir repo/$$PKG) && mv packages/$$PKG/"$$PKG.zip" repo/$$PKG && touch repo/$$PKG/$$PKG.md && rm -r packages/$$PKG
uglify:
# sudo npm install terser -g
#
terser $(BUILDDIR)/scripts/antos.js --compress --mangle --output $(BUILDDIR)/scripts/antos.js
# sudo npm install $(UGLIFYJS) -g
#
mv $(BUILDDIR)/scripts/antos.js $(BUILDDIR)/scripts/antos_src.js
cp $(BUILDDIR)/scripts/antos_src.js ./
$(UGLIFYJS) antos_src.js --compress --mangle --output antos.js --source-map "url='antos.js.map'"
mv antos.js* $(BUILDDIR)/scripts/
rm antos_src.js
# uglify tags
# npm install uglifycss -g
# npm install $(UGLIFYCSS) -g
# uglify the css
uglifycss --output $(BUILDDIR)/resources/themes/antos_light/antos_light.css $(BUILDDIR)/resources/themes/antos_light/antos_light.css
uglifycss --output $(BUILDDIR)/resources/themes/antos_dark/antos_dark.css $(BUILDDIR)/resources/themes/antos_dark/antos_dark.css
uglifycss --output $(BUILDDIR)/resources/themes/system/system.css $(BUILDDIR)/resources/themes/system/system.css
$(UGLIFYCSS) --output $(BUILDDIR)/resources/themes/antos_light/antos_light.css $(BUILDDIR)/resources/themes/antos_light/antos_light.css
$(UGLIFYCSS) --output $(BUILDDIR)/resources/themes/antos_dark/antos_dark.css $(BUILDDIR)/resources/themes/antos_dark/antos_dark.css
$(UGLIFYCSS) --output $(BUILDDIR)/resources/themes/system/system.css $(BUILDDIR)/resources/themes/system/system.css
#uglify each packages
for d in $(packages); do\
echo "Uglifying $$d";\
test -f $(BUILDDIR)/packages/$$d/main.js && terser $(BUILDDIR)/packages/$$d/main.js --compress --mangle --output $(BUILDDIR)/packages/$$d/main.js;\
test -f $(BUILDDIR)/packages/$$d/main.css && uglifycss --output $(BUILDDIR)/packages/$$d/main.css $(BUILDDIR)/packages/$$d/main.css;\
test -f $(BUILDDIR)/packages/$$d/main.js && \
$(UGLIFYJS) $(BUILDDIR)/packages/$$d/main.js \
--compress --mangle --output $(BUILDDIR)/packages/$$d/main.js;\
test -f $(BUILDDIR)/packages/$$d/main.css && $(UGLIFYCSS) --output $(BUILDDIR)/packages/$$d/main.css $(BUILDDIR)/packages/$$d/main.css;\
done
ar:
@ -206,13 +224,25 @@ ar:
echo -n $(VERSION) > release/latest
release: main uglify
.PHONY: doc release clean
doc:
./node_modules/.bin/typedoc --mode file --excludeNotExported --hideGenerator --name "AntOS API" --out $(DOCDIR)
# npm install typedoc --save-dev
# npm install typedoc-plugin-merge-modules --save-dev
# ./node_modules/.bin/typedoc --mode file --excludeNotExported --hideGenerator --name "AntOS $(VERSION)-$(BUILDID) API" --out $(DOCDIR)
./node_modules/.bin/typedoc --hideGenerator --plugin typedoc-plugin-merge-modules --entryPointStrategy expand --name "AntOS $(VERSION)-$(BUILDID) API" --out $(DOCDIR)
test: build_javascripts
jest
install_dev:
npm init -y
npm install terser
npm install uglifycss
npm install typescript@5.0
npm install @types/jquery
npm i typedoc
npm i typedoc-plugin-merge-modules
clean:
rm -rf $(BUILDDIR)/resources
rm -rf $(BUILDDIR)/scripts

View File

@ -1,18 +1,81 @@
# antOS v1.2.0
# AntOS frontend
[![Build Status](https://travis-ci.org/lxsang/antos.svg?branch=master)](https://travis-ci.org/lxsang/antos)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Flxsang%2Fantos.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Flxsang%2Fantos?ref=badge_shield)
Frontend implementation of AntOS remote desktop environment: [https://github.com/antos-rde](https://github.com/antos-rde).
AntOS is a front-end system and API that implement the traditional desktop UI environment on the web browser. The front-end can connect to a remote server and acts as a virtual desktop environment (VDE). The original purpose of AntOS is to offer: (1) visual tools to access and control resource on remote server
and embedded linux environment; (2) front-end API for SaaS web-based applications. With its application API and the provided SDK, AntOS facilitates the
development and deployment of user specific applications inside de VDE environment.
The frontend is developed in typescript/javascript + CSS, it provides the
Core API, web-based window manager, application APIs, a GUI toolkit, and file system abstractions. It also includes an application store and an SDK for in-browser application development, deployment, and packaging. The frontend is designed to work across devices, including desktop computers and mobile devices.
![https://os.lxsang.me/VFS/shared/d4645d65b3e4bb348f1bde0d42598ad9b99367f5](https://os.lxsang.me/VFS/shared/d4645d65b3e4bb348f1bde0d42598ad9b99367f5)
## Build
Github: [https://github.com/lxsang/antos](https://github.com/lxsang/antos)
`Nodejs` and `npm` is necessary to build the project:
```sh
# install dependencies packages
make install_dev
# build release
BUILDDIR=/path/to/output make release
# see more in Makefile for more build target
```
## demo
A demo of the VDE is available at [https://app.iohub.dev/antos/](https://app.iohub.dev/antos/) using username: demo and password: demo.
If one want to run AntOS VDE locally in their system, a docker image is available at:
[https://github.com/lxsang/antosaio](https://github.com/lxsang/antosaio)
## AntOS applications (Available on the MarketPlace)
[https://github.com/lxsang/antosdk-apps](https://github.com/lxsang/antosdk-apps)
## Frontend Documentation
- API: [https://ci.iohub.dev/public/antos%2Drelease/doc/2.0.x/](https://ci.iohub.dev/public/antos%2Drelease/doc/2.0.x/)
## Change logs
* Improvement GUI API
### v.2.0.0
- Work In Progress: The UI is redesigned to support mobile device
### V1.2.1
- 9b5da17 - App name now can differ from pkgname
- b381294 - fix: fix icon display problem when application is installed, remove all related settings when an application is uinstalled
- b6c90e5 - update image path in readme
- 14b72ef - Fix dragndrop bug on Fileview (grid mod)
- c96919e - fix: correct jenkins build demo stage
- 1cf7181 - Fix fileview status incorrect, add more build stage to Jenkinsfile
- 255f9dc - update readme file, and include it to delivery
- d08b33a - fix ar generation problem: with new version format
- da5bbda - Allow to set version number and build ID to the current Antos build
- 699c697 - update login form style
- 2fd4bb5 - Bug fix + improvement
- 6cbb463 - Fileview: list view display modified date instead of mime
- f7081ae - Include current Antos version to login screen
- 5d17c42 - Makefile read current version from gcode
- 583a0c0 - update version number in code
- c0603cd - Minor style fixes on menus and dropdown list
- 8b029c2 - fix minor visual bug on grid view, list view and tree view
- 86bcaf9 - visual bug fix on label: inline block by default
- 61de957 - Visual improvements
- 52af4b6 - fix visualize bug after style changes
- e63cae1 - style improvement on Label, FileView, GridView, system menu and app Panel
- f97a45b - Add more control to mem file + bug fix on File
- fdcc5ce - allow to create memory-based temporal VFS file system
- 81d78aa - robusify VFS mem file handling
- d109d6a - fix: file name display inconsitent between views
- c26e27d - Fix multiple dialogs focus bug
- 8b23ebe - Loading animation is now based on the current context (global or application context)
- 2cdd8f9 - support dnd and grid sort
- 079af3b - fix type conversion error in gridview tag
- a6d725e - User a custom tag to manage the desktop instead of GUI
- 0624f42 - API improvement & bug fix: - subscribed global event doesnt unsubcribed when process is killed - process killall API doesnt work as expected - improve core API
- 3a24df1 - update announcement system
- e345a61 - update bootstrap icons to v.1.7.1
- b3d38cc - allow multiple files upload in single request
- 66e96cc - update VFS API
- 86a94a8 - update GUI API
- 27ac7c0 - Minor bug fix on desktop handling
- 99e0d02 - enable setting blur overlay window
- 52709d5 - improve Window GUI API
- 9c06d88 - AntOS load automatically custom VFS handles if available
- c23cb1b - Improve core API: - improve OS exit API - improve VFS API
### V.1.2.0 Improvement GUI API
- [x] File dialog should remember last opened folder
- [x] Add dynamic key-value dialog that work on any object
- [x] Window list panel should show window title in tooltip when mouse hovering on application icon
@ -41,23 +104,9 @@ Github: [https://github.com/lxsang/antos](https://github.com/lxsang/antos)
- Introduce new JSON based syntax for SDK task/target definition
* From this version, docker image of All-in-one AntOS system is available at: [https://hub.docker.com/r/xsangle/antosaio](https://hub.docker.com/r/xsangle/antosaio)
## Demo
A demo of the VDE is available at [https://app.iohub.dev/antos/](https://app.iohub.dev/antos/) using username: demo and password: demo.
If one want to run AntOS VDE locally in their system, a docker image is available at:
[https://github.com/lxsang/antosaio](https://github.com/lxsang/antosaio)
## AntOS applications (Available on the MarketPlace)
[https://github.com/lxsang/antosdk-apps](https://github.com/lxsang/antosdk-apps)
## Documentation
- Documentation: [https://doc.iohub.dev/antos](https://doc.iohub.dev/antos)
- API: (https://doc.iohub.dev/antos/api/)[https://doc.iohub.dev/antos/api/]
## Licence
Copyright 2017-2021 Xuan Sang LE <mrsang AT iohub DOT dev>
Copyright 2017-2022 Xuan Sang LE <mrsang AT iohub DOT dev>
AnTOS is is licensed under the GNU General Public License v3.0, see the LICENCE file for more information
@ -74,3 +123,4 @@ AnTOS is is licensed under the GNU General Public License v3.0, see the LICENCE
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
**For comercial use, please contact author**

View File

@ -1 +0,0 @@
theme: jekyll-theme-slate

17223
d.ts/antos.d.ts vendored

File diff suppressed because it is too large Load Diff

3
d.ts/jquery.d.ts vendored
View File

@ -1,7 +1,4 @@
export as namespace Sizzle;
declare const Sizzle: SizzleStatic;
export = Sizzle;
interface SizzleStatic {
selectors: Sizzle.Selectors;

BIN
release/antos-1.2.1.tar.gz Normal file

Binary file not shown.

BIN
release/antos-2.0.0.tar.gz Normal file

Binary file not shown.

View File

@ -1 +1 @@
1.2.0
2.0.0

View File

@ -21,5 +21,15 @@ Ant.onload = function () {
"webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange",
() => (Ant.OS.GUI.fullscreen = !Ant.OS.GUI.fullscreen)
);
const agent = navigator.userAgent||navigator.vendor||(window as any).opera;
Ant.OS.mobile = false;
if(
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(agent)
||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(agent.substr(0,4))
)
{
Ant.OS.mobile = true;
}
return Ant.OS.boot();
};

View File

@ -18,6 +18,63 @@
namespace OS {
export namespace API {
/**
* Data type exchanged via
* the global Announcement interface
*
* @export
* @interface AnnouncementDataType
*/
export interface AnnouncementDataType<T> {
/**
* message string
*
* @type {string| FormattedString}
* @memberof AppAnnouncementDataType
*/
message: string | FormattedString;
/**
* Process ID
*
* @type {number}
* @memberof AppAnnouncementDataType
*/
id: number;
/**
* App name
*
* @type {string | FormattedString}
* @memberof AppAnnouncementDataType
*/
name: string | FormattedString;
/**
* Icon file
*
* @type {string}
* @memberof AppAnnouncementDataType
*/
icon?: string;
/**
* App icon class
*
* @type {string}
* @memberof AppAnnouncementDataType
*/
iconclass?: string;
/**
* User specific data
*
* @type {*}
* @memberof AppAnnouncementDataType
*/
u_data?: T;
}
/**
* Observable entry type definition
*
@ -50,21 +107,19 @@ namespace OS {
* @interface AnnouncerListenerType
*/
export interface AnnouncerListenerType {
[index: number]: {
/**
* The event name
*
* @type {string}
*/
e: string;
/**
* The event name
*
* @type {string}
*/
e: string;
/**
* The event callback
*
*/
f: (d: any) => void;
}[];
}
/**
* The event callback
*
*/
f: (d: any) => void;
};
/**
* This class is the based class used in AntOS event
@ -77,7 +132,7 @@ namespace OS {
export class Announcer {
/**
* The observable object that stores event name
* and its corresponding callback in [[ObservableEntryType]]
* and its corresponding callback in {@link ObservableEntryType}
*
* @type {GenericObject<ObservableEntryType>}
* @memberof Announcer
@ -235,33 +290,42 @@ namespace OS {
* and callbacks
*/
export var observable: API.Announcer = new API.Announcer();
/**
* This variable is used to allocate the `id` of all messages
* passing between publishers and subscribers in the
* system announcement
*/
export var quota: 0;
/**
* Placeholder of all global events listeners
*/
export var listeners: API.AnnouncerListenerType = {};
export var listeners: Map<BaseModel | 0, API.AnnouncerListenerType[]> = new Map();
/**
* Subscribe to a global event
*
* @export
* @param {string} e event name
* @param {(d: any) => void} f event callback
* @param {(d: API.AnnouncementDataType<any>) => void} f event callback
* @param {GUI.BaseModel} a the process (Application/service) related to the callback
*/
export function on(e: string, f: (d: any) => void, a: BaseModel): void {
if (!announcer.listeners[a.pid]) {
announcer.listeners[a.pid] = [];
export function on(e: string, f: (d: API.AnnouncementDataType<any>) => void, a?: BaseModel): void {
let key: BaseModel | 0 = 0;
if(a)
key = a;
if (!announcer.listeners.has(key)) {
announcer.listeners.set(key, []);
}
announcer.listeners[a.pid].push({ e, f });
const collection = announcer.listeners.get(key);
collection.push({ e, f });
announcer.observable.on(e, f);
}
/**
* Subscribe to a global event once
*
* @export
* @param {string} e event name
* @param {(d: API.AnnouncementDataType<any>) => void} f event callback
*/
export function one(e: string, f: (d: API.AnnouncementDataType<any>) => void): void {
announcer.observable.one(e, f);
}
/**
* Trigger a global event
*
@ -282,7 +346,7 @@ namespace OS {
* @param {Error} e error to be reported
*/
export function osfail(m: string | FormattedString, e: Error): void {
announcer.ostrigger("fail", { m, e });
announcer.ostrigger("fail", m, e );
}
/**
@ -294,7 +358,7 @@ namespace OS {
* @param {Error} e error to be reported
*/
export function oserror(m: string | FormattedString, e: Error): void {
announcer.ostrigger("error", { m, e });
announcer.ostrigger("error", m, e );
}
/**
@ -304,18 +368,24 @@ namespace OS {
* @param {(string | FormattedString)} m notification message
*/
export function osinfo(m: string | FormattedString): void {
announcer.ostrigger("info", { m, e: null });
announcer.ostrigger("info", m);
}
/**
* trigger a specific global event
*
*
* @export
* @param {string} e event name
* @param {*} d event data
* @param {(string| FormattedString)} m event message
* @param {*} [d] user data
*/
export function ostrigger(e: string, d: any): void {
announcer.trigger(e, { id: 0, data: d, name: "OS" });
export function ostrigger(e: string, m: string| FormattedString, d?: any): void {
const aob: API.AnnouncementDataType<any> = {} as API.AnnouncementDataType<any>;
aob.id = 0;
aob.message = m;
aob.u_data = d;
aob.name = "OS";
announcer.trigger(e, aob);
}
/**
@ -327,27 +397,14 @@ namespace OS {
* @returns {void}
*/
export function unregister(app: BaseModel): void {
if (
!announcer.listeners[app.pid] ||
!(announcer.listeners[app.pid].length > 0)
) {
if (!announcer.listeners.has(app)) {
return;
}
for (let i of announcer.listeners[app.pid]) {
const collection = announcer.listeners.get(app);
for (let i of collection) {
announcer.observable.off(i.e, i.f);
}
delete announcer.listeners[app.pid];
}
/**
* Allocate message id
*
* @export
* @returns {number}
*/
export function getMID(): number {
quota += 1;
return quota;
announcer.listeners.delete(app);
}
}
}

View File

@ -35,9 +35,21 @@ namespace OS {
*/
export abstract class BaseApplication extends BaseModel {
/**
* Placeholder of all settings specific to the application.
* The settings stored in this object will be saved to system
* setting when logout and can be reused in the next login session
* Watcher of all settings specific to the application.
* The settings stored in this object will be saved to application folder
* in JSON format as .settings.json and will be loaded automatically
* when application is initialized.
*
* This object is globally accessible to all processes of the same application
*
* @type {GenericObject<any>}
* @memberof BaseApplication
*/
static setting_wdg: GenericObject<any>;
/**
* Reference to per application setting i.e. setting_wdg
*
* @type {GenericObject<any>}
* @memberof BaseApplication
@ -61,15 +73,6 @@ namespace OS {
*/
sysdock: GUI.tag.AppDockTag;
/**
* Reference to the system application menu located
* on the system panel
*
* @type {GUI.tag.MenuTag}
* @memberof BaseApplication
*/
appmenu: GUI.tag.MenuTag;
/**
*Creates an instance of BaseApplication.
* @param {string} name application name
@ -78,16 +81,8 @@ namespace OS {
*/
constructor(name: string, args: AppArgumentsType[]) {
super(name, args);
if (!setting.applications[this.name]) {
setting.applications[this.name] = {};
}
this.setting = setting.applications[this.name];
this.setting = (this.constructor as any).setting_wdg;
this.keycomb = {};
this.subscribe("appregistry", (m) => {
if (m.name === this.name) {
this.applySetting(m.data.m);
}
});
}
/**
@ -101,53 +96,90 @@ namespace OS {
* @returns {void}
* @memberof BaseApplication
*/
init(): void {
this.off("*");
this.on("exit", () => this.quit(false));
// first register some base event to the app
this.on("focus", () => {
this.sysdock.selectedApp = this;
this.appmenu.pid = this.pid;
this.appmenu.items = this.baseMenu() || [];
this.appmenu.onmenuselect = (
d: GUI.tag.MenuEventData
): void => {
return this.trigger("menuselect", d);
};
if (this.dialog) {
return this.dialog.show();
init(): Promise<any> {
return new Promise(async (ok, nok) =>{
try {
this.off("*");
this.on("exit", () => this.quit(false));
// first register some base event to the app
this.on("focus", () => {
//if(this.sysdock.selectedApp != this)
this.sysdock.selectedApp = this;
(this.scheme as GUI.tag.WindowTag).onmenuopen = (el) => el.nodes = this.baseMenu() || [];
OS.PM.pidactive = this.pid;
this.trigger("focused", undefined);
if (this.dialog) {
return this.dialog.show();
}
});
this.on("hide", () => {
this.sysdock.selectedApp = null;
if (this.dialog) {
return this.dialog.hide();
}
});
this.on("menuselect", (d) => {
switch (d.data.item.data.dataid) {
case `${this.name}-about`:
return this.openDialog("AboutDialog");
case `${this.name}-exit`:
return this.trigger("exit", undefined);
}
});
this.on("apptitlechange", () => this.sysdock.update(this));
this.subscribe("APP-REGISTRY", (m) => {
if (m.name === this.name) {
this.applySetting(m.message as string);
}
});
this.updateLocale(this.systemsetting.system.locale);
await this.loadScheme();
this.applyAllSetting();
}
catch(e)
{
nok(__e(e));
}
});
this.on("hide", () => {
this.sysdock.selectedApp = null;
this.appmenu.items = [];
this.appmenu.pid = -1;
if (this.dialog) {
return this.dialog.hide();
}
});
this.on("menuselect", (d) => {
switch (d.data.item.data.dataid) {
case `${this.name}-about`:
return this.openDialog("AboutDialog");
case `${this.name}-exit`:
return this.trigger("exit", undefined);
}
});
this.on("apptitlechange", () => this.sysdock.update(this));
this.updateLocale(this.systemsetting.system.locale);
return this.loadScheme();
}
/**
* API function to register responsive UI event to the current window tag
*
* @protected
* @param {GUI.TagResponsiveValidator} responsive validator
* @param {GUI.TagResponsiveCallback} responsive callback
* @returns {void}
* @memberof BaseApplication
*/
protected morphon(validator: GUI.TagResponsiveValidator, callback: GUI.TagResponsiveCallback)
{
const win = this.scheme as GUI.tag.WindowTag;
win.morphon(validator, callback);
}
/**
* API function to unregister responsive UI event from current window tag
*
* @protected
* @param {GUI.TagResponsiveValidator} responsive validator
* @returns {void}
* @memberof BaseApplication
*/
protected morphoff(validator: GUI.TagResponsiveValidator)
{
const win = this.scheme as GUI.tag.WindowTag;
win.morphoff(validator);
}
/**
* Render the application UI by first loading its scheme
* and then mount this scheme to the DOM tree
*
* @protected
* @returns {void}
* @returns {Promise<any>}
* @memberof BaseApplication
*/
protected loadScheme(): void {
protected loadScheme(): Promise<any> {
//now load the scheme
const path = `${this.meta().path}/scheme.html`;
return this.render(path);
@ -155,9 +187,8 @@ namespace OS {
/**
* API function to perform an heavy task.
* This function will trigger the global `loading`
* event at the beginning of the task, and the `loaded`
* event after finishing the task
* This function will create a Task that is tracked by any
* task manager implementation
*
* @protected
* @param {Promise<any>} promise the promise on a task to be performed
@ -165,15 +196,11 @@ namespace OS {
* @memberof BaseApplication
*/
protected load(promise: Promise<any>): Promise<void> {
const q = this._api.mid();
return new Promise(async (resolve, reject) => {
this._api.loading(q, this.name);
return this._api.Task(async (resolve, reject) => {
try {
await promise;
this._api.loaded(q, this.name, "OK");
return resolve();
return resolve(undefined);
} catch (e) {
this._api.loaded(q, this.name, "FAIL");
return reject(__e(e));
}
});
@ -195,7 +222,7 @@ namespace OS {
f: (e: JQuery.KeyboardEventBase) => void
): void {
const arr = k.toUpperCase().split("-");
const c = arr.pop();
let c = arr.pop();
let fnk = "";
if (arr.includes("META")) {
fnk += "META";
@ -209,7 +236,10 @@ namespace OS {
if (arr.includes("SHIFT")) {
fnk += "SHIFT";
}
if (fnk == "" && arr.length == 0 && c == "ESC") {
fnk = "ESC";
c = String.fromCharCode(27).toUpperCase();
}
if ( fnk == "") {
return;
}
@ -225,12 +255,11 @@ namespace OS {
* Update the application local from the system
* locale or application specific locale configuration
*
* @private
* @param {string} name locale name e.g. `en_GB`
* @returns {void}
* @memberof BaseApplication
*/
protected updateLocale(name: string): void {
updateLocale(name: string): void {
const meta = this.meta();
if (!meta || !meta.locales) {
return;
@ -289,21 +318,6 @@ namespace OS {
}
}
/**
* Set a setting value to the application setting
* registry
*
* @protected
* @param {string} k setting name
* @param {*} v setting value
* @returns {void}
* @memberof BaseApplication
*/
protected registry(k: string, v: any): void {
this.setting[k] = v;
return this.publish("appregistry", k);
}
/**
* Show the appliation
*
@ -321,10 +335,11 @@ namespace OS {
* @memberof BaseApplication
*/
blur(): void {
if (this.appmenu && this.pid === this.appmenu.pid) {
this.appmenu.items = [];
this.trigger("blur", undefined);
if(this.dialog)
{
this.dialog.blur();
}
return this.trigger("blur", undefined);
}
/**
@ -357,7 +372,17 @@ namespace OS {
title(): string | FormattedString {
return (this.scheme as GUI.tag.WindowTag).apptitle;
}
/**
* Getter to access the application window instance
*
* @memberof BaseApplication
*/
get window(): GUI.tag.WindowTag
{
return this.scheme as GUI.tag.WindowTag;
}
/**
* Function called when the application exit.
* If the input exit event is prevented, the application
@ -371,9 +396,6 @@ namespace OS {
protected onexit(evt: BaseEvent): void {
this.cleanup(evt);
if (!evt.prevent) {
if (this.pid === this.appmenu.pid) {
this.appmenu.items = [];
}
$(this.scheme).remove();
}
}
@ -392,7 +414,7 @@ namespace OS {
* Base menu definition. This function
* returns the based menu definition of all applications.
* Other application specific menu entries
* should be defined in [[menu]] function
* should be defined in {@link menu} function
*
* @protected
* @returns {GUI.BasicItemType[]}
@ -436,7 +458,36 @@ namespace OS {
}
/**
* The cleanup function that is called by [[onexit]] function.
* Show local toast notification
*
* @param {any} data to send
* @param {GUI.ToastOptions} notification options see {@link GUI.ToastOptions}
* @returns {void}
* @memberof BaseApplication
*/
toast(data: any, opts?: GUI.ToastOptions): void {
let options: GUI.ToastOptions = {
location: GUI.ANCHOR.SOUTH_EST,
timeout: 3,
tag: "afx-label"
};
if(opts)
{
for(const k in opts)
{
options[k] = opts[k];
}
}
let d = data;
if(typeof data == "string" || data instanceof FormattedString)
{
d = {text: data};
}
this._gui.toast(d,options, this);
}
/**
* The cleanup function that is called by {@link onexit} function.
* Application need to override this function to perform some
* specific task before exiting or to prevent the application
* to be exited

View File

@ -17,6 +17,7 @@
//along with this program. If not, see https://www.gnu.org/licenses/.
namespace OS {
export namespace GUI {
declare var showdown: any;
/**
* the SubWindow class is the abstract prototype of all
* modal windows or dialogs definition in AntOS
@ -44,6 +45,7 @@ namespace OS {
*/
parent: BaseModel | typeof GUI;
/**
*Creates an instance of SubWindow.
* @param {string} name SubWindow (class) name
@ -56,22 +58,15 @@ namespace OS {
}
/**
* Exit the sub-window
* Purge the model from the system
*
* @returns {void}
* @memberof SubWindow
* @protected
* @memberof BaseModel
*/
quit(): void {
const evt = new BaseEvent("exit", false);
this.onexit(evt);
if (!evt.prevent) {
delete this._observable;
if (this.scheme) {
$(this.scheme).remove();
}
if (this.dialog) {
return this.dialog.quit();
}
protected destroy(): void
{
if (this.scheme) {
$(this.scheme).remove();
}
}
@ -82,10 +77,26 @@ namespace OS {
*
* Need to be implemented by subclasses
*
* @abstract
* @memberof SubWindow
*
* @returns {void}
* @memberof BaseDialog
*/
abstract init(): void;
init(): void {
// show the app if it is not active
this.on("focus",() => {
if((this.pid == -1) || (PM.pidactive == this.pid))
{
return;
}
const app = PM.appByPid(this.pid);
if(app)
{
app.show();
}
});
}
/**
* Main entry point after rendering of the sub-window
@ -115,8 +126,8 @@ namespace OS {
* @memberof SubWindow
*/
show(): void {
this.trigger("focus");
$(this.scheme).css("z-index", GUI.zindex + 2);
this.trigger("focus", undefined);
this.trigger("focused", undefined);
if (this.dialog) {
this.dialog.show();
}
@ -129,8 +140,25 @@ namespace OS {
* @memberof SubWindow
*/
hide(): void {
return this.trigger("hide");
this.trigger("hide", undefined);
if (this.dialog) {
this.dialog.hide();
}
}
/**
* blur the sub-window
*
* @returns {void}
* @memberof SubWindow
*/
blur(): void {
this.trigger("blur", undefined);
if (this.dialog) {
this.dialog.blur();
}
}
}
SubWindow.type = ModelType.SubWindow;
@ -182,6 +210,20 @@ namespace OS {
return (this.parent.dialog = undefined);
}
}
/**
* Show the dialog
*
* @memberof BaseDialog
*/
show(): void {
this.trigger("focus", undefined);
this.trigger("focused", undefined);
if (this.dialog) {
this.dialog.show();
}
}
}
/**
@ -194,6 +236,7 @@ namespace OS {
* @extends {BaseDialog}
*/
export class BasicDialog extends BaseDialog {
['constructor']: typeof BasicDialog
/**
* Placeholder for the UI scheme to be rendered. This can
* be either the string definition of the scheme or
@ -207,7 +250,7 @@ namespace OS {
/**
* If the `markup` variable is not provided, then
* the [[init]] function will find the scheme definition
* the {@link init} function will find the scheme definition
* in this class variable
*
* @static
@ -238,19 +281,19 @@ namespace OS {
* @memberof BasicDialog
*/
init(): void {
super.init();
//this._onenter = undefined;
if (this.markup) {
if (typeof this.markup === "string") {
return GUI.htmlToScheme(this.markup, this, this.host);
} else {
// a file handle
return this.render(this.markup.path);
this.render(this.markup.path);
}
} else if (
GUI.dialogs[this.name] &&
GUI.dialogs[this.name].scheme
this.constructor.scheme
) {
const html: string = GUI.dialogs[this.name].scheme;
const html: string = this.constructor.scheme;
return GUI.htmlToScheme(html.trim(), this, this.host);
} else {
this.error(__("Unable to find dialog scheme"));
@ -264,7 +307,6 @@ namespace OS {
*/
main(): void {
const win = this.scheme as tag.WindowTag;
$(win).attr("tabindex", 0);
$(win).on('keydown', (e) => {
switch (e.which) {
case 27:
@ -284,6 +326,7 @@ namespace OS {
}
win.resizable = false;
win.minimizable = false;
win.menu = undefined;
$(win).trigger("focus");
}
}
@ -329,23 +372,15 @@ namespace OS {
*/
main(): void {
super.main();
const $input = $(this.find("txtInput"));
if (this.data && this.data.label) {
(this.find(
"lbl"
) as tag.LabelTag).text = this.data.label;
}
if (this.data && this.data.value) {
$input.val(this.data.value);
}
if (this.data && this.data.type) {
($input[0] as HTMLInputElement).type = this.data.type
const input = this.find("txtInput") as GUI.tag.InputTag;
if(this.data)
{
input.set(this.data);
}
(this.find("btnOk") as tag.ButtonTag).onbtclick = (_e) => {
if (this.handle) {
this.handle($input.val());
this.handle(input.value);
}
return this.quit();
};
@ -356,49 +391,39 @@ namespace OS {
return this.quit();
};
$input.on("keyup", (e) => {
input.on("keyup", (e) => {
if (e.which !== 13) {
return;
}
if (this.handle) {
this.handle($input.val());
this.handle(input.value);
}
return this.quit();
});
$input.trigger("focus");
input.trigger("focus");
}
}
/**
* Scheme definition of the Prompt dialog
*/
PromptDialog.scheme = `\
<afx-app-window width='200' height='150' apptitle = "Prompt">
<afx-vbox>
<afx-hbox>
<div data-width = "10" ></div>
<afx-vbox>
<div data-height="10" ></div>
<afx-label data-id = "lbl" ></afx-label>
<input type = "text" data-id= "txtInput" ></input>
<div data-height="10" ></div>
<afx-hbox data-height="30">
<div ></div>
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
</afx-hbox>
</afx-vbox>
<div data-width = "10" ></div>
</afx-hbox>
<afx-app-window width='250' height='200' apptitle = "Prompt">
<afx-vbox padding = "10">
<afx-input data-id= "txtInput"></afx-input>
<div data-height="35" style="text-align: right;">
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
</div>
</afx-vbox>
</afx-app-window>\
`;
/**
* A text dialog is similar to a [[PromptDialog]] nut allows
* A text dialog is similar to a {@link PromptDialog} nut allows
* user to input multi-line text.
*
* Refer to [[PromptDialog]] for the definition of input and callback data
* Refer to {@link PromptDialog} for the definition of input and callback data
* of the dialog
*
* @export
@ -421,15 +446,11 @@ namespace OS {
*/
main(): void {
super.main();
const $input = $(this.find("txtInput"));
if (this.data && this.data.value) {
$input.val(this.data.value);
}
if (this.data && this.data.disable) {
$input.prop('disabled', true);
}
const input = this.find("txtInput") as tag.InputTag;
if(this.data)
input.set(this.data);
(this.find("btn-Ok") as tag.ButtonTag).onbtclick = (_e) => {
const value = $input.val();
const value = input.value;
if (!value || value === "") {
return;
}
@ -445,7 +466,7 @@ namespace OS {
return this.quit();
};
$input.focus();
input.trigger("focus");
}
}
/**
@ -453,21 +474,12 @@ namespace OS {
*/
TextDialog.scheme = `\
<afx-app-window data-id = "TextDialog" width='400' height='300'>
<afx-vbox>
<afx-hbox>
<div data-width = "10" ></div>
<afx-vbox>
<div data-height="10" ></div>
<textarea data-id= "txtInput" ></textarea>
<div data-height="10" ></div>
<afx-hbox data-height="30">
<div ></div>
<afx-button data-id = "btn-Ok" text = "__(Ok)" data-width = "40" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
</afx-hbox>
</afx-vbox>
<div data-width = "10" ></div>
</afx-hbox>
<afx-vbox padding="10">
<afx-input data-id= "txtInput" verbose="true"></afx-input>
<div data-height="40" style="text-align:right;padding-top:5px;">
<afx-button data-id = "btn-Ok" text = "__(Ok)" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" ></afx-button>
</div>
</afx-vbox>
</afx-app-window>\
`;
@ -532,23 +544,13 @@ namespace OS {
* Scheme definition
*/
CalendarDialog.scheme = `\
<afx-app-window width='300' height='230' apptitle = "Calendar" >
<afx-vbox>
<afx-hbox>
<div data-width = "10" ></div>
<afx-vbox>
<div data-height="10" ></div>
<afx-calendar-view data-id = "cal" ></afx-calendar-view>
<div data-height="10" ></div>
<afx-hbox data-height="30">
<div ></div>
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
</afx-hbox>
<div data-height="10" ></div>
</afx-vbox>
<div data-width = "10" ></div>
</afx-hbox>
<afx-app-window width='350' height='380' apptitle = "Calendar" >
<afx-vbox padding="10">
<afx-calendar-view data-id = "cal" ></afx-calendar-view>
<div data-height="35" style = 'text-align: right;'>
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
</div>
</afx-vbox>
</afx-app-window>\
`;
@ -563,7 +565,7 @@ namespace OS {
* title: string // window title
* }
* ```
* Callback data: [[ColorType]] object
* Callback data: {@link ColorType} object
*
* @export
* @class ColorPickerDialog
@ -611,23 +613,13 @@ namespace OS {
* Scheme definition
*/
ColorPickerDialog.scheme = `\
<afx-app-window width='320' height='250' apptitle = "Color picker" >
<afx-vbox>
<afx-hbox>
<div data-width = "10" ></div>
<afx-vbox>
<div data-height="10" ></div>
<afx-color-picker data-id = "cpicker" ></afx-color-picker>
<div data-height="10" ></div>
<afx-hbox data-height="30">
<div ></div>
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
</afx-hbox>
<div data-height="10" ></div>
</afx-vbox>
<div data-width = "10" ></div>
</afx-hbox>
<afx-app-window width='320' height='300' apptitle = "Color picker" >
<afx-vbox padding = "10">
<afx-color-picker data-id = "cpicker" ></afx-color-picker>
<div data-height="35" style = "text-align: right;">
<afx-button data-id = "btnOk" text = "__(Ok)" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" ></afx-button>
</div>
</afx-vbox>
</afx-app-window>\
`;
@ -672,7 +664,7 @@ namespace OS {
}
for (let k in this.data) {
const v = this.data[k];
rows.push([{ text: k }, { text: v }]);
rows.push([{ text: k }, { text: v, selectable: true }]);
}
const grid = this.find("grid") as tag.GridViewTag;
grid.header = [
@ -691,22 +683,12 @@ namespace OS {
* Scheme definition
*/
InfoDialog.scheme = `\
<afx-app-window width='250' height='300' apptitle = "Info" >
<afx-vbox>
<afx-hbox>
<div data-width = "10" ></div>
<afx-vbox>
<div data-height="10" ></div>
<afx-grid-view data-id = "grid" ></afx-grid-view>
<div data-height="10" ></div>
<afx-hbox data-height="30">
<div ></div>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
</afx-hbox>
<div data-height="10" ></div>
</afx-vbox>
<div data-width = "10" ></div>
</afx-hbox>
<afx-app-window width='300' height='350' apptitle = "Info" >
<afx-vbox padding = "10">
<afx-grid-view data-id = "grid" focusable="true"></afx-grid-view>
<div data-height="35" style="text-align: right;">
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
</div>
</afx-vbox>
</afx-app-window>\
`;
@ -772,22 +754,13 @@ namespace OS {
* Scheme definition
*/
YesNoDialog.scheme = `\
<afx-app-window width='200' height='150' apptitle = "Prompt">
<afx-vbox>
<afx-hbox>
<div data-width = "10" ></div>
<afx-vbox>
<div data-height="10" ></div>
<afx-label data-id = "lbl" ></afx-label>
<div data-height="10" ></div>
<afx-hbox data-height="30">
<div ></div>
<afx-button data-id = "btnYes" text = "__(Yes)" data-width = "40" ></afx-button>
<afx-button data-id = "btnNo" text = "__(No)" data-width = "40" ></afx-button>
</afx-hbox>
</afx-vbox>
<div data-width = "10" ></div>
</afx-hbox>
<afx-app-window width='250' height='200' apptitle = "Warning">
<afx-vbox padding = "10">
<afx-label data-id = "lbl" valign="top" ></afx-label>
<div data-height="35" style = "text-align: right;">
<afx-button data-id = "btnYes" text = "__(Yes)" ></afx-button>
<afx-button data-id = "btnNo" text = "__(No)"></afx-button>
</div>
</afx-vbox>
</afx-app-window>\
`;
@ -858,22 +831,13 @@ namespace OS {
* Scheme definition
*/
SelectionDialog.scheme = `\
<afx-app-window width='250' height='300' apptitle = "Selection">
<afx-vbox>
<afx-hbox>
<div data-width = "10" ></div>
<afx-vbox>
<div data-height="10" ></div>
<afx-list-view data-id = "list" ></afx-list-view>
<div data-height="10" ></div>
<afx-hbox data-height="30">
<div ></div>
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
</afx-hbox>
</afx-vbox>
<div data-width = "10" ></div>
</afx-hbox>
<afx-app-window width='350' height='300' apptitle = "Selection">
<afx-vbox padding = "10">
<afx-list-view data-id = "list" focusable="true"></afx-list-view>
<div data-height="35" style = "text-align: right;">
<afx-button data-id = "btnOk" text = "__(Ok)" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
</div>
</afx-vbox>
</afx-app-window>\
`;
@ -917,19 +881,23 @@ namespace OS {
iconclass: mt.iconclass,
text: `${mt.name}(v${mt.version})`,
});
$(this.find("mydesc")).html(mt.description);
$(this.find("mydesc")).html(`${mt.description} <br/> ${mt.info.author} (${mt.info.email})`);
// grid data for author info
if (!mt.info) {
return;
}
const rows = [];
for (let k in mt.info) {
const v = mt.info[k];
rows.push([{ text: k }, { text: v }]);
}
const grid = this.find("mygrid") as tag.GridViewTag;
grid.header = [{ text: "", width: 100 }, { text: "" }];
grid.rows = rows;
`pkg://${mt.pkgname?mt.pkgname:mt.app}/README.md`
.asFileHandle()
.read()
.then(async (data) => {
let _ = await API.requires("os://scripts/showdown.min.js");
const converter = new showdown.Converter();
const html = converter.makeHtml(data);
const el = this.find("read-me");
$(el).html(html);
$("img", el).css("width", "100%");
})
.catch(e => {});
(this.find("btnCancel") as tag.ButtonTag).onbtclick = (
_e
): void => {
@ -941,24 +909,19 @@ namespace OS {
* Scheme definition
*/
AboutDialog.scheme = `\
<afx-app-window data-id = 'about-window' width='300' height='200'>
<afx-vbox>
<afx-app-window data-id = 'about-window' width='550' height='450'>
<afx-vbox padding = "5">
<div style="text-align:center; margin-top:10px;" data-height="50">
<h3 style = "margin:0;padding:0;">
<afx-label data-id = 'mylabel'></afx-label>
<afx-label data-id = 'mylabel' style="display: inline-block;"></afx-label>
</h3>
<i><p style = "margin:0; padding:0" data-id = 'mydesc'></p></i>
</div>
<afx-hbox>
<div data-width="10"></div>
<afx-grid-view data-id = 'mygrid'></afx-grid-view>
</afx-hbox>
<afx-hbox data-height="30">
<div ></div>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "60" ></afx-button>
</afx-hbox>
<div data-height = "10"></div>
<div data-id="read-me" style="overflow-x: hidden; overflow-y: auto;"></div>
<div data-height="10"></div>
<div data-height="35" style = "text-align: right;">
<afx-button data-id = "btnCancel" text = "__(Cancel)" ></afx-button>
</div>
</afx-vbox>
</afx-app-window>\
`;
@ -1021,7 +984,7 @@ namespace OS {
super.main();
const fileview = this.find("fileview") as tag.FileViewTag;
const location = this.find("location") as tag.ListViewTag;
const filename = this.find("filename") as HTMLInputElement;
const filename = this.find("filename") as tag.InputTag;
fileview.fetch = (path: string) =>
new Promise(function (resolve, reject) {
if (!path) {
@ -1035,12 +998,6 @@ namespace OS {
return reject(d);
}
FileDialog.last_opened = path;
if (!dir.isRoot()) {
const p = dir.parent();
p.filename = "[..]";
p.type = "dir";
d.result.unshift(p);
}
return resolve(d.result);
})
.catch((e: Error): void => reject(__e(e)));
@ -1052,7 +1009,7 @@ namespace OS {
__("Resource not found: {0}", path)
);
}
return (fileview.path = path);
fileview.path = path;
};
if (!this.data || !this.data.root) {
@ -1084,15 +1041,18 @@ namespace OS {
}
fileview.onfileselect = function (e) {
if (e.data.type === "file") {
return $(filename).val(e.data.filename);
return filename.value = e.data.filename;
}
};
(this.find("btnOk") as tag.ButtonTag).onbtclick = (_e) => {
const f = fileview.selectedFile;
let f = fileview.selectedFile;
if (!f) {
return this.notify(
__("Please select a file/fofler")
);
const sel = location.selectedItem;
if(!sel)
return this.notify(
__("Please select a file/fofler")
);
f = sel.data as API.FileInfoType;
}
if (
this.data &&
@ -1128,7 +1088,7 @@ namespace OS {
}
}
const name = $(filename).val();
const name = filename.value;
if (this.handle) {
this.handle({ file: f, name });
}
@ -1142,9 +1102,13 @@ namespace OS {
};
if (this.data && this.data.file) {
$(filename)
.css("display", "block")
.val(this.data.file.basename || "Untitled");
$(filename).show();
filename.value = (this.data.file.basename || "Untitled");
this.trigger("resize");
}
else
{
$(filename).hide();
this.trigger("resize");
}
if (this.data && this.data.hidden) {
@ -1158,21 +1122,16 @@ namespace OS {
* Scheme definition
*/
FileDialog.scheme = `\
<afx-app-window width='400' height='300'>
<afx-hbox>
<afx-list-view data-id = "location" dropdown = "false" data-width = "120"></afx-list-view>
<afx-vbox>
<afx-file-view data-id = "fileview" view="tree" status = "false"></afx-file-view>
<input data-height = '26' type = "text" data-id = "filename" style="margin-left:5px; margin-right:5px;display:none;" ></input>
<afx-hbox data-height = '30'>
<div style=' text-align:right;'>
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
<afx-button data-id = "bt-cancel" text = "__(Cancel)"></afx-button>
</div>
<div data-width="5"></div>
</afx-hbox>
</afx-vbox>
</afx-hbox>
<afx-app-window width='400' height='450'>
<afx-vbox>
<afx-list-view data-id = "location" dropdown = "true" data-height = "35"></afx-list-view>
<afx-file-view data-id = "fileview" view="tree" status = "false"></afx-file-view>
<afx-input data-height = '52' label = "__(Target file/folder)" type = "text" data-id = "filename" ></afx-input>
<div style=' text-align:right;' data-height="35">
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
<afx-button data-id = "bt-cancel" text = "__(Cancel)"></afx-button>
</div>
</afx-vbox>
</afx-app-window>\
`;
@ -1254,17 +1213,15 @@ namespace OS {
* @memberof MultiInputDialog
*/
init(): void {
let height = 60;
let height = 85;
let html = "";
if (this.data && this.data.model) {
const model = this.data.model;
for (const key in model) {
html += `\
<afx-label data-height="25" text="{0}" ></afx-label>
<input data-height="25" type="text" name="{1}" ></input>
<div data-height="10" ></div>
<afx-input data-height="52" text="{0}" type="text" name = {1} ></afx-input>
`.format(model[key], key);
height += 60;
height += 52;
}
}
this.markup = MultiInputDialog.scheme.format(height, html);
@ -1307,20 +1264,13 @@ namespace OS {
*/
MultiInputDialog.scheme = `\
<afx-app-window width='350' height='{0}'>
<afx-hbox>
<div data-width="10" ></div>
<afx-vbox>
<div data-height="5" ></div>
{1}
<afx-hbox data-height="30">
<div ></div>
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
</afx-hbox>
<div data-height="5" ></div>
</afx-vbox>
<div data-width="10" ></div>
</afx-hbox>
<afx-vbox padding = "5">
{1}
<div data-height="35" style = "text-align: right;">
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
</div>
</afx-vbox>
</afx-app-window>`;
@ -1406,28 +1356,22 @@ namespace OS {
*/
private addField(key: string, value: string, removable: boolean): void {
const div = $("<div>")
.css("width", "100%")
.css("display", "flex")
.css("flex-direction", "row")
.appendTo(this.container);
$("<input>")
.attr("type", "text")
.css("width", "120px")
.css("height", "23px")
.css("flex", "1")
.val(key)
.appendTo(div);
$("<input>")
.attr("type", "text")
.css("width", "200px")
.css("height", "23px")
.css("flex", "1")
.val(value)
.appendTo(div);
if (removable) {
const btn = $("<afx-button>");
btn[0].uify(undefined);
$("button", btn)
.css("width", "23px")
.css("height", "23px");
(btn[0] as tag.ButtonTag).iconclass = "fa fa-minus";
btn
.on("click", () => {
@ -1435,12 +1379,13 @@ namespace OS {
})
.appendTo(div);
}
else {
else
{
$("<div>")
.css("width", "23px")
.appendTo(div);
.css("width", "40px")
.css("height", "35px")
.appendTo(div);
}
}
}
@ -1449,23 +1394,18 @@ namespace OS {
* Scheme definition
*/
KeyValueDialog.scheme = `\
<afx-app-window width='350' height='300'>
<afx-hbox>
<div data-width="10" ></div>
<afx-vbox>
<div data-height="5" ></div>
<afx-app-window width='400' height='350'>
<afx-vbox padding = "10">
<afx-label text="__(Enter key-value data)" data-height="30"></afx-label>
<div data-id="container"></div>
<afx-hbox data-height="30">
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "30" ></afx-button>
<div ></div>
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
<afx-hbox data-height="35">
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "35" ></afx-button>
<div style = "text-align: right;">
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
</div>
</afx-hbox>
<div data-height="5" ></div>
</afx-vbox>
<div data-width="10" ></div>
</afx-hbox>
</afx-app-window>`;
}
}

View File

@ -43,6 +43,7 @@ namespace OS {
*/
[propName: string]: any;
}
/**
* Enum definition of different model types
*
@ -154,7 +155,7 @@ namespace OS {
* to handle all local events inside that model.
*
* This observable object is propagate to all the
* UI elements ([[AFXTag]]) inside the model
* UI elements ({@link OS.GUI.AFXTag}) inside the model
*
* @protected
* @type {API.Announcer}
@ -192,10 +193,10 @@ namespace OS {
* The HTML element ID of the virtual desktop
*
* @protected
* @type {string}
* @type {HTMLElement}
* @memberof BaseModel
*/
protected host: string;
protected host: HTMLElement;
/**
* The process number of the current model.
@ -293,12 +294,10 @@ namespace OS {
this._gui = GUI;
this.systemsetting = setting;
this.on("exit", () => this.quit(false));
this.host = this._gui.workspace;
this.host = this._gui.desktop();
this.dialog = undefined;
this.subscribe("systemlocalechange", (name) => {
this.updateLocale(name);
return this.update();
});
// relay global events to local events
this.subscribe("DESKTOP-RESIZE", (e) => this.observable.trigger("desktopresize", e));
}
/**
@ -314,20 +313,19 @@ namespace OS {
/**
* Update the model locale
*
* @protected
* @param {string} name
* @memberof BaseModel
*/
protected updateLocale(name: string) {}
updateLocale(name: string) {}
/**
* Render the model's UI
*
* @protected
* @param {string} p VFS path to the UI scheme definition
* @returns {void}
* @returns {Promise<any>}
* @memberof BaseModel
*/
protected render(p: string): void {
protected render(p: string): Promise<any> {
return GUI.loadScheme(p, this, this.host);
}
@ -338,7 +336,7 @@ namespace OS {
* @returns {void}
* @memberof BaseModel
*/
quit(force: boolean): void {
quit(force: boolean = false): void {
const evt = new BaseEvent("exit", force);
this.onexit(evt);
if (!evt.prevent) {
@ -350,10 +348,22 @@ namespace OS {
if (this.dialog) {
this.dialog.quit();
}
return PM.kill(this);
announcer.unregister(this);
this.destroy();
}
}
/**
* Purge the model from the system
*
* @protected
* @memberof BaseModel
*/
protected destroy(): void
{
return PM.kill(this);
}
/**
* Model meta data, need to be implemented by
* subclasses
@ -508,7 +518,6 @@ namespace OS {
/**
* trigger a local event
*
* @protected
* @param {string} e event name
* @param {*} [d] event data
* @returns {void}
@ -524,14 +533,14 @@ namespace OS {
*
* @protected
* @param {string} e event name
* @param {(d: any) => void} f event callback
* @param {(d: API.AnnouncementDataType<any>) => void} f event callback
* @returns {void}
* @memberof BaseModel
*/
subscribe(e: string, f: (d: any) => void): void {
subscribe(e: string, f: (d: API.AnnouncementDataType<any>) => void): void {
return announcer.on(e, f, this);
}
/**
* Open a dialog
*
@ -570,10 +579,10 @@ namespace OS {
}
/**
* Open a [[YesNoDialog]] to confirm a task
* Open a {@link OS.GUI.dialogs.YesNoDialog} to confirm a task
*
* @protected
* @param {GenericObject<any>} data [[YesNoDialog]] input data
* @param {GenericObject<any>} data {@link OS.GUI.dialogs.YesNoDialog} input data
* @returns {Promise<boolean>}
* @memberof BaseModel
*/
@ -587,41 +596,39 @@ namespace OS {
* @protected
* @param {string} t event name
* @param {(string | FormattedString)} m event message
* @param {Error} [e] error object if any
* @param {any} u_data user data object if any
* @returns {void}
* @memberof BaseModel
*/
protected publish(
t: string,
m: string | FormattedString,
e?: Error
u_data?: any
): void {
const mt = this.meta();
let icon: string = undefined;
const data: API.AnnouncementDataType<any> = {} as API.AnnouncementDataType<any>;
data.icon = undefined;
if (mt && mt.icon) {
icon = `${mt.path}/${mt.icon}`;
data.icon = `${mt.path}/${mt.icon}`;
}
return announcer.trigger(t, {
id: this.pid,
name: this.name,
data: {
m: m,
icon: icon,
iconclass: mt?mt.iconclass:undefined,
e: e,
},
});
data.id = this.pid;
data.name = this.name;
data.message = m;
data.iconclass = mt?mt.iconclass:undefined;
data.u_data = u_data;
return announcer.trigger(t, data);
}
/**
* Publish a global notification
*
* @param {(string | FormattedString)} m notification string
* @param {any} u_data user data object if any
* @returns {void}
* @memberof BaseModel
*/
notify(m: string | FormattedString): void {
return this.publish("notification", m);
notify(m: string | FormattedString, data?: any): void {
return this.publish("notification", m, data);
}
/**
@ -677,7 +684,11 @@ namespace OS {
*/
update(): void {
if (this.scheme) {
return this.scheme.update();
this.scheme.update();
}
if(this.dialog)
{
this.dialog.update();
}
}
@ -690,12 +701,16 @@ namespace OS {
* @returns {HTMLElement}
* @memberof BaseModel
*/
protected find(id: string): HTMLElement {
protected find<T extends HTMLElement>(id: string): T {
if (this.scheme) {
return $(`[data-id='${id}']`, this.scheme)[0];
return $(`[data-id='${id}']`, this.scheme)[0] as T;
}
}
/*protected $(id: string) : T {
return this.find(id) as T;
}*/
/**
* Select all DOM Element inside the UI of the model
* using JQuery selector

View File

@ -55,10 +55,10 @@ namespace OS {
/**
* Text of the service shown in the system tray
*
* @type {string}
* @type {string | FormattedString}
* @memberof BaseService
*/
text: string;
text: string | FormattedString;
/**
* Reference to the menu entry DOM element attached
@ -71,7 +71,7 @@ namespace OS {
/**
* Reference to the timer that periodically executes the callback
* defined in [[watch]].
* defined in {@link watch}.
*
* @private
* @type {number}
@ -79,13 +79,6 @@ namespace OS {
*/
private timer: number;
/**
* Reference to the system tray menu
*
* @type {HTMLElement}
* @memberof BaseService
*/
holder: HTMLElement;
/**
* Placeholder for service select callback
@ -93,7 +86,7 @@ namespace OS {
* @memberof BaseService
*/
onmenuselect: (
d: OS.GUI.TagEventType<GUI.tag.MenuEventData>
d: OS.GUI.TagEventType<GUI.tag.StackMenuEventData>
) => void;
/**
@ -108,7 +101,6 @@ namespace OS {
this.iconclass = "fa fa-paper-plane-o";
this.text = "";
this.timer = undefined;
this.holder = undefined;
this.onmenuselect = (d) => {
return this.awake(d);
};
@ -140,7 +132,9 @@ namespace OS {
* @memberof BaseService
*/
update(): void {
(this.domel as GUI.tag.MenuEntryTag).data = this;
if(!this.domel)
return;
(this.domel as GUI.tag.ListViewItemTag).data = this;
}
/**
@ -153,17 +147,6 @@ namespace OS {
return application[this.name].meta;
}
/**
* Attach the service to a menu element
* such as the system tray menu
*
* @param {HTMLElement} h
* @memberof BaseService
*/
attach(h: HTMLElement): void {
this.holder = h;
}
/**
* Set the callback that will be called periodically
* after a period of time.
@ -234,7 +217,7 @@ namespace OS {
* @param {GUI.TagEventType} e
* @memberof BaseService
*/
abstract awake(e: GUI.TagEventType<GUI.tag.MenuEventData>): void;
abstract awake(e: GUI.TagEventType<GUI.tag.StackMenuEventData>): void;
/**
* Do nothing

View File

@ -39,8 +39,8 @@ interface String {
/**
* Parse the current string and convert it
* to an object of type [[Version]] if the string
* is in the format recognized by [[Version]],
* to an object of type {@link OS.Version} if the string
* is in the format recognized by {@link OS.Version},
* e.g.: `1.0.1-a`
*
* @returns {OS.Version}
@ -98,7 +98,7 @@ interface String {
format(...args: any[]): string;
/**
* Create a [[FormattedString]] object using the current
* Create a {@link OS.FormattedString} object using the current
* string and the input parameters
*
* @param {...any[]} args
@ -157,9 +157,28 @@ interface String {
trimBy(arg: string): string;
}
/**
* Extend the Array prototype with some API
* functions used by AntOS API
*
* @interface Array
* @template T
*/
interface Array<T> {
/**
* Check if the array includes an element
*
* @param {T} element to check
* @returns {boolean}
* @memberof Array
*/
includes(element: T): boolean;
}
/**
* Extend the Data prototype with the
* [[timestamp]] function
* {@link timestamp} function
*
* @interface Date
*/
@ -171,6 +190,13 @@ interface Date {
* @memberof Date
*/
timestamp(): number;
/**
* Covnert to GMTString
*
* @returns {number}
* @memberof Date
*/
toGMTString(): string;
}
/**
@ -184,7 +210,7 @@ interface GenericObject<T> {
}
/**
* Global function to create a [[FormattedString]] from
* Global function to create a {@link OS.FormattedString} from
* a formatted string and a list of parameters. Example
*
* ```typescript
@ -207,6 +233,71 @@ declare function __(...args: any[]): OS.FormattedString | string;
*/
declare function __e(e: Error): Error;
/**
* JQuery event-extensions to support doubletap on
* mobile device
*/
jQuery.event.special.dbltap = {
bindType: 'touchend',
delegateType: 'touchend',
handle: function (event: any) {
var handleObj = event.handleObj,
targetData = jQuery.data(event.target),
now = new Date().getTime(),
delta = targetData.lastTouchEnd ? now - targetData.lastTouchEnd : 0,
delay = delay == null ? 300 : delay;
if (delta < delay && delta > 30) {
targetData.lastTouchEnd = null;
event.type = handleObj.origType;
['clientX', 'clientY', 'pageX', 'pageY'].forEach(function (property) {
event[property] = event.originalEvent.changedTouches[0][property];
})
// let jQuery handle the triggering of "dbltap" event handlers
handleObj.handler.apply(this, arguments);
} else {
targetData.lastTouchEnd = now;
}
}
};
/**
* JQuery event-extensions to support long touch event on
* mobile device
*/
jQuery.event.special.longtouch = {
bindType: 'touchstart',
//delegateType: 'touchstart',
handle: function (evt: any) {
let targetData = jQuery.data(evt.target);
let handleObj = evt.handleObj;
targetData.lastTouchStart = new Date().getTime();
$(evt.target).on("touchend", (event) => {
let now = new Date().getTime();
let end_targetData = jQuery.data(event.target);
let delta = end_targetData.lastTouchStart ? now - end_targetData.lastTouchStart : 0;
$(event.target).off("touchend");
const offset_top = Math.abs(event.originalEvent.changedTouches[0].clientY - evt.originalEvent.changedTouches[0].clientY);
const offset_left = Math.abs(event.originalEvent.changedTouches[0].clientX - evt.originalEvent.changedTouches[0].clientX);
console.log(offset_left, offset_top);
if(delta > 1000 && offset_top < 10 && offset_left < 10)
{
['clientX', 'clientY', 'pageX', 'pageY'].forEach(function (property) {
evt[property] = event.originalEvent.changedTouches[0][property];
})
event.preventDefault();
evt.type = handleObj.origType;
handleObj.handler.apply(this, arguments);
}
});
}
};
/**
* This namespace is the main entry point of AntOS
* API
@ -252,7 +343,7 @@ namespace OS {
};
Ant.__e = function (e: Error): Error {
const reason = new Error(e.toString());
const reason = new Error(e.toString().replace(/^Error: /g, ""));
reason.stack += "\nCaused By:\n" + e.stack;
return reason;
};
@ -279,7 +370,7 @@ namespace OS {
/**
* The value of the format pattern represented
* in [[fs]]
* in {@link fs}
*
* @type {any[]}
* @memberof FormattedString
@ -417,13 +508,14 @@ namespace OS {
* AntOS version number is in the following format:
*
* ```
* [major_number].[minor_number].[patch]-[branch]
* [major_number].[minor_number].[patch]-[branch]-[build ID])
*
* e.g.: 1.2.3-r means that:
* e.g.: 1.2.3-r-b means that:
* - version major number is 1
* - version minor number is 2
* - patch version is 3
* - the current branch is release `r`
* - build ID (optional)
* ```
*
* @export
@ -433,10 +525,11 @@ namespace OS {
/**
* The version string
*
* @private
* @type {string}
* @memberof Version
*/
string: string;
private string: string;
/**
* The current branch
@ -474,13 +567,38 @@ namespace OS {
*/
patch: number;
/**
* Version build ID (optional): usually the current git commit hash
*
* @type {number}
* @memberof Version
*/
build_id: string;
/**
*Creates an instance of Version.
*
* @param {string} string string represents the version
* @memberof Version
*/
constructor(string: string) {
this.string = string;
this.version_string = string;
}
/**
* Setter/getter to set the version string to the object
*
* @memberof Version
*/
set version_string(v: string) {
if (!v) {
this.string = undefined;
this.major = undefined;
this.minor = undefined;
this.patch = undefined;
this.build_id = undefined;
return;
}
this.string = v;
const arr = this.string.split("-");
const br = {
r: 3,
@ -488,9 +606,13 @@ namespace OS {
a: 1,
};
this.branch = 3;
if (arr.length === 2 && br[arr[1]]) {
if (arr.length >= 2 && br[arr[1]]) {
this.branch = br[arr[1]];
if (arr[2]) {
this.build_id = arr[2];
}
}
const mt = arr[0].match(/\d+/g);
if (!mt) {
API.throwe(
@ -510,6 +632,9 @@ namespace OS {
this.patch = Number(mt[2]);
}
}
get version_string(): string {
return this.string;
}
/**
* Compare the current version with another version.
@ -752,16 +877,27 @@ namespace OS {
/**
* Variable represents the current AntOS version, it
* is an instance of [[Version]]
* is an instance of {@link OS.Version}
*/
export const VERSION: Version = "1.0.0-a".__v();
export const VERSION: Version = new Version(undefined);
/**
* Variable represents the current AntOS source code repository
* is an instance of string
*/
export const REPOSITORY: string = "https://github.com/antos-rde/antos";
/**
* Indicate whether the current de
*/
export var mobile: boolean = false;
/**
* Register a model prototype to the system namespace.
* There are two types of model to be registered, if the model
* is of type [[SubWindow]], its prototype will be registered
* in the [[dialogs]] namespace, otherwise, if the model type
* is [[Application]] or [[Service]], its prototype will be
* registered in the [[application]] namespace.
* is of type {@link OS.GUI.SubWindow}, its prototype will be registered
* in the {@link OS.GUI.dialogs} namespace, otherwise, if the model type
* is {@link OS.application.BaseApplication} or {@link OS.application.BaseService}, its prototype will be
* registered in the {@link application} namespace.
*
* When a model is loaded in the system, its prototype is registered
* for later uses
@ -807,7 +943,6 @@ namespace OS {
$("#wrapper").empty();
GUI.clearTheme();
announcer.observable = new API.Announcer();
announcer.quota = 0;
resetSetting();
PM.processes = {};
PM.pidalloc = 0;
@ -815,7 +950,7 @@ namespace OS {
/**
* Booting up AntOS. This function checks whether the user
* is successfully logged in, then call [[startAntOS]], otherwise
* is successfully logged in, then call {@link OS.GUI.startAntOS}, otherwise
* it shows the login screen
*
* @export
@ -823,6 +958,7 @@ namespace OS {
export function boot(): void {
//first login
console.log("Booting system");
// check whether we are on mobile device
API.handle
.auth()
.then(function (d: API.RequestResult) {
@ -843,39 +979,40 @@ namespace OS {
* exits. These callbacks are useful when an application or service wants
* to perform a particular task before shuting down the system
*/
export const cleanupHandles: { [index: string]: () => void } = {};
export const cleanupHandles: { [index: string]: () => Promise<any> } = {};
/**
* Perform the system shutdown operation. This function calls all
* clean up handles in [[cleanupHandles]], then save the system setting
* clean up handles in {@link cleanupHandles}, then save the system setting
* before exiting
*
* @export
*/
export function exit(): void {
//do clean up first
const promises: Promise<any>[] = [];
for (let n in cleanupHandles) {
const f = cleanupHandles[n];
f();
promises.push(cleanupHandles[n]());
}
API.handle
.setting()
.then(function (r: any) {
promises.push(API.handle.setting());
Promise.all(promises)
.then(async function (r: any) {
cleanup();
return API.handle.logout().then((d: any) => boot());
const d = await API.handle.logout();
return boot();
})
.catch((e: Error) => console.error(e));
}
/**
* Register a callback to the system [[cleanupHandles]]
* Register a callback to the system {@link cleanupHandles}
*
* @export
* @param {string} n callback string name
* @param {() => void} f the callback handle
* @returns
*/
export function onexit(n: string, f: () => void) {
export function onexit(n: string, f: () => Promise<any>) {
if (!cleanupHandles[n]) {
return (cleanupHandles[n] = f);
}
@ -906,7 +1043,7 @@ namespace OS {
export interface PackageMetaType {
/**
* The application class name, if the package has only services
* this property is ignored and [[pkgname]] should be specified
* this property is ignored and {@link pkgname} should be specified
*
* @type {string}
* @memberof PackageMetaType
@ -914,7 +1051,7 @@ namespace OS {
app?: string;
/**
* Package name, in case of [[app]] being undefined, this property
* Package name, in case of {@link app} being undefined, this property
* need to be specified
*
* @type {string}
@ -1053,7 +1190,7 @@ namespace OS {
/**
* Package version, should be in a format conforming
* to the version definition in [[Version]] class
* to the version definition in {@link Version} class
*
* @type {string}
* @memberof PackageMetaType
@ -1110,14 +1247,41 @@ namespace OS {
export var lang: GenericObject<string> = {};
/**
* Re-export the system announcement [[getMID]] function to the
* core API
*
* A task is a Promise object that is tracked by AntOS via the
* Announcerment system
*
* Task manager implementation can subscribe to the following global events
* - ANTOS-TASK-PENDING : a new task/promise is created and executing
* - ANTOS-TASK-FULFILLED: a fullfilled task is a resolved promise
* - ANTOS-TASK-REJECTED: a rejected task is a rejected or error promise
*
* Whenever a task is created by this API, it states will be automatically announced
* to any subscribers of these events
*
* @export
* @returns {number}
* @param {Promise} a Promise object
*/
export function mid(): number {
return announcer.getMID();
export function Task(fn: (resolve: (any) => void, reject: (any) => void) => void): Promise<any> {
return new Promise(async (ok, nok) => {
const promise = new Promise(fn);
const ann: API.AnnouncementDataType<Promise<any>> = {} as API.AnnouncementDataType<Promise<any>>;
ann.name = "OS";
ann.u_data = promise;
ann.id = Math.floor(Math.random() * 1e6);
try {
ann.message = "ANTOS-TASK-PENDING";
announcer.trigger(ann.message, ann);
const data = await promise;
ok(data);
ann.message = "ANTOS-TASK-FULFILLED";
announcer.trigger(ann.message, ann);
}
catch (e) {
ann.message = "ANTOS-TASK-REJECTED";
announcer.trigger(ann.message, ann);
nok(__e(e));
}
});
}
/**
@ -1132,34 +1296,35 @@ namespace OS {
* @returns {Promise<any>} a promise on the result data
*/
export function post(p: string, d: any): Promise<any> {
return new Promise(function (resolve, reject) {
const q = announcer.getMID();
API.loading(q, p);
return $.ajax({
type: "POST",
url: p,
contentType: "application/json",
data: JSON.stringify(
d,
function (k, v) {
if (k === "domel") {
return undefined;
}
return v;
},
4
),
dataType: "json",
success: null,
})
.done(function (data) {
API.loaded(q, p, "OK");
return resolve(data);
return API.Task(async (resolve, reject) => {
try {
$.ajax({
type: "POST",
url: p,
contentType: "application/json",
data: JSON.stringify(
d,
function (k, v) {
if (k === "domel") {
return undefined;
}
return v;
},
4
),
dataType: "json",
success: null,
})
.fail(function (j, s, e) {
API.loaded(q, p, "FAIL");
return reject(API.throwe(s));
});
.done(function (data) {
return resolve(data);
})
.fail(function (j, s, e) {
reject(e);
});
}
catch (e) {
reject(__e(e));
}
});
}
@ -1175,21 +1340,17 @@ namespace OS {
* @returns {Promise<ArrayBuffer>} a promise on the returned binary data
*/
export function blob(p: string): Promise<ArrayBuffer> {
return new Promise(function (resolve, reject) {
const q = announcer.getMID();
return API.Task(function (resolve, reject) {
const r = new XMLHttpRequest();
r.open("GET", p, true);
r.responseType = "arraybuffer";
r.onload = function (e) {
if (this.status === 200 && this.readyState === 4) {
API.loaded(q, p, "OK");
resolve(this.response);
} else {
API.loaded(q, p, "FAIL");
reject(API.throwe(__("Unable to get blob: {0}", p)));
}
};
API.loading(q, p);
r.send();
});
}
@ -1205,49 +1366,41 @@ namespace OS {
* @returns {Promise<any>}
*/
export function upload(p: string, d: string): Promise<any> {
return new Promise(function (resolve, reject) {
const q = announcer.getMID();
return new Promise((resolve, reject) => {
//insert a temporal file selector
const o =
const o =
$("<input>")
.attr("type","file")
.attr("multiple","true");
o.on("change", function () {
const files = (o[0] as HTMLInputElement).files;
const n_files = files.length;
const tasks = [];
if (n_files > 0)
API.loading(q, p);
Array.from(files).forEach(file => {
.attr("type", "file")
.attr("multiple", "true");
o.on("change", async () => {
try {
const files = (o[0] as HTMLInputElement).files;
const formd = new FormData();
formd.append("path", d);
formd.append("upload", file);
return $.ajax({
url: p,
data: formd,
type: "POST",
contentType: false,
processData: false,
})
.done(function (data) {
tasks.push("OK");
if (tasks.length == n_files)
{
API.loaded(q, p, "OK");
resolve(data);
o.remove();
}
jQuery.each(files, (i, file) => {
formd.append(`upload-${i}`, file);
});
const ret = await API.Task((ok, nok) => {
$.ajax({
url: p,
data: formd,
type: "POST",
contentType: false,
processData: false,
})
.fail(function (j, s, e) {
tasks.push("FAIL");
if (tasks.length == n_files)
{
API.loaded(q, p, "FAIL");
o.remove();
}
reject(API.throwe(s));
});
});
.done(function (data) {
ok(data);
})
.fail(function (j, s, e) {
//o.remove();
nok(API.throwe(s));
});
});
resolve(ret);
}
catch (e) {
reject(__e(e));
}
});
return o.trigger("click");
});
@ -1274,42 +1427,6 @@ namespace OS {
o.remove();
}
/**
* Helper function to trigger the global `loading`
* event. This event should be triggered in the
* beginning of a heavy task
*
* @export
* @param {number} q message id, see [[mid]]
* @param {string} p message string
*/
export function loading(q: number, p: string): void {
announcer.trigger("loading", {
id: q,
data: { m: `${p}`, s: true },
name: "OS",
});
}
/**
* Helper function to trigger the global `loaded`
* event: This event should be triggered in the
* end of a heavy task that has previously triggered
* the `loading` event
*
* @export
* @param {number} q the message id of the corresponding `loading` event
* @param {string} p the message string
* @param {string} m message status (`OK` of `FAIL`)
*/
export function loaded(q: number, p: string, m: string): void {
announcer.trigger("loaded", {
id: q,
data: { m: `${m}: ${p}`, s: false },
name: "OS",
});
}
/**
* Perform an REST GET request
*
@ -1323,7 +1440,7 @@ namespace OS {
* @returns {Promise<any>} a Promise on the requested data
*/
export function get(p: string, t: string = undefined): Promise<any> {
return new Promise(function (resolve, reject) {
return API.Task(function (resolve, reject) {
const conf: any = {
type: "GET",
url: p,
@ -1331,15 +1448,11 @@ namespace OS {
if (t) {
conf.dataType = t;
}
const q = announcer.getMID();
API.loading(q, p);
return $.ajax(conf)
.done(function (data) {
API.loaded(q, p, "OK");
return resolve(data);
})
.fail(function (j, s, e) {
API.loaded(q, p, "FAIL");
return reject(API.throwe(s));
});
});
@ -1395,7 +1508,7 @@ namespace OS {
* @returns {Promise<void>} a promise on the result data
*/
export function requires(l: string, force: boolean = false): Promise<void> {
return new Promise(async (resolve, reject) =>{
return new Promise(async (resolve, reject) => {
try {
if (!API.shared[l] || force) {
const libfp = l.asFileHandle();
@ -1461,8 +1574,8 @@ namespace OS {
* Fetch the package meta-data from the server
*
* @export
* @returns {Promise<RequestResult>} Promise on a [[RequestResult]].
* A success request result should contain a list of [[PackageMetaType]]
* @returns {Promise<RequestResult>} Promise on a {@link RequestResult}.
* A success request result should contain a list of {@link PackageMetaType}
*/
export function fetch(): Promise<RequestResult> {
return API.handle.packages({
@ -1508,7 +1621,7 @@ namespace OS {
* Save the current user setting
*
* @export
* @returns {Promise<RequestResult>} promise on a [[RequestResult]]
* @returns {Promise<RequestResult>} promise on a {@link RequestResult}
*/
export function setting(): Promise<RequestResult> {
return API.handle.setting();
@ -1554,7 +1667,7 @@ namespace OS {
* text in spotlight.
*
* This function will call all the search handles stored
* in [[searchHandle]] and build the search result based
* in {@link searchHandle} and build the search result based
* on output of these handle
*
* @export
@ -1567,11 +1680,11 @@ namespace OS {
for (let k in searchHandle) {
const ret = searchHandle[k](text);
if (ret.length > 0) {
ret.unshift({
/*ret.unshift({
text: k,
class: "search-header",
dataid: "header",
});
});*/
r = r.concat(ret);
}
}
@ -1579,7 +1692,7 @@ namespace OS {
}
/**
* Register a search handle to the global [[searchHandle]]
* Register a search handle to the global {@link searchHandle}
*
* @export
* @param {string} name handle name string
@ -1598,7 +1711,7 @@ namespace OS {
* Set the current system locale: This function will
* find and load the locale dictionary definition file in the
* system asset resource, then trigger the global event
* `systemlocalechange` to translated all translatable text
* `SYSTEM-LOCALE-CHANGED` to translated all translatable text
* to the target language
*
* @export
@ -1612,7 +1725,7 @@ namespace OS {
const d = await API.get(path, "json");
OS.setting.system.locale = name;
API.lang = d;
announcer.trigger("systemlocalechange", name);
announcer.ostrigger("SYSTEM-LOCALE-CHANGED", name);
return resolve(d);
} catch (e) {
return reject(__e(e));
@ -1738,5 +1851,37 @@ namespace OS {
});
return o;
}
/**
* A watcher is a Proxy wrapper to an object
*
* It is used to automatically detect changes in the
* target object and notify the change to a callback
* handler
*
* @export
* @param {Object} target object
* @param {(obj: Object, key: string, value: any, path: any[]) => void} callback function
* @returns {Proxy} the wrapper object
*/
export function watcher(target: GenericObject<any>, callback: (obj: Object, key: any, value: any, path: any[]) => void): Object {
const create_handle_for = (path: any[]) => {
return {
get: (obj: Object, key: any) => {
if (typeof obj[key] === "object" && obj[key] !== null) {
return new Proxy(obj[key], create_handle_for(path.concat(key)));
}
return obj[key];
},
set: (obj: Object, prop: any, value: any) => {
obj[prop] = value;
callback(obj, prop, value, path);
return true;
}
}
};
return new Proxy(target, create_handle_for([]));
}
}
}

View File

@ -1,202 +0,0 @@
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// AnTOS Web desktop is is licensed under the GNU General Public
// License v3.0, see the LICENCE file for more information
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see https://www.gnu.org/licenses/.
namespace OS {
export namespace API {
/**
* Simple Virtual Database (VDB) application API.
*
* This API abstracts and provides a standard way to
* connect to a server-side relational database (e.g. sqlite).
*
* Each user when connected has their own database previously
* created. All VDB operations related to that user will be
* performed on this database.
*
* The creation of user database need to be managed by the server-side API.
* The VDB API assumes that the database already exist. All operations
* is performed in tables level
*
* @export
* @class DB
*/
export class DB {
/**
* A table name on the user's database
*
* @private
* @type {string}
* @memberof DB
*/
private table: string;
/**
*Creates an instance of DB.
* @param {string} table table name
* @memberof DB
*/
constructor(table: string) {
this.table = table;
}
/**
* Save data to the current table. The input
* data must conform to the table record format.
*
* On the server side, if the table doest not
* exist yet, it should be created automatically
* by inferring the data structure of the input
* object
*
* @param {GenericObject<any>} d data object represents a current table record
* @returns {Promise<API.RequestResult>}
* @memberof DB
*/
save(d: GenericObject<any>): Promise<API.RequestResult> {
return new Promise(async (resolve, reject) => {
try {
const r = await API.handle.dbquery("save", {
table: this.table,
data: d,
});
if (r.error) {
return reject(API.throwe(r.error.toString()));
}
return resolve(r);
} catch (e) {
return reject(__e(e));
}
});
}
/**
* delete record(s) from the current table by
* a conditional object
*
* @param {*} c conditional object, c can be:
*
* * a `number`: the operation will delete the record with `id = c`
* * a `string`: The SQL string condition that selects record to delete
* * a conditional object represents a SQL condition statement as an object,
* example: `pid = 10 AND cid = 2` is represented by:
*
* ```typescript
* {
* exp: {
* "and": {
* pid: 10,
* cid: 2
* }
* }
* ```
*
* @returns {Promise<API.RequestResult>}
* @memberof DB
*/
delete(
c: GenericObject<any> | number | string
): Promise<API.RequestResult> {
return new Promise(async (resolve, reject) => {
const rq: any = { table: this.table };
if (!c || c === "") {
reject(API.throwe("OS.DB: unknown condition"));
}
if (isNaN(c as number)) {
rq.cond = c;
} else {
rq.id = c;
}
try {
const r = await API.handle.dbquery("delete", rq);
if (r.error) {
return reject(API.throwe(r.error.toString()));
}
return resolve(r);
} catch (e) {
return reject(__e(e));
}
});
}
/**
* Get a record in the table by its primary key
*
* @param {number} id the primary key value
* @returns {Promise<GenericObject<any>>} Promise on returned record data
* @memberof DB
*/
get(id: number): Promise<GenericObject<any>> {
return new Promise(async (resolve, reject) => {
try {
const r = await API.handle.dbquery("get", {
table: this.table,
id: id,
});
if (r.error) {
return reject(API.throwe(r.error.toString()));
}
return resolve(r.result as GenericObject<any>);
} catch (e) {
return reject(__e(e));
}
});
}
/**
* Find records by a condition
*
* @param {GenericObject<any>} cond conditional object
*
* a conditional object represents a SQL condition statement as an object,
* example: `pid = 10 AND cid = 2 ORDER BY date DESC` is represented by:
*
* ```typescript
* {
* exp: {
* "and": {
* pid: 10,
* cid: 2
* }
* },
* order: {
* date: "DESC"
* }
* }
* ```
* @returns {Promise<GenericObject<any>[]>}
* @memberof DB
*/
find(cond: GenericObject<any>): Promise<GenericObject<any>[]> {
return new Promise(async (resolve, reject) => {
try {
const r = await API.handle.dbquery("select", {
table: this.table,
cond,
});
if (r.error) {
return reject(API.throwe(r.error.toString()));
}
return resolve(r.result as GenericObject<any>[]);
} catch (e) {
return reject(__e(e));
}
});
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -158,7 +158,7 @@ namespace OS {
*
* @export
* @param {string} p a VFS file path e.g. home://test/
* @returns {Promise<RequestResult>} A promise on a [[RequestResult]]
* @returns {Promise<RequestResult>} A promise on a {@link RequestResult}
* which contains an error or a list of FileInfoType
*/
export function scandir(p: string): Promise<RequestResult> {
@ -203,7 +203,7 @@ namespace OS {
*
* @export
* @param {string} p VFS file path
* @returns {Promise<RequestResult>} A promise on a [[RequestResult]]
* @returns {Promise<RequestResult>} A promise on a {@link RequestResult}
* which contains an error or an object of FileInfoType
*/
export function fileinfo(p: string): Promise<RequestResult> {
@ -226,12 +226,12 @@ namespace OS {
* - xml, html: the response is a XML/HTML object
* - text: plain text
*
* @returns {Promise<any>} A promise on a [[RequestResult]]
* which contains an error or an object of [[FileInfoType]]
* @returns {Promise<any>} A promise on a {@link RequestResult}
* which contains an error or an object of {@link FileInfoType}
*/
export function readfile(p: string, t: string): Promise<any> {
const path = `${API.REST}/VFS/get/`;
return API.get(path + p, t);
return API.get(path + encodeURIComponent(p), t);
}
/**
@ -240,7 +240,7 @@ namespace OS {
* @export
* @param {string} s VFS source file path
* @param {string} d VFS destination file path
* @returns {Promise<RequestResult>} A promise on a [[RequestResult]]
* @returns {Promise<RequestResult>} A promise on a {@link RequestResult}
* which contains an error or a success response
*/
export function move(s: string, d: string): Promise<RequestResult> {
@ -253,7 +253,7 @@ namespace OS {
*
* @export
* @param {string} p VFS file path
* @returns {Promise<RequestResult>} A promise on a [[RequestResult]]
* @returns {Promise<RequestResult>} A promise on a {@link RequestResult}
* which contains an error or a success response
*/
export function remove(p: string): Promise<RequestResult> {
@ -278,7 +278,7 @@ namespace OS {
*
* @export
* @param {PackageCommandType} d a package command of type PackageCommandType
* @returns {Promise<RequestResult>} a promise on a [[RequestResult]]
* @returns {Promise<RequestResult>} a promise on a {@link RequestResult}
*/
export function packages(
d: PackageCommandType
@ -292,7 +292,7 @@ namespace OS {
*
* @export
* @param {string} d VFS destination directory path
* @returns {Promise<RequestResult>} a promise on a [[RequestResult]]
* @returns {Promise<RequestResult>} a promise on a {@link RequestResult}
*/
export function upload(d: string): Promise<RequestResult> {
const path = `${API.REST}/VFS/upload`;
@ -305,7 +305,7 @@ namespace OS {
* @export
* @param {string} p path to the VFS file
* @param {string} d file data encoded in Base 64
* @returns {Promise<RequestResult>} a promise on a [[RequestResult]]
* @returns {Promise<RequestResult>} a promise on a {@link RequestResult}
*/
export function write(
p: string,
@ -371,8 +371,8 @@ namespace OS {
* Check if a user is logged in
*
* @export
* @returns {Promise<RequestResult>} a promise on a [[RequestResult]] that
* contains an error or a [[UserSettingType]] object
* @returns {Promise<RequestResult>} a promise on a {@link RequestResult} that
* contains an error or a {@link OS.setting.UserSettingType} object
*/
export function auth(): Promise<RequestResult> {
const p = `${API.REST}/user/auth`;
@ -383,9 +383,9 @@ namespace OS {
* Perform a login operation
*
* @export
* @param {UserLoginType} d user data [[UserLoginType]]
* @returns {Promise<RequestResult>} a promise on a [[RequestResult]] that
* contains an error or a [[UserSettingType]] object
* @param {UserLoginType} d user data {@link UserLoginType}
* @returns {Promise<RequestResult>} a promise on a {@link RequestResult} that
* contains an error or a {@link OS.setting.UserSettingType} object
*/
export function login(d: UserLoginType): Promise<RequestResult> {
const p = `${API.REST}/user/login`;
@ -396,7 +396,7 @@ namespace OS {
* Perform a logout operation
*
* @export
* @returns {Promise<RequestResult>} a promise on a [[RequestResult]]
* @returns {Promise<RequestResult>} a promise on a {@link RequestResult}
*/
export function logout(): Promise<RequestResult> {
const p = `${API.REST}/user/logout`;
@ -407,7 +407,7 @@ namespace OS {
* Save the current user settings
*
* @export
* @returns {Promise<RequestResult>} a promise on a [[RequestResult]]
* @returns {Promise<RequestResult>} a promise on a {@link RequestResult}
*/
export function setting(): Promise<RequestResult> {
const p = `${API.REST}/system/settings`;
@ -415,56 +415,14 @@ namespace OS {
}
/**
* This is the low level function of AntOS VDB API.
* It requests the server API to perform some simple
* SQL query.
* Query the current versions of all system components
*
* @export
* @param {string} cmd action to perform: save, delete, get, select
* @param {GenericObject<any>} d data object of the request based on each action:
* - save:
* ```
* { table: "table name", data: [record data object]}
* ```
* - get:
* ```
* { table: "table name", id: [record id]}
* ```
* - delete:
* ```
* { table: "table name", id: [record id]}
* or
* { table: "table name", cond: [conditional object]}
* ```
* - select:
* ```
* { table: "table name", cond: [conditional object]}
* ```
* @returns {Promise<RequestResult>} a promise of [[RequestResult]] on the
* query data
*
* A conditional object represents a SQL condition statement as an object,
* example: `pid = 10 AND cid = 2 ORDER BY date DESC`
* ```
* {
* exp: {
* "and": {
* pid: 10,
* cid: 2
* }
* },
* order: {
* date: "DESC"
* }
* }
* ```
* @returns {Promise<RequestResult>} a promise on a {@link RequestResult}
*/
export function dbquery(
cmd: string,
d: GenericObject<any>
): Promise<RequestResult> {
const path = `${API.REST}/VDB/${cmd}`;
return API.post(path, d);
export function versions(): Promise<RequestResult> {
const p = `${API.REST}/system/version`;
return API.get(p);
}
}
}

View File

@ -11,7 +11,7 @@ namespace OS {
| application.BaseApplication
| application.BaseService;
/**
* Alias to all classes that extends [[BaseModel]]
* Alias to all classes that extends {@link BaseModel}
*/
export type ModelTypeClass = {
new <T extends BaseModel>(args: AppArgumentsType[]): T;
@ -24,6 +24,10 @@ namespace OS {
/**
* All running processes is stored in this variables
*/
/**
* Current active process ID
*/
export var pidactive: number = 0;
export var processes: GenericObject<BaseModel[]> = {};
/**
* Create a new process of application or service
@ -31,7 +35,7 @@ namespace OS {
* @export
* @param {string} app class name string
* @param {ProcessTypeClass} cls prototype class
* @param {GUI.AppArgumentsType[]} [args] process arguments
* @param {AppArgumentsType[]} [args] process arguments
* @returns {Promise<ProcessType>} a promise on the created process
*/
export function createProcess(
@ -63,6 +67,10 @@ namespace OS {
obj.birth = new Date().getTime();
PM.pidalloc++;
obj.pid = PM.pidalloc;
obj.subscribe("SYSTEM-LOCALE-CHANGED", (d) => {
obj.updateLocale(d.message as string);
return obj.update();
});
PM.processes[app].push(obj);
if (metaclass.type === ModelType.Application) {
GUI.dock(
@ -128,10 +136,28 @@ namespace OS {
if (i >= 0) {
if (application[app.name].type === ModelType.Application) {
GUI.undock(app as application.BaseApplication);
// save setting file if any
if(PM.processes[app.name].length == 1)
{
const app_class = application[app.name] as typeof OS.application.BaseApplication;
let file = `${app_class.meta.path}/.settings.json`.asFileHandle();
if(file.protocol !== "home")
{
file = `home:///.antos/settings/${app.name}.json`.asFileHandle();
}
file.cache = app_class.setting_wdg;
//file.cache = JSON.stringify(app_class.setting_wdg, undefined, 4);
file
.write("object")
.catch((e) =>{
return announcer.osinfo(
__("Unable to save settings for application {0}: {1}", app.name, e.toString()));
});
}
} else {
GUI.detachservice(app as application.BaseService);
}
announcer.unregister(app);
// announcer.unregister(app);
delete PM.processes[app.name][i];
PM.processes[app.name].splice(i, 1);
}
@ -149,7 +175,25 @@ namespace OS {
if (!PM.processes[app]) {
return;
}
PM.processes[app].map((a) => a.quit(force));
const arr = PM.processes[app].map( e => e);
for(const p of arr)
{
p.quit(force);
}
}
/**
* Get the current active application
* @export
* @returns {BaseModel}
*/
export function getActiveApp():BaseModel
{
if(PM.pidactive === 0)
{
return undefined;
}
return PM.appByPid(PM.pidactive);
}
}
}

View File

@ -308,7 +308,7 @@ namespace OS {
/**
* Package repositories setting.
* This configuration is used by [[MarketPlace]]
* This configuration is used by {@link OS.application.MarketPlace}
* for package management
*
* @type {{
@ -398,7 +398,7 @@ namespace OS {
*/
export function resetSetting(): void {
setting.desktop = {
path: "home://.desktop",
path: "home://.antos/desktop",
menu: [],
showhidden: false,
};
@ -476,13 +476,13 @@ namespace OS {
menu: [],
packages: {},
pkgpaths: {
user: "home://.packages",
user: "home://.antos/packages",
system: "os://packages",
},
repositories: [],
startup: {
apps: [],
services: ["Syslog/PushNotification", "Syslog/Calendar"],
services: ["SystemServices/PushNotification", "SystemServices/Calendar"],
pinned: [],
},
};
@ -535,7 +535,7 @@ namespace OS {
setting.system.repositories = [
{
text: "Github",
url: "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/packages.json"
url: "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/packages.json"
}
]
}

View File

@ -15,7 +15,7 @@ namespace OS {
* @type {application.BaseApplication}
* @memberof AppDockItemType
*/
app: application.BaseApplication;
app?: application.BaseApplication;
/**
* Reference to the DOM element of
@ -38,15 +38,12 @@ namespace OS {
*/
export class AppDockTag extends AFXTag {
/**
* variable holds the application select event
* callback handle
*
* Cache of touch event
*
* @private
* @type {TagEventCallback<any>}
* @memberof AppDockTag
* @meberof AppDockTag
*/
private _onappselect: TagEventCallback<any>;
private _previous_touch: {x: number, y: number};
/**
* Items data of the dock
*
@ -72,7 +69,6 @@ namespace OS {
*/
constructor() {
super();
this._onappselect = (e) => {};
}
/**
@ -98,9 +94,6 @@ namespace OS {
break;
}
}
if (i !== -1) {
$(this.items[i].domel).attr("tooltip", `cr:${app.title()}`);
}
}
/**
@ -111,6 +104,8 @@ namespace OS {
*/
protected init(): void {
this._items = [];
this._previous_touch = {x: 0, y:0};
}
/**
@ -155,17 +150,23 @@ namespace OS {
let el = undefined;
for (let it of this.items) {
it.app.blur();
$(it.domel).removeClass();
$(it.domel).removeClass("selected");
if (v && v === it.app) {
el = it;
}
}
this._selectedItem = el;
if (!el) {
PM.pidactive = 0;
return;
}
$(el.domel).addClass("selected");
($(Ant.OS.GUI.workspace)[0] as FloatListTag).unselect();
const evt = {
id: this.aid,
data: v
};
announcer.trigger("APP-SELECT", evt);
}
get selectedApp(): application.BaseApplication {
@ -187,6 +188,63 @@ namespace OS {
return this._selectedItem;
}
/**
* Add a button to the dock
*
* @private
* @param {string} [name] associated application name
* @param {AppDockItemType} [item] dock item
* @param {boolean} [pinned] the button is pinned to the dock ?
* @memberof AppDockTag
*/
private add_button(name:string, item: AppDockItemType, pinned: boolean = false): void
{
const collection = $(this).children().filter((i, e) => {
return (e as ButtonTag).data.name == name;
});
if(collection.length > 0)
{
(collection[0] as ButtonTag).data.pinned = true;
item.domel = collection[0] as ButtonTag;
return;
}
const el = $("<afx-button>");
const bt = el[0] as ButtonTag;
el.appendTo(this);
el[0].uify(this.observable);
bt.set(item);
bt.data = {
name: name,
pinned: pinned
};
item.domel = bt;
bt.onbtclick = (e) => {
e.data.stopPropagation();
this
.handleAppSelect(bt);
};
}
private update_button(el: ButtonTag): void
{
const collection = this.items.filter(it => it.app.name == el.data.name);
if(collection.length == 1)
{
$(el).removeClass("plural");
}
if(collection.length == 0)
{
if(el.data.pinned)
{
$(el).removeClass();
}
else
{
$(el).remove();
}
}
}
/**
* When a new application process is created, this function
* will be called to add new application entry to the dock.
@ -196,25 +254,84 @@ namespace OS {
* @param {AppDockItemType} item an application dock item entry
* @memberof AppDockTag
*/
newapp(item: AppDockItemType): void {
addapp(item: AppDockItemType): void {
const collection = this.items.filter(it => it.app.name == item.app.name);
let bt = undefined;
if(collection.length == 0)
{
this.add_button(item.app.name, item);
}
else
{
bt = collection[0].domel;
item.domel = bt;
$(bt).addClass("plural");
}
this.items.push(item);
const el = $("<afx-button>");
const bt = el[0] as ButtonTag;
el.appendTo(this);
el[0].uify(this.observable);
bt.set(item);
bt.data = item.app;
item.domel = bt;
$(bt).attr("tooltip", `cr:${item.app.title()}`);
bt.onbtclick = (e) => {
e.id = this.aid;
//e.data.item = item;
this._onappselect(e);
item.app.show();
};
this.selectedApp = item.app;
}
/**
* Handle the application selection action
*
* @private
* @returns {Promise<application.BaseApplication>}
* @memberof AppDockTag
*/
private handleAppSelect(bt: ButtonTag): Promise<application.BaseApplication>
{
return new Promise(async (resolve, reject) => {
try {
const name = bt.data.name as string;
const collection = this.items.filter(it => it.app.name == name);
const ctxmenu = $("#contextmenu")[0] as tag.StackMenuTag;
ctxmenu.hide();
if(collection.length == 0)
{
resolve(await GUI.launch(name, []) as application.BaseApplication);
return;
}
if(collection.length == 1)
{
if(PM.getActiveApp() == collection[0].app)
{
collection[0].app.hide();
}
else
{
collection[0].app.show();
}
resolve(collection[0].app);
return;
}
// show the context menu containning a list of application to select
const menu_data = collection.map(e => {
return {
text: (e.app.scheme as WindowTag).apptitle,
icon: e.icon,
iconclass: e.iconclass,
app: e.app
};
});
const offset = $(bt).offset();
ctxmenu.nodes = menu_data;
$(ctxmenu)
.css("left", offset.left)
.css("bottom", $(this).height());
ctxmenu.onmenuselect = (e) =>
{
e.data.item.data.app.show();
resolve(e.data.item.data.app);
}
ctxmenu.show();
}
catch(e)
{
reject(__e(e));
}
});
}
/**
* Delete and application entry from the dock.
* This function will be called when an application
@ -235,9 +352,11 @@ namespace OS {
}
if (i !== -1) {
const appName = this.items[i].app.name;
const el = this.items[i].domel as ButtonTag;
delete this.items[i].app;
this.items.splice(i, 1);
$($(this).children()[i]).remove();
this.update_button(el);
}
}
@ -252,36 +371,39 @@ namespace OS {
if (e.target === this) {
return;
}
m.hide();
const bt = ($(e.target).closest(
"afx-button"
)[0] as any) as ButtonTag;
const app = bt.data as application.BaseApplication;
m.items = [
const name = bt.data.name as string;
const collection = this.items.filter(it => it.app.name == name);
m.nodes = [
{ text: "__(New window)", dataid: "new" },
{ text: "__(Show)", dataid: "show" },
{ text: "__(Hide)", dataid: "hide" },
{ text: "__(Close)", dataid: "quit" },
{ text: "__(Hide all)", dataid: "hide" },
{ text: "__(Close all)", dataid: "quit" },
];
m.onmenuselect = function (evt) {
const item = evt.data.item.data;
if (app[item.dataid]) {
return app[item.dataid]();
}
else
{
switch (item.dataid) {
case "new":
GUI.launch(app.name, []);
break;
default:
break;
}
switch (evt.data.item.data.dataid) {
case "new":
GUI.launch(bt.data.name as string, []);
break;
case "hide":
collection.forEach((el,_) => el.app.hide());
break;
case "quit":
collection.forEach((el,_) => el.app.quit());
break;
default:
break;
}
};
return m.show(e);
const offset = $(bt).offset();
$(m)
.css("left", offset.left)
.css("bottom", $(this).height());
return m.show();
};
announcer.trigger("sysdockloaded", undefined);
announcer.trigger("SYS-DOCK-LOADED", undefined);
GUI.bindKey("CTRL-ALT-2", (e) =>{
if(!this.items || this.items.length === 0)
{
@ -317,6 +439,60 @@ namespace OS {
}
this.items[index].app.trigger("focus");
});
this.addEventListener("touchstart", e => {
this._previous_touch.x = e.touches[0].pageX;
this._previous_touch.y = e.touches[0].pageY;
}, { passive: true});
this.addEventListener("touchmove", e => {
const offset = {x:0, y:0};
offset.x = this._previous_touch.x - e.touches[0].pageX ;
offset.y = this._previous_touch.y - e.touches[0].pageY;
(this as any).scrollLeft += offset.x;
this._previous_touch.x = e.touches[0].pageX;
this._previous_touch.y = e.touches[0].pageY;
}, { passive: true});
this.addEventListener("wheel", (evt)=>{
(this as any).scrollLeft += (evt as WheelEvent).deltaY;
},{ passive: true});
announcer.on("APP-PINNED", (_) => {
this.refresh_pinned_app();
});
this.refresh_pinned_app();
}
/**
* refresh the pinned application list
*
* @private
* @memberof AppDockTag
*/
private refresh_pinned_app(): void
{
if(!setting.system.startup.pinned)
return;
// unpin all application on the dock
$(this).children().each((i,e) => {
(e as ButtonTag).data.pinned = false;
});
// pin all setting application on the dock
setting.system.startup.pinned
.filter((el) =>{
const app = setting.system.packages[el];
return app && app.app
})
.forEach((name) => {
const app = setting.system.packages[name];
const item = {
icon: app.icon,
iconclass: app.iconclass,
app: undefined
};
this.add_button(name, item, true);
});
// update to remove the button
$(this).children().each((i,e) => {
this.update_button(e as ButtonTag);
});
}
}
define("afx-apps-dock", AppDockTag);

View File

@ -21,10 +21,25 @@ namespace OS {
/**
* Custom user data
*
* @type {GenericObject<any>}
* @type {any}
* @memberof ButtonTag
*/
data: GenericObject<any>;
private _data: any;
/**
* Custom user data setter/gettter
*
* @memberof ButtonTag
*/
set data(v: any)
{
this._data = v;
this.set(v);
}
get data(): any
{
return this._data;
}
/**
*Creates an instance of ButtonTag.
@ -64,6 +79,27 @@ namespace OS {
$(this).attr("iconclass", v);
(this.refs.label as LabelTag).iconclass = v;
}
/**
* Set the icon class on the right side of the button, this property
* allows to style the button icon using CSS
*
* @memberof ButtonTag
*/
set iconclass$(v: string) {
$(this).attr("iconclass_end", v);
(this.refs.label as LabelTag).iconclass$ = v;
}
/**
* Set the CSS class of the label icon on the right side
*
* @memberof ButtonTag
*/
set iconclass_end(v: string) {
this.iconclass$ = v;
}
/**
* Setter: Set the text of the button

View File

@ -113,8 +113,20 @@ namespace OS {
* @memberof CalendarTag
*/
protected mount(): void {
$(this.refs.prev).on("click",(e) => this.prevmonth());
$(this.refs.next).on("click",(e) => this.nextmonth());
(this.refs.prev as ButtonTag).iconclass = "fa fa-angle-left";
(this.refs.next as ButtonTag).iconclass = "fa fa-angle-right";
(this.refs.prev_year as ButtonTag).iconclass = "fa fa-angle-left";
(this.refs.next_year as ButtonTag).iconclass = "fa fa-angle-right";
(this.refs.prev as ButtonTag).onbtclick = (e) => this.prevmonth();
(this.refs.next as ButtonTag).onbtclick = (e) => this.nextmonth();
(this.refs.next_year as ButtonTag).onbtclick = (e) => {
this._year++;
this.calendar(new Date(this._year, this._month, 1));
}
(this.refs.prev_year as ButtonTag).onbtclick = (e) => {
this._year--;
this.calendar(new Date(this._year, this._month, 1));
}
const grid = this.refs.grid as GridViewTag;
grid.header = [
{ text: "__(Sun)" },
@ -138,7 +150,7 @@ namespace OS {
* This function triggers the date select event
*
* @private
* @param {TagEventType} e AFX tag event data [[TagEventType]]
* @param {TagEventType} e AFX tag event data {@link TagEventType}
* @returns {void}
* @memberof CalendarTag
*/
@ -249,6 +261,8 @@ namespace OS {
];
const this_month = new Date(this._year, this._month, 1);
const next_month = new Date(this._year, this._month + 1, 1);
const grid = this.refs.grid as GridViewTag;
grid.rows = [];
// Find out when this month starts and ends.
const first_week_day = this_month.getDay();
const days_in_this_month = Math.round(
@ -292,18 +306,16 @@ namespace OS {
week_day++;
}
for (
let i = 0, end2 = 7 - row.length, asc2 = 0 <= end2;
asc2 ? i <= end2 : i >= end2;
asc2 ? i++ : i--
let i = 0; i < 7 - row.length; i++
) {
row.push({ text: "" });
}
rows.push(row);
const grid = this.refs.grid as GridViewTag;
grid.rows = rows;
(this.refs.mlbl as LabelTag).text = `${
months[this._month]
} ${this._year}`;
}`;
(this.refs.ylbl as LabelTag).text = `${this._year}`;
}
/**
@ -319,9 +331,13 @@ namespace OS {
el: "div",
ref: "ctrl",
children: [
{ el: "i", class: "prevmonth", ref: "prev" },
{ el: "afx-button", ref: "prev" },
{ el: "afx-label", ref: "mlbl" },
{ el: "i", class: "nextmonth", ref: "next" },
{ el: "afx-button", ref: "next" },
{ el: "div"},
{ el: "afx-button", ref: "prev_year" },
{ el: "afx-label", ref: "ylbl" },
{ el: "afx-button", ref: "next_year" },
],
},
{ el: "afx-grid-view", ref: "grid" },

292
src/core/tags/DesktopTag.ts Normal file
View File

@ -0,0 +1,292 @@
namespace OS {
export namespace GUI {
export namespace tag {
/**
* Meta tag that represents the virtual desktop environment.
* In near future, we may have multiple virtual desktop environments.
* Each desktop environment has a simple file manager and a window
* manager that render the window in a specific order.
*
* @export
* @class DesktopTag
* @extends {FloatListTag}
*/
export class DesktopTag extends FloatListTag {
/**
* internal handle to the desktop file location
*
* @private
* @type {API.VFS.BaseFileHandle}
* @memberof DesktopTag
*/
private file: API.VFS.BaseFileHandle;
/**
* local observer that detect if a new child element is
* added or removed
*
* @private
* @type {MutationObserver}
* @memberof DesktopTag
*/
private observer: MutationObserver;
/**
* Internal list of the current opened window
*
* @private
* @type {Set<WindowTag>}
* @memberof DesktopTag
*/
private window_list: Set<WindowTag>;
/**
* Creates an instance of DesktopTag.
* @memberof DesktopTag
*/
constructor() {
super();
this.observer = undefined;
this.window_list = new Set<WindowTag>();
}
/**
* Mount the virtual desktop to the DOM tree
*
* @protected
* @memberof DesktopTag
*/
protected mount(): void {
/**
* TRICKY HACK
* When focusing on a window which overflows the desktop,
* the desktop scrolls automatically to bottom,
* even when `overflow: hidden` is set on CSS.
*
* The following event listener prevents
* the desktop to scroll down in this case
*/
$(this).on("scroll", (e) =>{
if(this.scrollTop != 0)
this.scrollTop = 0;
});
if(this.observer)
{
this.observer.disconnect();
this.observer = undefined;
}
this.observer = new MutationObserver((mutations_list) =>{
mutations_list.forEach((mutation) => {
mutation.removedNodes.forEach((removed_node) =>{
if((removed_node as HTMLElement).tagName === "AFX-APP-WINDOW")
{
this.window_list.delete(removed_node as WindowTag);
}
});
mutation.addedNodes.forEach((added_node) =>{
if((added_node as HTMLElement).tagName === "AFX-APP-WINDOW")
{
this.selectWindow(added_node as WindowTag);
}
});
});
});
this.observer.observe(this, { subtree: false, childList: true });
this.onready = (_) => {
this.observable = OS.announcer.observable;
window.onresize = () => {
const evt = {
id: this.aid,
data: {
width: $(this).width(),
height: $(this).height()
}
}
announcer.trigger("DESKTOP-RESIZE", evt);
this.calibrate();
};
this.onlistselect = (d: TagEventType<tag.ListItemEventData>) => {
GUI.systemDock().selectedApp = null;
};
this.onlistdbclick = (d: TagEventType<tag.ListItemEventData>) => {
GUI.systemDock().selectedApp = null;
const it = this.selectedItem;
return GUI.openWith(it.data as AppArgumentsType);
};
//($ "#workingenv").on "click", (e) ->
// desktop[0].set "selected", -1
$(this).on("click", (e) => {
let el: any = $(e.target).closest("afx-app-window")[0];
if (el) {
return;
}
el = $(e.target).parent();
if (!(el.length > 0)) {
return;
}
el = el.parent();
if (!(el.length > 0)) {
return;
}
if (el[0] !== this) {
return;
}
this.unselect();
GUI.systemDock().selectedApp = null;
});
this.contextmenuHandle = (e, m) => {
if (e.target.tagName.toUpperCase() === "UL") {
this.unselect();
}
GUI.systemDock().selectedApp = null;
let menu = [
{ text: __("Open"), dataid: "desktop-open" } as GUI.BasicItemType,
{ text: __("Refresh"), dataid: "desktop-refresh" } as GUI.BasicItemType,
];
menu = menu.concat(setting.desktop.menu.map(e => e));
m.nodes = menu;
m.onmenuselect = (evt) => {
if (!evt.data || !evt.data.item) return;
const item = evt.data.item.data;
switch (item.dataid) {
case "desktop-open":
var it = this.selectedItem;
if (it) {
return GUI.openWith(
it.data as AppArgumentsType
);
}
let arg = setting.desktop.path.asFileHandle() as AppArgumentsType;
arg.mime = "dir";
arg.type = "dir";
return GUI.openWith(arg);
case "desktop-refresh":
return this.refresh();
default:
if (item.app) {
return GUI.launch(item.app, item.args);
}
}
};
return m.show(e);
};
this.refresh();
announcer.on("VFS", (d: API.AnnouncementDataType<API.VFS.BaseFileHandle>) => {
if (["read", "publish", "download"].includes(d.message as string)) {
return;
}
if (d.u_data.hash() === this.file.hash() || d.u_data.parent().hash() === this.file.hash()) {
return this.refresh();
}
});
return announcer.ostrigger("DESKTOP-LOADED", undefined);
};
super.mount();
}
/**
* Display all files and folders in the specific desktop location
*
* @return {*} {Promise<any>}
* @memberof DesktopTag
*/
refresh(): Promise<any> {
return new Promise<any>(async (resolve, reject) => {
try {
this.file = setting.desktop.path.asFileHandle();
await this.file.onready();
const d = await this.file.read();
if (d.error) {
throw new Error(d.error);
}
const items = [];
$.each(d.result, function (i, v) {
if (v.filename[0] === "." && !setting.desktop.showhidden) {
return;
}
v.text = v.filename;
//v.text = v.text.substring(0,9) + "..." ifv.text.length > 10
v.iconclass = v.type;
return items.push(v);
});
this.data = items;
this.calibrate();
}
catch (err) {
announcer.osfail(err.toString(), err);
reject(__e(err));
}
});
}
/**
* Remove this element from its parent
*
* @memberof DesktopTag
*/
remove(): void {
if(this.observer)
{
this.observer.disconnect();
}
super.remove();
}
/**
* Active a window above all other windows
*
* @private
* @param {WindowTag} win
* @memberof DesktopTag
*/
private selectWindow(win: WindowTag)
{
if(this.window_list.has(win))
{
this.window_list.delete(win);
}
else
{
win.observable.on("focused",(_)=>{
this.selectWindow(win);
});
}
this.window_list.add(win);
this.render();
}
/**
* Render all windows in order from bottom to top
*
* @private
* @memberof DesktopTag
*/
private render(){
let zindex = 10;
for(let win of this.window_list)
{
$(win).css("z-index", zindex++);
}
}
}
define("afx-desktop", DesktopTag);
}
}
}

View File

@ -28,13 +28,22 @@ namespace OS {
private _onfileopen: TagEventCallback<API.FileInfoType>;
/**
* Reference to the currently selected file meta-data
* placeholder for directory changed event callback
*
* @private
* @type {API.FileInfoType}
* @type {TagEventCallback<API.VFS.BaseFileHandle>}
* @memberof FileViewTag
*/
private _selectedFile: API.FileInfoType;
private _ondirchanged: TagEventCallback<API.VFS.BaseFileHandle>;
/**
* Reference to the all selected files meta-datas
*
* @private
* @type {API.FileInfoType[]}
* @memberof FileViewTag
*/
private _selectedFiles: API.FileInfoType[];
/**
* Data placeholder of the current working directory
@ -58,10 +67,10 @@ namespace OS {
* Header definition of the widget grid view
*
* @private
* @type {(GenericObject<string | number>[])}
* @type {(GenericObject<any>[])}
* @memberof FileViewTag
*/
private _header: GenericObject<string | number>[];
private _header: GenericObject<any>[];
/**
* placeholder for the user-specified meta-data fetch function
@ -92,10 +101,57 @@ namespace OS {
this.chdir = true;
this.view = "list";
this._onfileopen = this._onfileselect = (e) => { };
this._ondirchanged = (e) => { };
this._selectedFiles = [];
const fn = function(r1, r2, i) {
let t1 = r1[i].text;
let t2 = r2[i].text;
if(!t1 || !t2) return 0;
if(i == 1)
{
// sort by date
t1 = new Date(t1);
t2 = new Date(t2);
}
else if(i==2)
{
// sort by size
t1 = parseInt(t1);
t2 = parseInt(t2);
}
else
{
// sort by name
t1 = t1.toString().toLowerCase();
t2 = t2.toString().toLowerCase();
}
if(this.desc)
{
if(t1 < t2) { return -1; }
if(t1 > t2) { return 1; }
}
else
{
if(t1 > t2) { return -1; }
if(t1 < t2) { return 1; };
}
return 0;
};
this._header = [
{ text: "__(File name)" },
{ text: "__(Type)" },
{ text: "__(Size)" },
{
text: "__(File name)",
sort: fn
},
{
text: "__(Modified)",
sort: fn
},
{
text: "__(Size)",
sort: fn
},
];
}
@ -111,7 +167,7 @@ namespace OS {
/**
* set the function that allows to fetch file entries.
* This handle function should return a promise on
* an arry of [[API.FileInfoType]]
* an arry of {@link API.FileInfoType}
*
* @memberof FileViewTag
*/
@ -122,7 +178,7 @@ namespace OS {
/**
* set the callback handle for the file select event.
* The parameter of the callback should be an object
* of type [[TagEventType]]<T> with the data type `T` is [[API.FileInfoType]]
* of type {@link TagEventType}<T> with the data type `T` is {@link API.FileInfoType}
*
* @memberof FileViewTag
*/
@ -130,10 +186,21 @@ namespace OS {
this._onfileselect = e;
}
/**
* set the callback handle for the directory changed event.
* The parameter of the callback should be an object
* of type {@link TagEventType}<T> with the data type `T` is {@link API.VFS.BaseFileHandle}
*
* @memberof FileViewTag
*/
set onchdir(e: TagEventCallback<API.VFS.BaseFileHandle>) {
this._ondirchanged = e;
}
/**
set the callback handle for the file open event.
* The parameter of the callback should be an object
* of type [[TagEventType]]<T> with the data type `T` is [[API.FileInfoType]]
* of type {@link TagEventType}<T> with the data type `T` is {@link API.FileInfoType}
*
* @memberof FileViewTag
*/
@ -168,7 +235,7 @@ namespace OS {
*
* Turn on/off the changing current working directory feature
* of the widget when a directory is double clicked. If enabled,
* the widget will use the configured [[fetch]] function to query
* the widget will use the configured {@link fetch} function to query
* the content of the selected directory
*
* Getter:
@ -227,6 +294,28 @@ namespace OS {
return this.hasattr("showhidden");
}
/**
* Setter:
*
* Allow multiple selection on file view
*
* Getter:
*
* Check whether the multiselection is actived
*
* @memberof FileViewTag
*/
set multiselect(v: boolean) {
this.attsw(v, "multiselect");
(this.refs.listview as ListViewTag).multiselect = v;
(this.refs.gridview as GridViewTag).multiselect = v;
}
get multiselect(): boolean {
return this.hasattr("multiselect");
}
/**
* Get the current selected file
*
@ -235,7 +324,21 @@ namespace OS {
* @memberof FileViewTag
*/
get selectedFile(): API.FileInfoType {
return this._selectedFile;
if(this._selectedFiles.length == 0)
return undefined;
return this._selectedFiles[this._selectedFiles.length - 1];
}
/**
* Get all selected files
*
* @readonly
* @type {API.FileInfoType[]}
* @memberof FileViewTag
*/
get selectedFiles(): API.FileInfoType[] {
return this._selectedFiles;
}
/**
@ -243,7 +346,7 @@ namespace OS {
*
* Set the path of the current working directory.
* When called the widget will refresh the current
* working directory using the configured [[fetch]]
* working directory using the configured {@link fetch}
* function
*
* Getter:
@ -270,6 +373,9 @@ namespace OS {
if (this.status) {
(this.refs.status as LabelTag).text = " ";
}
const evt = { id: this.aid, data: v.asFileHandle() };
this._ondirchanged(evt);
this.observable.trigger("chdir", evt);
})
.catch((e: Error) =>
announcer.oserror(e.toString(), e)
@ -286,7 +392,7 @@ namespace OS {
*
* @memberof FileViewTag
*/
set data(v: API.FileInfoType[]) {
set data(v: API.FileInfoType[]) {
if (!v) {
return;
}
@ -305,11 +411,21 @@ namespace OS {
*/
set ondragndrop(
v: TagEventCallback<
DnDEventDataType<TreeViewTag | ListViewItemTag>
DnDEventDataType<TreeViewTag | ListViewItemTag | GridCellPrototype>
>
) {
(this.refs.treeview as TreeViewTag).ondragndrop = v;
(this.refs.listview as ListViewTag).ondragndrop = v;
(this.refs.gridview as GridViewTag).ondragndrop = (e) => {
const evt = {
id: this.aid,
data: {
from: e.data.from.map(x => x.data[0].domel),
to: e.data.to.data[0].domel
}
};
v(evt);
};
}
/**
@ -365,7 +481,7 @@ namespace OS {
let h = $(this).outerHeight();
const w = $(this).width();
if (this.status) {
h -= $(this.refs.status).height() + 10;
h -= $(this.refs.status).height();
}
$(this.refs.listview).css("height", h + "px");
$(this.refs.gridview).css("height", h + "px");
@ -388,12 +504,15 @@ namespace OS {
if (v.filename[0] === "." && !this.showhidden) {
return;
}
v.text = v.filename;
if(!v.text)
v.text = v.filename;
/*
if (v.text.length > 10) {
v.text = v.text.substring(0, 9) + "...";
}
}*/
v.iconclass = v.iconclass ? v.iconclass : v.type;
v.icon = v.icon;
if(v.icon)
v.iconclass = undefined;
items.push(v);
});
(this.refs.listview as ListViewTag).data = items;
@ -412,12 +531,19 @@ namespace OS {
if (v.filename[0] === "." && !this.showhidden) {
return;
}
v.text = v.filename;
v.iconclass = v.iconclass ? v.iconclass : v.type;
if(v.icon)
v.iconclass = undefined;
const row = [
v,
{
text: v.mime,
text: v.filename,
icon: v.icon,
iconclass: v.iconclass,
path: v.path,
data: v
},
{
text: v.mtime,
data: v,
},
{
@ -467,13 +593,15 @@ namespace OS {
if (v.filename[0] === "." && !this.showhidden) {
return undefined;
}
v.text = v.filename;
if(!v.text)
v.text = v.filename;
if (v.type === "dir") {
v.nodes = [];
v.open = false;
}
v.iconclass = v.iconclass ? v.iconclass : v.type;
v.icon = v.icon;
if(v.icon)
v.iconclass = undefined;
return nodes.push(v);
});
return nodes;
@ -511,7 +639,7 @@ namespace OS {
$(this.refs.listview).hide();
$(this.refs.gridview).hide();
$(this.refs.treecontainer).hide();
this._selectedFile = undefined;
this._selectedFiles = [];
switch (this.view) {
case "icon":
$(this.refs.listview).show();
@ -543,13 +671,12 @@ namespace OS {
}
if (this.status) {
(this.refs.status as LabelTag).text = __(
"Selected: {0} ({1} bytes)",
"{0} ({1} bytes)",
e.filename,
e.size ? e.size : "0"
);
}
const evt = { id: this.aid, data: e };
this._selectedFile = e;
this._onfileselect(evt);
this.observable.trigger("fileselect", evt);
}
@ -566,10 +693,11 @@ namespace OS {
e.type = "dir";
e.mime = "dir";
}
const evt = { id: this.aid, data: e };
if (e.type === "dir" && this.chdir) {
this.path = e.path;
} else {
const evt = { id: this.aid, data: e };
this._onfileopen(evt);
this.observable.trigger("fileopen", evt);
}
@ -605,29 +733,38 @@ namespace OS {
};
const grid = this.refs.gridview as GridViewTag;
const list = this.refs.listview as ListViewTag;
list.focusable = true;
grid.focusable = true;
tree.focusable = true;
grid.resizable = true;
grid.header = this._header;
tree.dragndrop = true;
list.dragndrop = true;
grid.dragndrop = true;
// even handles
list.onlistselect = (e) => {
this.fileselect(e.data.item.data as API.FileInfoType);
this._selectedFiles = e.data.items.map( x => x.data as API.FileInfoType);
};
grid.onrowselect = (e) => {
this.fileselect(
$(e.data.item).children()[0]
.data as API.FileInfoType
($(e.data.item).children()[0] as GridCellPrototype)
.data.data as API.FileInfoType
);
this._selectedFiles = e.data.items.map( x => ($(x).children()[0] as GridCellPrototype).data.data as API.FileInfoType);
};
tree.ontreeselect = (e) => {
this.fileselect(e.data.item.data as API.FileInfoType);
this._selectedFiles = [e.data.item.data as API.FileInfoType];
};
// dblclick
list.onlistdbclick = (e) => {
this.filedbclick(e.data.item.data as API.FileInfoType);
};
grid.oncelldbclick = (e) => {
this.filedbclick(e.data.item.data as API.FileInfoType);
this.filedbclick(e.data.item.data.data as API.FileInfoType);
};
tree.ontreedbclick = (e) => {
this.filedbclick(e.data.item.data as API.FileInfoType);

View File

@ -48,30 +48,6 @@ namespace OS {
this._onready = v;
}
/**
* Setter:
*
* Set the direction of the list item layout.
* Two directions are available:
* - `vertical`
* - `horizontal`
*
* This setter acts as a DOM attribute
*
* Getter:
*
* Get the currently set direction of list
* item layout
*
* @memberof FloatListTag
*/
set dir(v: string) {
$(this).attr("dir", v);
this.calibrate();
}
get dir(): string {
return $(this).attr("dir");
}
/**
* Disable the dropdown option in this list
@ -125,6 +101,7 @@ namespace OS {
* @memberof FloatListTag
*/
protected mount(): void {
$(this.refs.current).hide();
$(this.refs.container)
.css("width", "100%")
.css("height", "100%");
@ -147,49 +124,26 @@ namespace OS {
*/
push(v: GenericObject<any>) {
const el = super.push(v);
this.enable_drag(el);
return el;
}
/**
* Enable drag and drop on the list
*
* @private
* @param {ListViewItemTag} el the list item DOM element
* @memberof FloatListTag
*/
private enable_drag(el: ListViewItemTag): void {
$(el)
.css("user-select", "none")
.css("cursor", "default")
.css("display", "block")
.css("position", "absolute")
.on("mousedown", (evt) => {
const globalof = $(this.refs.mlist).offset();
evt.preventDefault();
const offset = $(el).offset();
offset.top = evt.clientY - offset.top;
offset.left = evt.clientX - offset.left;
const mouse_move = function (
e: JQuery.MouseEventBase
) {
let top = e.clientY - offset.top - globalof.top;
let left =
e.clientX - globalof.left - offset.left;
left = left < 0 ? 0 : left;
top = top < 0 ? 0 : top;
return $(el)
.css("top", `${top}px`)
.css("left", `${left}px`);
};
var mouse_up = function (e: JQuery.MouseEventBase) {
$(window).off("mousemove", mouse_move);
return $(window).off("mouseup", mouse_up);
};
$(window).on("mousemove", mouse_move);
return $(window).on("mouseup", mouse_up);
});
.css("position", "absolute");
el.enable_drag();
$(el).on("dragging", (evt) => {
const e = evt.originalEvent as CustomEvent;
const globalof = $(this.refs.mlist).offset();
const offset = e.detail.offset;
let top = e.detail.current.clientY - offset.top - globalof.top;
let left =
e.detail.current.clientX - globalof.left - offset.left;
left = left < 0 ? 0 : left;
top = top < 0 ? 0 : top;
$(el)
.css("top", `${top}px`)
.css("left", `${left}px`);
})
return el;
}
/**
@ -215,7 +169,7 @@ namespace OS {
.css("left", `${cleft}px`);
const w = $(e).width();
const h = $(e).height();
if (this.dir === "vertical") {
if (this.dir === "column") {
ctop += h + 20;
if (ctop + h > gh) {
ctop = 20;

View File

@ -19,6 +19,10 @@ interface Array<T> {
namespace OS {
export namespace GUI {
export namespace tag {
/**
* Row item event data type
*/
export type GridRowEventData = TagEventDataType<GridRowTag>;
/**
* A grid Row is a simple element that
* contains a group of grid cell
@ -31,10 +35,20 @@ namespace OS {
/**
* Data placeholder for a collection of cell data
*
* @private
* @type {GenericObject<any>[]}
* @memberof GridRowTag
*/
data: GenericObject<any>[];
private _data: GenericObject<any>[];
/**
* placeholder for the row select event callback
*
* @private
* @type {TagEventCallback<GridRowEventData>}
* @memberof ListViewItemTag
*/
private _onselect: TagEventCallback<GridRowEventData>;
/**
*Creates an instance of GridRowTag.
@ -44,15 +58,64 @@ namespace OS {
super();
this.refs.yield = this;
this._onselect = (e) => { };
}
/**
* Set item select event handle
*
* @memberof ListViewItemTag
*/
set onrowselect(v: TagEventCallback<GridRowEventData>) {
this._onselect = v;
}
/**
* Setter: select/unselect the current item
*
* Getter: Check whether the current item is selected
*
* @memberof ListViewItemTag
*/
set selected(v: boolean) {
this.attsw(v, "selected");
$(this).removeClass();
if (!v) {
return;
}
$(this).addClass("afx-grid-row-selected");
this._onselect({ id: this.aid, data: this });
}
get selected(): boolean {
return this.hasattr("selected");
}
/**
* setter: set row data
*
* getter: get row data
*/
set data(v: GenericObject<any>[]) {
this._data = v;
if(v)
{
this.attach(v);
}
for (let celi = 0; celi < this.children.length; celi++) {
const cel = (this.children[celi] as GridCellPrototype);
cel.data = v[celi];
}
}
get data(): GenericObject<any>[] {
return this._data;
}
/**
* Mount the tag, do nothing
*
* @protected
* @memberof GridRowTag
*/
protected mount(): void {}
protected mount(): void { }
/**
* Init the tag before mounting: reset the data placeholder
@ -81,7 +144,7 @@ namespace OS {
* @protected
* @memberof GridRowTag
*/
protected calibrate(): void {}
protected calibrate(): void { }
/**
* This function does nothing in this tag
@ -90,7 +153,7 @@ namespace OS {
* @param {*} [d]
* @memberof GridRowTag
*/
protected reload(d?: any): void {}
protected reload(d?: any): void { }
}
/**
@ -166,7 +229,7 @@ namespace OS {
* Setter:
*
* Set the data of the cell, this will trigger
* the [[ondatachange]] function
* the {@link ondatachange} function
*
* Getter:
*
@ -177,6 +240,7 @@ namespace OS {
set data(v: GenericObject<any>) {
if (!v) return;
this._data = v;
this.attach(v);
this.ondatachange();
if (!v.selected) {
return;
@ -191,7 +255,7 @@ namespace OS {
* Setter:
*
* Set/unset the current cell as selected.
* This will trigger the [[cellselect]]
* This will trigger the {@link cellselect}
* event
*
* Getter:
@ -234,14 +298,14 @@ namespace OS {
$(this).attr("class", "afx-grid-cell");
this.oncelldbclick = this.oncellselect = (
e: TagEventType<GridCellPrototype>
): void => {};
): void => { };
this.selected = false;
$(this).css("display", "block");
$(this).on("click",(e) => {
//$(this).css("display", "block");
$(this).on("click", (e) => {
let evt = { id: this.aid, data: this };
return this.cellselect(evt, false);
});
$(this).on("dblclick", (e) => {
$(this).on(OS.mobile?"dbltap":"dblclick", (e) => {
let evt = { id: this.aid, data: this };
return this.cellselect(evt, true);
});
@ -281,7 +345,7 @@ namespace OS {
/**
* Simple grid cell defines a grid cell with
* an [[LabelTag]] as it cell layout
* an {@link LabelTag} as it cell layout
*
* @export
* @class SimpleGridCellTag
@ -303,6 +367,9 @@ namespace OS {
* @memberof SimpleGridCellTag
*/
protected ondatachange(): void {
const label = (this.refs.cell as LabelTag);
label.icon = undefined;
label.iconclass = undefined;
(this.refs.cell as LabelTag).set(this.data);
}
@ -312,7 +379,7 @@ namespace OS {
* @protected
* @memberof SimpleGridCellTag
*/
protected init(): void {}
protected init(): void { }
/**
* This function do nothing in this tag
@ -320,10 +387,10 @@ namespace OS {
* @protected
* @memberof SimpleGridCellTag
*/
protected calibrate(): void {}
protected calibrate(): void { }
/**
* The layout of the cell with a simple [[LabelTag]]
* The layout of the cell with a simple {@link LabelTag}
*
* @returns
* @memberof SimpleGridCellTag
@ -420,14 +487,104 @@ namespace OS {
*/
private _oncelldbclick: TagEventCallback<CellEventData>;
/**
* Event data passing between mouse event when performing
* drag and drop on the list
*
* @private
* @type {{ from: GridRowTag[]; to: GridRowTag }}
* @memberof GridViewTag
*/
private _dnd: { from: GridRowTag[]; to: GridRowTag };
/**
* Grid navigation index
*
* @private
* @type {number}
*/
private _nav_index: number;
/**
* placeholder of list drag and drop event handle
*
* @private
* @type {TagEventCallback<DnDEventDataType<GridRowTag>>}
* @memberof GridViewTag
*/
private _ondragndrop: TagEventCallback<
DnDEventDataType<GridRowTag>
>;
/**
* Creates an instance of GridViewTag.
* @memberof GridViewTag
*/
constructor() {
super();
this._nav_index = -1;
}
/**
* Set drag and drop event handle
*
* @memberof GridViewTag
*/
set ondragndrop(
v: TagEventCallback<DnDEventDataType<GridRowTag>>
) {
this._ondragndrop = v;
this.dragndrop = this.dragndrop;
}
/**
* Setter: Enable/disable drag and drop event in the list
*
* Getter: Check whether the drag and drop event is enabled
*
* @memberof GridViewTag
*/
set dragndrop(v: boolean) {
this.attsw(v, "dragndrop");
if (!v) {
$(this.refs.container).off("mousedown", this._onmousedown);
}
else {
$(this.refs.container).on(
"mousedown",
this._onmousedown
);
}
}
get dragndrop(): boolean {
return this.hasattr("dragndrop");
}
/**
* placeholder of drag and drop mouse down event handle
*
* @private
* @memberof GridViewTag
*/
private _onmousedown: (e: JQuery.MouseEventBase) => void;
/**
* placeholder of drag and drop mouse up event handle
*
* @private
* @memberof GridViewTag
*/
private _onmouseup: (e: JQuery.MouseEventBase) => void;
/**
* placeholder of drag and drop mouse move event handle
*
* @private
* @memberof GridViewTag
*/
private _onmousemove: (e: JQuery.MouseEventBase) => void;
/**
* Init the grid view before mounting.
* Reset all the placeholders to default values
@ -444,9 +601,13 @@ namespace OS {
this._selectedRow = undefined;
this._rows = [];
this.resizable = false;
this.dragndrop = false;
this._oncellselect = this._onrowselect = this._oncelldbclick = (
e: TagEventType<CellEventData>
): void => {};
): void => { };
this._ondragndrop = (
e: TagEventType<DnDEventDataType<GridRowTag>>
) => { };
}
/**
@ -456,7 +617,7 @@ namespace OS {
* @param {*} [d]
* @memberof GridViewTag
*/
protected reload(d?: any): void {}
protected reload(d?: any): void { }
/**
* set the cell select event callback
@ -507,7 +668,13 @@ namespace OS {
* @memberof GridViewTag
*/
set cellitem(v: string) {
const currci = this.cellitem;
$(this).attr("cellitem", v);
if (v != currci) {
// force render data
$(this.refs.grid).empty();
this.rows = this.rows;
}
}
get cellitem(): string {
return $(this).attr("cellitem");
@ -538,14 +705,24 @@ namespace OS {
)[0] as GridCellPrototype;
element.uify(this.observable);
element.data = item;
item.domel = element;
element.oncellselect = (e) => {
if (element.data.sort) {
this.sort(element.data, element.data.sort);
if (element.data.desc) {
$(element).attr("sort", "desc");
}
else {
$(element).attr("sort", "asc");
}
}
};
i++;
if (this.resizable) {
if (i != v.length) {
const rz = $(`<afx-resizer>`).appendTo(
this.refs.header
)[0] as ResizerTag;
$(rz).css("width", "3px");
$(rz).css("width", "1px");
let next_item = undefined;
if (i < v.length) {
next_item = v[i];
@ -604,9 +781,38 @@ namespace OS {
* @memberof GridViewTag
*/
set rows(rows: GenericObject<any>[][]) {
$(this.refs.grid).empty();
this._rows = rows;
rows.map((row) => this.push(row, false));
if (!rows) return;
for (const el of this._header) {
$(el.domel).attr("sort", "none");
}
// update existing row with new data
const ndrows = rows.length;
const ncrows = this.refs.grid.children.length;
const nmin = ndrows < ncrows ? ndrows : ncrows;
if (this.selectedRow) {
this.selectedRow.selected = false;
this._selectedRow = undefined;
this._selectedRows = [];
}
for (let i = 0; i < nmin; i++) {
const rowel = (this.refs.grid.children[i] as GridRowTag);
rowel.data = rows[i];
}
// remove existing remaining rows
if (ndrows < ncrows) {
const arr = Array.prototype.slice.call(this.refs.grid.children);
const blacklist = arr.slice(nmin, ncrows);
for (const r of blacklist) {
this.delete(r);
}
}
// or add more rows
else if (ndrows > ncrows) {
for (let i = nmin; i < ndrows; i++) {
this.push(rows[i], false);
}
}
}
get rows(): GenericObject<any>[][] {
return this._rows;
@ -639,7 +845,24 @@ namespace OS {
get resizable(): boolean {
return this.hasattr("resizable");
}
/**
* Sort the grid using a sort function
*
* @param {context: any} context of the executed function
* @param {(a:GenericObject<any>[], b:GenericObject<any>[]) => boolean} a sort function that compares two rows data
* * @param {index: number} current header index
* @returns {void}
* @memberof GridViewTag
*/
sort(context: any, fn: (a: GenericObject<any>[], b: GenericObject<any>[], index?: number) => number): void {
const index = this._header.indexOf(context);
const __fn = (a, b) => {
return fn.call(context, a, b, index);
}
this._rows.sort(__fn);
context.desc = !context.desc;
this.rows = this._rows;
}
/**
* Delete a grid rows
*
@ -670,13 +893,61 @@ namespace OS {
$(row).remove();
}
/**
* Scroll the grid view to item
*
* @memberof GridViewTag
*/
scroll_to_item(item: GridRowTag)
{
const cell = $(item).children()[0];
const offset = $(this.refs.container).offset();
const cell_offset = $(cell).offset();
const top = $(this.refs.container).scrollTop();
const cell_height = $(cell).outerHeight();
const container_height = $(this.refs.container).outerHeight();
if (cell_offset.top + cell_height > container_height + offset.top)
{
$(this.refs.container).scrollTop(
top +
(cell_offset.top + cell_height - offset.top - container_height)
);
} else if (cell_offset.top < offset.top) {
$(this.refs.container).scrollTop(
top -
(offset.top - cell_offset.top)
);
}
}
/**
* Scroll the grid view to bottom
*
* @memberof GridViewTag
*/
scroll_to_bottom()
{
this.refs.container.scrollTo({ top: this.refs.container.scrollHeight, behavior: 'smooth' })
}
/**
* Scroll the grid view to top
*
* @memberof GridViewTag
*/
scroll_to_top()
{
this.refs.container.scrollTo({ top: 0, behavior: 'smooth' });
}
/**
* Push a row to the grid
*
* @param {GenericObject<any>[]} row list of cell data
* @param {boolean} flag indicates where the row is add to beginning or end
* of the row
* @memberof GridViewTags
* @memberof GridViewTag
*/
push(row: GenericObject<any>[], flag: boolean): void {
const rowel = $("<afx-grid-row>").css(
@ -697,22 +968,23 @@ namespace OS {
const el = rowel[0] as GridRowTag;
rowel[0].uify(this.observable);
el.data = row;
row.domel = rowel[0];
for (let cell of row) {
let tag = this.cellitem;
if (cell.tag) {
({ tag } = cell);
tag = cell.tag;
}
const el = $(`<${tag}>`).appendTo(rowel);
cell.domel = el[0];
const element = el[0] as GridCellPrototype;
element.uify(this.observable);
element.oncellselect = (e) => this.cellselect(e, false);
element.oncelldbclick = (e) => this.cellselect(e, true);
element.data = cell;
}
el.data = row;
el.onrowselect = (e) => this.rowselect({
id: el.aid,
data: { item: el }
});
}
/**
@ -751,7 +1023,10 @@ namespace OS {
} else {
this.observable.trigger("cellselect", e);
this._oncellselect(e);
return this.rowselect(e);
const row = ($(
e.data.item
).parent()[0] as any) as GridRowTag;
row.selected = true;
}
}
@ -759,11 +1034,11 @@ namespace OS {
* This function triggers the row select event, a cell select
* event will also trigger this event
*
* @param {TagEventType<CellEventData>} e
* @param {TagEventType<GridRowEventData>} e
* @returns {void}
* @memberof GridViewTag
*/
private rowselect(e: TagEventType<CellEventData>): void {
private rowselect(e: TagEventType<GridRowEventData>): void {
if (!e.data.item) {
return;
}
@ -774,39 +1049,56 @@ namespace OS {
items: [],
},
};
const row = ($(
e.data.item
).parent()[0] as any) as GridRowTag;
const row = e.data.item as GridRowTag;
if (this.multiselect) {
if (this.selectedRows.includes(row)) {
this.selectedRows.splice(
this.selectedRows.indexOf(row),
1
);
$(row).removeClass();
row.selected = false;
return;
} else {
this.selectedRows.push(row);
$(row)
.removeClass()
.addClass("afx-grid-row-selected");
}
evt.data.items = this.selectedRows;
} else {
if (this.selectedRows.length > 0) {
for (const item of this.selectedRows) {
if (item != row) {
item.selected = false;
}
}
}
if (this.selectedRow === row) {
return;
}
$(this.selectedRow).removeClass();
this._selectedRows = [row];
evt.data.item = row;
if (this.selectedRow)
this.selectedRow.selected = false;
evt.data.items = [row];
$(row).removeClass().addClass("afx-grid-row-selected");
this._selectedRows = [row];
}
evt.data.item = row;
this._selectedRow = row;
this._nav_index = $(row).index();
this._onrowselect(evt);
return this.observable.trigger("rowselect", evt);
}
/**
* Unselect all the selected rows in the grid
*
* @returns {void}
* @memberof GridViewTag
*/
unselect(): void {
for (let v of this.selectedRows) {
v.selected = false;
}
this._selectedRows = [];
this._selectedRow = undefined;
}
/**
* Check whether the grid has header
*
@ -831,8 +1123,8 @@ namespace OS {
$(this.refs.container).css(
"height",
$(this).height() -
$(this.refs.header).height() +
"px"
$(this.refs.header).height() +
"px"
);
} else {
$(this.refs.container).css(
@ -882,8 +1174,8 @@ namespace OS {
let i = 0;
for (let v of colssize) {
template += `${v}px `;
i++;
template_header += `${v}px `;
i++;
if (i < colssize.length && this.resizable) {
template_header += "3px ";
}
@ -893,6 +1185,105 @@ namespace OS {
"grid-template-columns",
template_header
);
if(this.resizable)
{
$(this.refs.grid).css("column-gap","3px");
}
}
/**
* Reset the navigation indicator
*
* @private
*/
private nav_reset() {
const el = $(this.refs.grid).children().eq(this._nav_index);
if(el)
{
$(el).removeClass("gridview-nav-focus");
}
}
/**
* Navigate the list up
*
* @public
* @returns {void}
* @memberof ListViewTag
*/
public nav_prev() {
console.log("nav_prev");
if(this._nav_index <= 0) {
return;
}
this.nav_reset();
this._nav_index--;
const el = $(this.refs.grid).children().eq(this._nav_index);
if(el) {
$(el).addClass("gridview-nav-focus");
this.scroll_to_item(el[0] as GridRowTag);
}
}
/**
* Navigate the list down
*
* @returns {void}
* @memberof ListViewTag
*/
public nav_next() {
console.log("nav_next");
if(this._nav_index >= this.rows.length - 1) {
return;
}
this.nav_reset();
this._nav_index++;
const el = $(this.refs.grid).children().eq(this._nav_index);
if(el) {
$(el).addClass("gridview-nav-focus");
this.scroll_to_item(el[0] as GridRowTag);
}
}
/**
* Commit the navigated item
*
* @returns {void}
* @memberof ListViewTag
*/
public nav_commit() {
if(this._nav_index > this.rows.length - 1 || this._nav_index < 0) {
return;
}
const el = $(this.refs.grid).children().eq(this._nav_index);
if(el) {
(el[0] as GridRowTag).selected = true;
}
}
/**
* Handle special key event such as key up and down
*
* @private
* @param {JQuery.KeyboardEventBase} event
* @returns {void}
*/
private handle_special_key(event: JQuery.KeyboardEventBase) {
switch (event.which) {
case 37:
case 38:
this.nav_prev();
return event.preventDefault();
case 39:
case 40:
this.nav_next();
return event.preventDefault();
case 13:
event.preventDefault();
return this.nav_commit();
default:
break;
}
}
/**
@ -903,8 +1294,10 @@ namespace OS {
* @memberof GridViewTag
*/
protected mount(): void {
$(this).on("keyup", (e) => {
this.handle_special_key(e);
});
$(this).css("overflow", "hidden");
$(this.refs.grid).css("display", "grid");
$(this.refs.header).css("display", "grid");
this.observable.on("resize", (e) => this.calibrate());
@ -912,6 +1305,70 @@ namespace OS {
.css("width", "100%")
.css("overflow-x", "hidden")
.css("overflow-y", "auto");
// drag and drop
this._dnd = {
from: undefined,
to: undefined,
};
this._onmousedown = (e) => {
if (this.multiselect || this.selectedRows == undefined || this.selectedRows.length == 0) {
return;
}
let el: any = $(e.target).closest("afx-grid-row");
if (el.length === 0) {
return;
}
el = el[0];
if (!this.selectedRows.includes(el)) {
return;
}
this._dnd.from = this.selectedRows;
this._dnd.to = undefined;
$(window).on("mouseup", this._onmouseup);
$(window).on("mousemove", this._onmousemove);
};
this._onmouseup = (e) => {
$(window).off("mouseup", this._onmouseup);
$(window).off("mousemove", this._onmousemove);
$("#systooltip").hide();
let el: any = $(e.target).closest("afx-grid-row");
if (el.length === 0) {
return;
}
el = el[0];
if (this._dnd.from.includes(el)) {
return;
}
this._dnd.to = el;
this._ondragndrop({ id: this.aid, data: this._dnd });
this._dnd = {
from: undefined,
to: undefined,
};
};
this._onmousemove = (e) => {
if (!e) {
return;
}
if (!this._dnd.from) {
return;
}
const data = {
text: __("{0} selected elements", this._dnd.from.length).__(),
items: this._dnd.from
};
const $label = $("#systooltip");
const top = e.clientY + 5;
const left = e.clientX + 5;
$label.show();
const label = $label[0] as LabelTag;
label.set(data);
return $label
.css("top", top + "px")
.css("left", left + "px");
};
return this.calibrate();
}
@ -928,6 +1385,7 @@ namespace OS {
{
el: "div",
ref: "container",
class: "grid_content_container",
children: [{ el: "div", ref: "grid" }],
},
];

265
src/core/tags/InputTag.ts Normal file
View File

@ -0,0 +1,265 @@
namespace OS {
export namespace GUI {
export namespace tag {
/**
* This tag define a basic text input and its behavior
*
* @export
* @class InputTag
* @extends {AFXTag}
*/
export class InputTag extends AFXTag {
/**
*Creates an instance of InputTag.
* @memberof InputTag
*/
constructor() {
super();
}
/**
* Set the path to the header icon, the path should be
* a VFS file path
*
* @memberof InputTag
*/
set icon(v: string) {
$(this).attr("icon", v);
(this.refs.label as LabelTag).icon = v;
}
/**
* Set the icon class to the header
*
* @memberof InputTag
*/
set iconclass(v: string) {
$(this).attr("iconclass", v);
(this.refs.label as LabelTag).iconclass = v;
}
/**
* Alias to header setter/getter
*
* @memberof InputTag
*/
set text(v: string | FormattedString) {
this.label = v;
}
get text(): string | FormattedString {
return this.label;
}
/**
* Setter: Set the text of the label
*
* Getter: Get the current label test
*
* @memberof InputTag
*/
set label(v: string | FormattedString) {
(this.refs.label as LabelTag).text = v;
}
get label(): string | FormattedString {
return (this.refs.label as LabelTag).text;
}
/**
* Setter: Enable or disable the input
*
* Getter: Get the `enable` property of the input
*
* @memberof InputTag
*/
set disable(v: boolean) {
$(this.refs.area).prop("disabled", v);
$(this.refs.input).prop("disabled", v);
}
get disable(): boolean {
return !$(this.input).prop("disabled");
}
/**
* Setter: set verbosity of the input
*
* Getter: Get the current input verbosity
*
* @memberof InputTag
*/
set verbose(v: boolean) {
this.attsw(v, "verbose");
this.calibrate();
}
get verbose(): boolean {
return this.hasattr("verbose");
}
/**
* JQuery style generic event handling on the input element
*
* @param {string} enname: JQuery event name
* @param {JQuery.TypeEventHandler<HTMLInputElement | HTMLTextAreaElement, unknown, any, any, string>} handle: JQuery handle
* @memberof InputTag
*/
on(ename: string, handle:JQuery.TypeEventHandler<HTMLInputElement | HTMLTextAreaElement, unknown, any, any, string> ): void
{
$(this.input).on(ename, handle);
}
/**
* Manually trigger an event
*
* @param {string} evt: JQuery event name
* @memberof InputTag
*/
trigger(evt: string)
{
$(this.input).trigger(evt);
}
/**
* Mount the tag
*
* @protected
* @memberof InputTag
*/
protected mount() {
// Do nothing
}
/**
* Get the current active input element
*
* @memberof InputTag
*/
get input(): HTMLInputElement | HTMLTextAreaElement
{
if(this.verbose)
{
return this.refs.area as HTMLTextAreaElement;
}
return this.refs.input as HTMLInputElement;
}
/**
* Get/set the current active input value
*
* @memberof InputTag
*/
get value(): string{
return this.input.value;
}
set value(v: string)
{
this.input.value = v;
}
/**
* Get/set input type
* This only affects the inline input element
*
* @memberof InputTag
*/
get type(): string{
if(this.verbose) return undefined;
return (this.input as HTMLInputElement).type;
}
set type(v: string)
{
if(!this.verbose)
{
(this.input as HTMLInputElement).type = v;
}
}
/**
* Get/set input name
*
* @memberof InputTag
*/
get name(): string{
return (this.input as HTMLInputElement).name;
}
set name(v: string)
{
(this.input as HTMLInputElement).name = v;
}
/**
* Init the tag before mounting
*
* @protected
* @memberof InputTag
*/
protected init(): void {
this.disable = false;
this.verbose = false;
this.type = "text";
}
/**
* Re-calibrate, do nothing in this tag
*
* @protected
* @memberof InputTag
*/
protected calibrate(): void
{
/*$(this.refs.area)
.css("width", "100%");
$(this.refs.input)
.css("width", "100%");*/
if(this.verbose)
{
$(this.refs.area).show();
$(this.refs.input).hide();
(this.refs.input as HTMLInputElement).value = "";
}
else
{
$(this.refs.area).hide();
$(this.refs.input).show();
(this.refs.area as HTMLTextAreaElement).value = "";
}
}
/**
* Update the current tag, do nothing in this tag
*
* @param {*} [d]
* @memberof InputTag
*/
reload(d?: any): void {}
/**
* Input layout definition
*
* @protected
* @returns {TagLayoutType[]}
* @memberof InputTag
*/
protected layout(): TagLayoutType[] {
return [
{
el: "afx-label",
ref: "label"
},
{
el: "input",
ref:"input"
},
{
el: "textarea",
ref: "area"
},
{
el: "div"
}
];
}
}
define("afx-input", InputTag);
}
}
}

View File

@ -33,7 +33,18 @@ namespace OS {
* @protected
* @memberof LabelTag
*/
protected mount() {}
protected mount() {
$(this.refs.container)
.css("display", "flex");
$(this.refs.iclass)
.css("flex-shrink",0);
$(this.refs.iclass_end)
.css("flex-shrink",0);
$(this.refs.i)
.css("flex-shrink",0);
$(this.refs.text)
.css("flex",1);
}
/**
* Refresh the text in the label
@ -56,6 +67,8 @@ namespace OS {
this.icon = undefined;
this.iconclass = undefined;
this.text = undefined;
this.selectable = false;
this.iconclass$ = undefined;
}
/**
@ -84,6 +97,49 @@ namespace OS {
$(this.refs.i).hide();
}
}
/**
* set horizontal aligment of the label content
*
* @param {string} v shall be "left, right, or center"
*/
set halign(v: string)
{
let align = "center";
switch(v)
{
case "left":
align = "flex-start";
break;
case "right":
align = "flex-end";
break;
default:
break;
}
$(this.refs.container).css("justify-content", align);
}
/**
* set horizontal aligment of the label content
*
* @param {string} v shall be "top, bottom, or center"
*/
set valign(v: string)
{
let align = "center";
switch(v)
{
case "top":
align = "flex-start";
break;
case "bottom":
align = "flex-end";
break;
default:
break;
}
$(this.refs.container).css("align-items", align);
}
/**
* Set the CSS class of the label icon
@ -101,6 +157,31 @@ namespace OS {
}
}
/**
* Set the CSS class of the label icon on the right side
*
* @memberof LabelTag
*/
set iconclass_end(v: string) {
this.iconclass$ = v;
}
/**
* Set the CSS class of the label icon on the right side
*
* @memberof LabelTag
*/
set iconclass$(v: string) {
$(this).attr("iconclass_end", v);
$(this.refs.iclass_end).removeClass();
if (v) {
$(this.refs.iclass_end).addClass(v);
$(this.refs.iclass_end).show();
} else {
$(this.refs.iclass_end).hide();
}
}
/**
* Setter: Set the text of the label
*
@ -110,9 +191,9 @@ namespace OS {
*/
set text(v: string | FormattedString) {
this._text = v;
if (v && v !== "") {
if (v) {
$(this.refs.text).show();
$(this.refs.text).html(v.__());
$(this.refs.text).text(v.__());
} else {
$(this.refs.text).hide();
}
@ -121,6 +202,33 @@ namespace OS {
return this._text;
}
/**
* Setter: Turn on/off text selection
*
* Getter: Check whether the label is selectable
*
* @memberof LabelTag
*/
set selectable(v: boolean) {
this.attsw(v, "selectable");
if(v)
{
$(this.refs.text)
.css("user-select", "text")
.css("cursor", "text");
}
else
{
$(this.refs.text)
.css("user-select", "none")
.css("cursor", "default");
}
}
get swon(): boolean {
return this.hasattr("selectable");
}
/**
* Lqbel layout definition
*
@ -137,6 +245,7 @@ namespace OS {
{ el: "i", ref: "iclass" },
{ el: "i", ref: "i", class: "icon-style" },
{ el: "i", ref: "text", class: "label-text" },
{ el: "i", ref: "iclass_end" },
],
},
];

View File

@ -6,7 +6,7 @@ namespace OS {
*/
export type ListItemEventData = TagEventDataType<ListViewItemTag>;
/**
* A list item represent the individual view of an item in the [[ListView]].
* A list item represent the individual view of an item in the {@link OS.GUI.tag.ListViewTag}.
* This class is an abstract prototype class, implementation of any
* list view item should extend it
*
@ -42,7 +42,7 @@ namespace OS {
* @type {TagEventCallback<ListItemEventData>}
* @memberof ListViewItemTag
*/
private _onctxmenu: TagEventCallback<ListItemEventData>;
//private _onctxmenu: TagEventCallback<ListItemEventData>;
/**
* Click event callback placeholder
@ -77,7 +77,7 @@ namespace OS {
*/
constructor() {
super();
this._onselect = this._onctxmenu = this._onclick = this._ondbclick = this._onclose = (
this._onselect /*= this._onctxmenu*/ = this._onclick = this._ondbclick = this._onclose = (
e
) => {};
}
@ -134,9 +134,11 @@ namespace OS {
*
* @memberof ListViewItemTag
*/
/*
set onctxmenu(v: TagEventCallback<ListViewItemTag>) {
this._onctxmenu = v;
}
*/
/**
* Set the item click event handle
@ -172,20 +174,23 @@ namespace OS {
* @memberof ListViewItemTag
*/
protected mount(): void {
$(this.refs.item).attr("dataref", "afx-list-item");
$(this.refs.item).on("contextmenu", (e) => {
$(this).addClass("afx-list-item");
/*
$(this.refs.item).on(OS.mobile?"longtouch":"contextmenu", (e) => {
this._onctxmenu({ id: this.aid, data: this });
});
*/
$(this.refs.item).on("click",(e) => {
this._onclick({ id: this.aid, data: this });
this._onclick({ id: this.aid, data: this, originalEvent: e });
//e.stopPropagation();
});
$(this.refs.item).on("dblclick",(e) => {
this._ondbclick({ id: this.aid, data: this });
$(this.refs.item).on(OS.mobile?"dbltap":"dblclick",(e) => {
this._ondbclick({ id: this.aid, data: this, originalEvent: e });
e.stopPropagation();
});
$(this.refs.btcl).on("click",(e) => {
this._onclose({ id: this.aid, data: this });
this._onclose({ id: this.aid, data: this, originalEvent: e });
e.preventDefault();
e.stopPropagation();
});
@ -195,21 +200,29 @@ namespace OS {
* Layout definition of the item tag.
* This function define the outer layout of the item.
* Custom inner layout of each item implementation should
* be defined in [[itemlayout]]
* be defined in {@link itemlayout}
*
* @protected
* @returns {TagLayoutType[]}
* @memberof ListViewItemTag
*/
protected layout(): TagLayoutType[] {
let children = [{el: "i", class: "closable", ref: "btcl"}] as TagLayoutType[];
const itemlayout = this.itemlayout();
if(Array.isArray(itemlayout))
{
children = children.concat(itemlayout);
}
else
{
children.unshift(itemlayout);
}
return [
{
el: "li",
ref: "item",
children: [
this.itemlayout(),
{ el: "i", class: "closable", ref: "btcl" },
],
children:children,
},
];
}
@ -218,7 +231,7 @@ namespace OS {
* Setter:
*
* Set the data of the list item. This will
* trigger the [[ondatachange]] function
* trigger the {@link ondatachange} function
*
* Getter:
*
@ -228,6 +241,10 @@ namespace OS {
*/
set data(v: GenericObject<any>) {
this._data = v;
if(v)
{
this.attach(v);
}
this.ondatachange();
}
get data(): GenericObject<any> {
@ -240,10 +257,10 @@ namespace OS {
*
* @protected
* @abstract
* @returns {TagLayoutType}
* @returns {TagLayoutType | TagLayoutType[]}
* @memberof ListViewItemTag
*/
protected abstract itemlayout(): TagLayoutType;
protected abstract itemlayout(): TagLayoutType | TagLayoutType[];
/**
* This function is called when the item data is changed.
@ -329,14 +346,101 @@ namespace OS {
* List item custom layout definition
*
* @protected
* @returns {TagLayoutType}
* @returns {TagLayoutType | TagLayoutType[]}
* @memberof SimpleListItemTag
*/
protected itemlayout(): TagLayoutType {
protected itemlayout(): TagLayoutType | TagLayoutType[] {
return { el: "afx-label", ref: "label" };
}
}
/**
* The layout of a double line list item contains two
* AFX labels
*
* @export
* @class DoubleLineListItemTag
* @extends {ListViewItemTag}
*/
export class DoubleLineListItemTag extends ListViewItemTag {
/**
*Creates an instance of DoubleLineListItemTag.
* @memberof DoubleLineListItemTag
*/
constructor() {
super();
}
/**
* Reset some property to default
*
* @protected
* @memberof DoubleLineListItemTag
*/
protected init(): void {
this.closable = false;
this.data = {};
}
/**
* Do nothing
*
* @protected
* @memberof DoubleLineListItemTag
*/
protected calibrate(): void {}
/**
* Refresh the inner label when the item data
* is changed
*
* @protected
* @returns {void}
* @memberof DoubleLineListItemTag
*/
protected ondatachange(): void {
const v = this.data;
if (!v) {
return;
}
const line1 = this.refs.line1 as LabelTag;
const line2 = this.refs.line2 as LabelTag;
line1.set(v);
if(v.description)
{
line2.set(v.description);
}
if (v.selected) {
this.selected = v.selected;
}
if (v.closable) {
this.closable = v.closable;
}
}
/**
* Re-render the list item
*
* @protected
* @memberof DoubleLineListItemTag
*/
protected reload(): void {
this.data = this.data;
}
/**
* List item custom layout definition
*
* @protected
* @returns {TagLayoutType | TagLayoutType[]}
* @memberof DoubleLineListItemTag
*/
protected itemlayout(): TagLayoutType | TagLayoutType[] {
return [{ el: "afx-label", ref: "line1", class:"title" }, { el: "afx-label", ref: "line2", class:"description" }];
}
}
/**
* This tag defines a traditional or a dropdown list widget.
* It contains a collection of list items in which layout
@ -422,14 +526,22 @@ namespace OS {
/**
* A collection of selected items in the list.
* The maximum size of this collection is 1 if
* the [[multiselect]] feature is disabled
* the {@link multiselect} feature is disabled
*
* @private
* @type {ListViewItemTag[]}
* @memberof ListViewTag
*/
private _selectedItems: ListViewItemTag[];
/**
* The anchor element that the list view positioned on
* This is helpful when rendering dropdown list
* @private
* @type{HTMLElement}
* @memberof ListViewTag
*/
private _anchor: HTMLElement;
/**
* Data placeholder of the list
*
@ -438,16 +550,28 @@ namespace OS {
* @memberof ListViewTag
*/
private _data: GenericObject<any>[];
/**
* Navigation index, used for keyboard navigation only
*
* @private
* @type {number}
* @memberof ListViewTag
*/
private _nav_index: number;
private _drop: (any) => void;
private _show: (any) => void;
/**
* Event data passing between mouse event when performing
* drag and drop on the list
*
* @private
* @type {{ from: ListViewItemTag; to: ListViewItemTag }}
* @type {{ from: ListViewItemTag[]; to: ListViewItemTag }}
* @memberof ListViewTag
*/
private _dnd: { from: ListViewItemTag; to: ListViewItemTag };
private _dnd: { from: ListViewItemTag[]; to: ListViewItemTag };
/**
*Creates an instance of ListViewTag.
@ -468,6 +592,8 @@ namespace OS {
) => {};
this._selectedItems = [];
this._selectedItem = undefined;
this._drop = (e) => {this.dropoff(e)};
this._show = (e) => {this.showlist(e)};
}
/**
@ -482,10 +608,10 @@ namespace OS {
this.dropdown = false;
this.selected = -1;
this.dragndrop = false;
$(this)
.css("display", "flex")
.css("flex-direction", "column");
this._anchor = undefined;
this.itemtag = "afx-list-item";
this._nav_index = -1;
$(this).addClass("afx-list-view");
}
/**
@ -508,32 +634,20 @@ namespace OS {
this.attsw(v, "dropdown");
$(this.refs.container).removeAttr("style");
$(this.refs.mlist).removeAttr("style");
$(this.refs.container).css("flex", 1);
this.dir = "column";
$(this).removeClass("dropdown");
const drop = (e: any) => {
return this.dropoff(e);
};
const show = (e: any) => {
return this.showlist(e);
};
if (v) {
$(this).addClass("dropdown");
$(this.refs.current).show();
$(document).on("click", drop);
$(this.refs.current).on("click", show);
$(this.refs.container)
.css("position", "absolute")
.css("display", "inline-block");
$(this.refs.mlist)
.css("position", "absolute")
.css("display", "none")
.css("top", "100%")
.css("left", "0");
$(document).on("click", this._drop);
$(this.refs.current).on("click", this._show);
$(this.refs.mlist).hide();
this.calibrate();
} else {
$(document).off("click", this._drop);
$(this.refs.current).off("click", this._show);
$(this.refs.current).hide();
$(document).off("click", drop);
$(this.refs.current).off("click", show);
$(this.refs.mlist).show();
}
}
@ -642,7 +756,7 @@ namespace OS {
* Button layout allows to add some custom
* behaviors to the list.
*
* Each button data should define the [[onbtclick]]
* Each button data should define the {@link OS.GUI.tag.ButtonTag.onbtclick}
* event handle to specify the custom behavior
*
* When the list is configured as dropdown. The buttons
@ -676,7 +790,32 @@ namespace OS {
(bt[0] as ButtonTag).set(item);
}
}
/**
* Getter: Get list direction: row or column (default)
*
* Setter: Get list direction: row or column
*
* @type {string}
* @memberof ListViewTag
*/
set dir(v: string) {
if(this.dropdown)
{
$(this).attr("dir", "column");
}
else
{
$(this).attr("dir", v);
}
this.calibrate();
}
get dir(): string {
const v = $(this).attr("dir");
if(!v) {
return "column";
}
return v;
}
/**
* Getter: Get data of the list
*
@ -737,14 +876,17 @@ namespace OS {
const el = data[i].domel as ListViewItemTag;
el.selected = true;
};
this.nav_reset();
if (Array.isArray(idx)) {
if (this.multiselect) {
for (const i of idx as number[]) {
select(i);
this._nav_index = i;
}
}
} else {
select(idx as number);
this._nav_index = idx;
}
}
@ -769,7 +911,13 @@ namespace OS {
get selectedItems(): ListViewItemTag[] {
return this._selectedItems;
}
/**
* get the selected item index
*
* @readonly
* @type {number}
* @memberof ListViewTag
*/
get selected(): number | number[] {
if (this.multiselect) {
return this.selectedItems.map(function (
@ -808,7 +956,7 @@ namespace OS {
* Add an item to the beginning or end of the list
*
* @param {GenericObject<any>} item list item data
* @param {boolean} [flag] indicates whether to add the item in the beginning of the list
* @param {boolean} flag indicates whether to add the item in the beginning of the list
* @returns {ListViewItemTag} the added list item element
* @memberof ListViewTag
*/
@ -832,9 +980,11 @@ namespace OS {
}
el[0].uify(this.observable);
const element = el[0] as ListViewItemTag;
$(element).attr("list-id",this.aid);
/*
element.onctxmenu = (e) => {
return this.iclick(e, true);
};
};*/
element.onitemdbclick = (e) => {
this.idbclick(e);
this.iclick(e, false);
@ -849,7 +999,6 @@ namespace OS {
return this.iclose(e);
};
element.data = item;
item.domel = el[0];
return element;
}
@ -943,24 +1092,116 @@ namespace OS {
if (!e.data) {
return;
}
this.nav_reset();
this._nav_index = -1;
const list = this.selectedItems;
if (this.multiselect && list.includes(e.data) && !flag) {
list.splice(list.indexOf(e.data), 1);
e.data.selected = false;
return;
}
this._nav_index = $(e.data).index();
e.data.selected = true;
}
/**
* Reset the navigation indicator
*
* @private
*/
private nav_reset() {
if(this._nav_index >= 0 && this._nav_index < this.data.length)
{
$(this.data[this._nav_index].domel as ListViewItemTag).removeClass("listview_nav_focus");
}
}
/**
* Navigate the list up
*
* @public
* @returns {void}
* @memberof ListViewTag
*/
public nav_prev() {
if(this._nav_index <= 0) {
return;
}
this.nav_reset();
this._nav_index--;
const new_el = this.data[this._nav_index].domel as ListViewItemTag;
if(new_el) {
$(new_el).addClass("listview_nav_focus");
this.scroll_to_item(new_el);
}
}
/**
* Navigate the list down
*
* @returns {void}
* @memberof ListViewTag
*/
public nav_next() {
if(this._nav_index >= this.data.length - 1) {
return;
}
this.nav_reset();
this._nav_index++;
const new_el = this.data[this._nav_index].domel as ListViewItemTag;
if(new_el) {
$(new_el).addClass("listview_nav_focus");
this.scroll_to_item(new_el);
}
}
/**
* Commit the navigated item
*
* @returns {void}
* @memberof ListViewTag
*/
public nav_commit() {
if(this._nav_index > this.data.length - 1 || this._nav_index < 0) {
return;
}
this.selected = this._nav_index;
}
/**
* Handle special key event such as key up and down
*
* @private
* @param {JQuery.KeyboardEventBase} event
* @returns {void}
* @memberof ListViewTag
*/
private handle_special_key(event: JQuery.KeyboardEventBase) {
switch (event.which) {
case 37:
case 38:
this.nav_prev();
return event.preventDefault();
case 39:
case 40:
this.nav_next();
return event.preventDefault();
case 13:
event.preventDefault();
return this.nav_commit();
default:
break;
}
}
/**
* This function triggers the double click event on an item
*
* @private
* @protected
* @param {TagEventType} e tag event object
* @returns
* @memberof ListViewTag
*/
private idbclick(e: TagEventType<ListViewItemTag>) {
protected idbclick(e: TagEventType<ListViewItemTag>) {
const evt: TagEventType<ListItemEventData> = {
id: this.aid,
data: { item: e.data },
@ -968,16 +1209,66 @@ namespace OS {
this._onlistdbclick(evt);
return this.observable.trigger("listdbclick", evt);
}
/**
* This function scroll to item if it is not visible
*
* @public
* @param {ListViewItemTag} item tag event object
* @returns
* @memberof ListViewTag
*/
public scroll_to_item(item: ListViewItemTag) {
const li = $(item).children()[0];
const offset = $(this.refs.container).offset();
const li_offset = $(li).offset();
if(this.dir == "column") {
const top = $(this.refs.container).scrollTop();
const li_height = $(li).outerHeight();
const container_height = $(this.refs.container).outerHeight();
if (li_offset.top + li_height > container_height + offset.top)
{
$(this.refs.container).scrollTop(
top +
(li_offset.top + li_height - offset.top - container_height)
);
} else if ($(li).offset().top < offset.top) {
$(this.refs.container).scrollTop(
top -
(offset.top - li_offset.top)
);
}
}
else
{
const left = $(this.refs.container).scrollLeft();
const li_w = $(li).outerWidth();
const container_w = $(this.refs.container).outerWidth();
if (li_offset.left + li_w > offset.left + container_w)
{
$(this.refs.container).scrollLeft(
left +
(li_offset.left + li_w - offset.left - container_w)
);
} else if ($(li).offset().left < offset.left) {
$(this.refs.container).scrollLeft(
left -
(offset.left - li_offset.left)
);
}
}
}
/**
* This function triggers the list item select event
*
* @private
* @protected
* @param {TagEventType} e tag event object
* @returns
* @memberof ListViewTag
*/
private iselect(e: TagEventType<ListViewItemTag>) {
protected iselect(e: TagEventType<ListViewItemTag>) {
if (!e.data) {
return;
}
@ -990,6 +1281,16 @@ namespace OS {
this.selectedItems.push(e.data);
edata.items = this.selectedItems;
} else {
if(this.selectedItems.length > 0)
{
for(const item of this.selectedItems)
{
if(item != e.data)
{
item.selected = false;
}
}
}
if (this.selectedItem === e.data) {
return;
}
@ -1000,30 +1301,13 @@ namespace OS {
this._selectedItems = [e.data];
edata.items = [e.data];
//scroll element
const li = $(e.data).children()[0];
const offset = $(this.refs.container).offset();
const top = $(this.refs.container).scrollTop();
if (
$(li).offset().top + $(li).height() >
$(this.refs.container).height() + offset.top
) {
$(this.refs.container).scrollTop(
top +
$(this.refs.container).height() -
$(li).height()
);
} else if ($(li).offset().top < offset.top) {
$(this.refs.container).scrollTop(
top -
$(this.refs.container).height() +
$(li).height()
);
}
this.scroll_to_item(e.data);
}
// set the label content event it is hidden
const label = this.refs.drlabel as LabelTag;
label.set(e.data.data);
if (this.dropdown) {
const label = this.refs.drlabel as LabelTag;
label.set(e.data.data);
$(this.refs.mlist).hide();
}
const evt = { id: this.aid, data: edata };
@ -1043,15 +1327,29 @@ namespace OS {
from: undefined,
to: undefined,
};
$(this).on("keyup", (e) => {
this.handle_special_key(e);
});
//$(this.refs.search).on("click",(e) => {
// console.log("focus")
//})
this._onmousedown = (e) => {
if(this.multiselect || this.selectedItems == undefined || this.selectedItems.length == 0)
{
return;
}
let el: any = $(e.target).closest(
"li[dataref='afx-list-item']"
`[list-id='${this.aid}']`
);
if (el.length === 0) {
return;
}
el = el.parent()[0] as ListViewItemTag;
this._dnd.from = el;
el = el[0];
if(!this.selectedItems.includes(el))
{
return;
}
this._dnd.from = this.selectedItems;
this._dnd.to = undefined;
$(window).on("mouseup", this._onmouseup);
$(window).on("mousemove", this._onmousemove);
@ -1062,13 +1360,13 @@ namespace OS {
$(window).off("mousemove", this._onmousemove);
$("#systooltip").hide();
let el: any = $(e.target).closest(
"li[dataref='afx-list-item']"
`[list-id='${this.aid}']`
);
if (el.length === 0) {
return;
}
el = el.parent()[0];
if (el === this._dnd.from) {
el = el[0];
if (this._dnd.from.includes(el)) {
return;
}
this._dnd.to = el;
@ -1086,7 +1384,18 @@ namespace OS {
if (!this._dnd.from) {
return;
}
const data = this._dnd.from.data;
const data = {
text: '',
items: this._dnd.from
};
if(this._dnd.from.length == 1)
{
data.text = this._dnd.from[0].data.text;
}
else
{
data.text = __("{0} selected elements", this._dnd.from.length).__();
}
const $label = $("#systooltip");
const top = e.clientY + 5;
const left = e.clientX + 5;
@ -1097,9 +1406,20 @@ namespace OS {
.css("top", top + "px")
.css("left", left + "px");
};
const label = (this.refs.drlabel as LabelTag);
label.iconclass$ = "bi bi-chevron-down";
label.text = "";
$(this.refs.drlabel).css("display", "inline-block");
$(this.refs.btlist).hide();
this.observable.on("resize", (e) => this.calibrate());
let anchor = $(this).parent();
while (anchor && anchor.css('position') === 'static') {
anchor = anchor.parent();
}
if(anchor && anchor[0])
{
this._anchor = anchor[0];
}
return this.calibrate();
}
@ -1138,16 +1458,31 @@ namespace OS {
if (!this.dropdown) {
return;
}
const desktoph = $(Ant.OS.GUI.workspace).height();
const offset =
$(this).offset().top + $(this.refs.mlist).height();
if (offset > desktoph) {
$(this.refs.mlist).css(
"top",
`-${$(this.refs.mlist).outerHeight()}px`
);
if(! $(this.refs.mlist).is(":hidden"))
{
$(this.refs.mlist).hide();
return;
}
const desktoph = $(Ant.OS.GUI.workspace).outerHeight();
const wheight = $(this).offset().top + $(this.refs.mlist).outerHeight()*1.5;
const position = $(this).position();
let offset = 0;
if(this._anchor)
{
offset = $(this._anchor).scrollTop();
}
if (wheight > desktoph) {
const ypos = offset + position.top - $(this.refs.mlist).outerHeight();
$(this.refs.mlist)
.css("top",`${ypos}px`)
.css("left", `${position.left}px`);
} else {
$(this.refs.mlist).css("top", "100%");
const ypos = offset + $(this).position().top + $(this.refs.container).outerHeight();
$(this.refs.mlist)
.css("top", `${ypos}px`)
.css("left", `${position.left}px`);
}
$(this.refs.mlist).show();
}
@ -1167,6 +1502,40 @@ namespace OS {
}
}
/**
* Scroll the list view to end
*
* @memberof ListViewTag
*/
scroll_to_end()
{
if(this.dir == "column")
{
this.refs.mlist.scrollTo({ top: this.refs.mlist.scrollHeight, behavior: 'smooth' });
}
else
{
this.refs.mlist.scrollTo({ left: this.refs.mlist.scrollWidth, behavior: 'smooth' });
}
}
/**
* Scroll the list view to beginning
*
* @memberof ListViewTag
*/
scroll_to_start()
{
if(this.dir == "column")
{
this.refs.mlist.scrollTo({ top: 0, behavior: 'smooth' });
}
else
{
this.refs.mlist.scrollTo({ left: 0, behavior: 'smooth' });
}
}
/**
* calibrate the list layout
*
@ -1178,9 +1547,12 @@ namespace OS {
if (!this.dropdown) {
return;
}
const w = `${$(this).width()}px`;
$(this.refs.container).css("width", w);
$(this.refs.current).css("width", w);
const w = `${$(this).innerWidth()}px`;
const h = `${$(this).outerHeight()}px`;
$(this.refs.container).css("width", "100%");
$(this.refs.container).css("height", h);
$(this.refs.current).css("width", "100%");
$(this.refs.mlist).css("width", w);
}
@ -1215,6 +1587,7 @@ namespace OS {
define("afx-list-view", ListViewTag);
define("afx-list-item", SimpleListItemTag);
define("afx-dbline-list-item", DoubleLineListItemTag);
}
}
}

View File

@ -1,826 +0,0 @@
namespace OS {
export namespace GUI {
export namespace tag {
/**
* Menu event data interface definition
*/
export type MenuEventData = TagEventDataType<MenuEntryTag>;
/**
* This class defines the abstract prototype of an menu entry.
* Any implementation of menu entry tag should extend this class
*
* @export
* @abstract
* @class MenuEntryTag
* @extends {AFXTag}
*/
export abstract class MenuEntryTag extends AFXTag {
/**
* Data placeholder of the menu entry
*
* @private
* @type {GenericObject<any>}
* @memberof MenuEntryTag
*/
private _data: GenericObject<any>;
/**
* placeholder of `menu entry select` event handle
*
* @private
* @type {TagEventCallback<MenuEventData>}
* @memberof MenuEntryTag
*/
private _onmenuselect: TagEventCallback<MenuEventData>;
/**
* placeholder of `sub-menu entry select event` handle
*
* @private
* @type {TagEventCallback<MenuEventData>}
* @memberof MenuEntryTag
*/
private _onchildselect: TagEventCallback<MenuEventData>;
/**
* Reference to the parent menu entry of current one
*
* @type {MenuEntryTag}
* @memberof MenuEntryTag
*/
parent: MenuEntryTag;
/**
* Reference to the root menu entry
*
* @type {MenuTag}
* @memberof MenuEntryTag
*/
root: MenuTag;
/**
*Creates an instance of MenuEntryTag.
* @memberof MenuEntryTag
*/
constructor() {
super();
this._onmenuselect = this._onchildselect = (
e: TagEventType<MenuEventData>
): void => {};
}
/**
* Init the tag before mounting
*
* @protected
* @memberof MenuEntryTag
*/
protected init(): void {
this.nodes = undefined;
}
/**
* Set the `menu entry select` event handle
*
* @memberof MenuEntryTag
*/
set onmenuselect(v: TagEventCallback<MenuEventData>) {
this._onmenuselect = v;
}
/**
* Setter: Set the `sub menu entry select` event handle
*
* Getter: get the current `sub menu entry select` event handle
*
* @memberof MenuEntryTag
*/
set onchildselect(v: TagEventCallback<MenuEventData>) {
this._onchildselect = v;
}
get onchildselect(): TagEventCallback<MenuEventData> {
return this._onchildselect;
}
/**
* Setter: Set data to the entry
*
* Getter: Get data of the current menu entry
*
* @memberof MenuEntryTag
*/
set data(data: GenericObject<any>) {
this._data = data;
this.set(data);
}
get data(): GenericObject<any> {
return this._data;
}
/**
* Check whether the current menu entry has sub-menu
*
* @protected
* @returns {boolean}
* @memberof MenuEntryTag
*/
protected has_nodes(): boolean {
const ch = this.nodes;
return ch && ch.length > 0;
}
/**
* Check whether the current menu entry is the root entry
*
* @protected
* @returns
* @memberof MenuEntryTag
*/
protected is_root() {
if (this.parent) {
return false;
} else {
return true;
}
}
/**
* Layout definition of the menu entry
* This function define the outer layout of the menu entry.
* Custom inner layout of each item implementation should
* be defined in [[itemlayout]]
* @protected
* @returns {TagLayoutType[]}
* @memberof MenuEntryTag
*/
protected layout(): TagLayoutType[] {
return [
{
el: "li",
ref: "container",
children: [
{
el: "a",
ref: "entry",
children: this.itemlayout(),
},
{ el: "afx-menu", ref: "submenu" },
],
},
];
}
/**
* Setter: Set the sub-menu data
*
* Getter: Get the sub-menu data
*
* @memberof MenuEntryTag
*/
set nodes(v: GenericObject<any>[]) {
$(this.refs.container).removeClass("afx_submenu");
if (!v || !(v.length > 0)) {
$(this.refs.submenu).hide();
return;
}
$(this.refs.container).addClass("afx_submenu");
$(this.refs.submenu).show().attr("style", "");
const element = this.refs.submenu as MenuTag;
element.parent = this;
element.root = this.root;
element.items = v;
// ensure that the data is in sync
this._data.nodes = v;
if (this.is_root()) {
$(this.refs.container).on("mouseleave",(e) => {
return $(this.refs.submenu).attr("style", "");
});
}
}
get nodes(): GenericObject<any>[] {
if (this.data && this.data.nodes) {
return this.data.nodes;
}
return undefined;
}
/**
* Bind some base event to the menu entry
*
* @protected
* @memberof MenuEntryTag
*/
protected mount(): void {
$(this.refs.entry).on("click",(e) => this.select(e));
}
/**
* Hide the sub-menu of the current menu entry
*
* @private
* @returns {void}
* @memberof MenuEntryTag
*/
private submenuoff(): void {
const p = this.parent;
if (!p) {
$(this.refs.submenu).attr("style", "");
return;
}
return p.submenuoff();
}
/**
* This function trigger two event:
* - the `onmenuselect` event on the current entry
* - the `onchildselect` event on the parent of the current entry
*
* @protected
* @param {JQuery.ClickEvent} e
* @memberof MenuEntryTag
*/
protected select(e: JQuery.ClickEvent): void {
const evt = {
id: this.aid,
data: { item: this, event: e },
};
e.preventDefault();
if (this.is_root() && this.has_nodes()) {
$(this.refs.submenu).show();
} else {
this.submenuoff();
}
this._onmenuselect(evt);
if (this.parent) {
this.parent.onchildselect(evt);
}
if (this.root) {
this.root.onmenuitemselect(evt);
}
}
/**
* custom inner layout of a menu entry
*
* @protected
* @abstract
* @returns {TagLayoutType[]}
* @memberof MenuEntryTag
*/
protected abstract itemlayout(): TagLayoutType[];
}
/**
* This class extends the [[MenuEntryTag]] prototype. It inner layout is
* defined with the following elements:
* - a [[SwitchTag]] acts as checker or radio
* - a [[LabelTag]] to display the content of the menu entry
* - a `span` element that display the keyboard shortcut of the entry
*
* @class SimpleMenuEntryTag
* @extends {MenuEntryTag}
*/
export class SimpleMenuEntryTag extends MenuEntryTag {
/**
*Creates an instance of SimpleMenuEntryTag.
* @memberof SimpleMenuEntryTag
*/
constructor() {
super();
}
/**
* Reset some properties to default value
*
* @protected
* @memberof SimpleMenuEntryTag
*/
protected init(): void {
super.init();
this.switch = false;
this.radio = false;
this.checked = false;
}
/**
* Do nothing
*
* @protected
* @memberof SimpleMenuEntryTag
*/
protected calibrate(): void {}
/**
* Do nothing
*
* @protected
* @param {*} [d]
* @memberof SimpleMenuEntryTag
*/
protected reload(d?: any): void {}
/**
* Setter: Turn on/off the checker feature of the menu entry
*
* Getter: Check whether the checker feature is enabled on this menu entry
*
* @memberof SimpleMenuEntryTag
*/
set switch(v: boolean) {
this.attsw(v, "switch");
if (this.radio || v) {
$(this.refs.switch).show();
} else {
$(this.refs.switch).hide();
}
}
get switch(): boolean {
return this.hasattr("switch");
}
/**
* Setter: Turn on/off the radio feature of the menu entry
*
* Getter: Check whether the radio feature is enabled
*
* @memberof SimpleMenuEntryTag
*/
set radio(v: boolean) {
this.attsw(v, "radio");
if (this.switch || v) {
$(this.refs.switch).show();
} else {
$(this.refs.switch).hide();
}
}
get radio(): boolean {
return this.hasattr("radio");
}
/**
* Setter:
*
* Toggle the switch on the menu entry, this setter
* only works when the `checker` or `radio` feature is
* enabled
*
* Getter:
*
* Check whether the switch is turned on
*
* @memberof SimpleMenuEntryTag
*/
set checked(v: boolean) {
this.attsw(v, "checked");
if (this.data) this.data.checked = v;
if (!this.radio && !this.switch) {
return;
}
(this.refs.switch as SwitchTag).swon = v;
}
get checked(): boolean {
return this.hasattr("checked");
}
/**
* Set the label icon using a VFS path
*
* @memberof SimpleMenuEntryTag
*/
set icon(v: string) {
$(this.refs.container).removeClass("fix_padding");
if (!v) {
return;
}
//$(this).attr("icon", v);
const label = this.refs.label as LabelTag;
label.icon = v;
$(this.refs.container).addClass("fix_padding");
}
/**
* Set the label CSS icon class
*
* @memberof SimpleMenuEntryTag
*/
set iconclass(v: string) {
if (!v) {
return;
}
const label = this.refs.label as LabelTag;
label.iconclass = v;
}
/**
* Set the label text
*
* @memberof SimpleMenuEntryTag
*/
set text(v: string) {
if (v === undefined) {
return;
}
const label = this.refs.label as LabelTag;
label.text = v;
}
/**
* Set the keyboard shortcut text
*
* @memberof SimpleMenuEntryTag
*/
set shortcut(v: string) {
$(this.refs.shortcut).hide();
if (!v) {
return;
}
$(this.refs.shortcut).show();
$(this.refs.shortcut).text(v);
}
/**
* Uncheck all sub-menu items of the current menu entry
* that have the radio feature enabled
*
* @returns {void}
* @memberof SimpleMenuEntryTag
*/
protected reset_radio(): void {
if (!this.has_nodes()) {
return;
}
for (let v of this.nodes) {
if (!v.domel.radio) {
continue;
}
v.domel.checked = false;
}
}
/**
* Mount the current tag
*
* @protected
* @memberof SimpleMenuEntryTag
*/
protected mount(): void {
super.mount();
(this.refs.switch as SwitchTag).enable = false;
}
/**
* Trigger the onmenuselect and onchildselect events
*
* @protected
* @param {JQuery.ClickEvent} e Mouse click event
* @returns {void}
* @memberof SimpleMenuEntryTag
*/
protected select(e: JQuery.ClickEvent): void {
if (this.switch) {
this.checked = !this.checked;
} else if (this.radio) {
const p = this.parent as SimpleMenuEntryTag;
if (p) {
p.reset_radio();
}
this.checked = !this.checked;
}
return super.select(e);
}
/**
* Inner item layout of the menu entry
*
* @returns
* @memberof SimpleMenuEntryTag
*/
itemlayout() {
return [
{ el: "afx-switch", ref: "switch" },
{ el: "afx-label", ref: "label" },
{ el: "span", class: "shortcut", ref: "shortcut" },
];
}
}
/**
* A menu tag contains a collection of menu entries in which each
* entry maybe a leaf entry or may contain a submenu
*
* @export
* @class MenuTag
* @extends {AFXTag}
*/
export class MenuTag extends AFXTag {
/**
* Reference to the parent menu entry of the current value.
* This value is `undefined` in case of the current menu is
* the root menu
*
* @type {MenuEntryTag}
* @memberof MenuTag
*/
parent: MenuEntryTag;
/**
* Reference to the root menu
*
* @type {MenuTag}
* @memberof MenuTag
*/
root: MenuTag;
/**
* The `pid` of the application that attached to this menu.
* This value is optional
*
* @type {number}
* @memberof MenuTag
*/
pid?: number;
/**
* placeholder for menu select event handle
*
* @private
* @type {TagEventCallback<MenuEventData>}
* @memberof MenuTag
*/
private _onmenuselect: TagEventCallback<MenuEventData>;
/**
* Menu data placeholder
*
* @private
* @type {GenericObject<any>[]}
* @memberof MenuTag
*/
private _items: GenericObject<any>[];
/**
*Creates an instance of MenuTag.
* @memberof MenuTag
*/
constructor() {
super();
}
/**
* Reset some properties to default value
*
* @protected
* @memberof MenuTag
*/
protected init(): void {
this.contentag = "afx-menu-entry";
this.context = false;
this._items = [];
this._onmenuselect = (
e: TagEventType<MenuEventData>
): void => {};
}
/**
* Do nothing
*
* @protected
* @memberof MenuTag
*/
protected calibrate(): void {}
/**
* Do nothing
*
* @protected
* @param {*} [d]
* @memberof MenuTag
*/
protected reload(d?: any): void {}
/**
* Setter: Set the menu items data
*
* Getter: Get menu items data
*
* @memberof MenuTag
*/
set items(data: GenericObject<any>[]) {
this._items = data;
$(this.refs.container).empty();
data.map((item) => this.push(item, false));
}
get items(): GenericObject<any>[] {
return this._items;
}
/**
* Setter: Set whether the current menu is a context menu
*
* Getter: Check whether the current menu is a context menu
*
* @memberof MenuTag
*/
set context(v: boolean) {
this.attsw(v, "context");
$(this.refs.wrapper).removeClass("context");
if (!v) {
return;
}
$(this.refs.wrapper).addClass("context");
$(this).hide();
}
get context(): boolean {
return this.hasattr("context");
}
/**
* Set menu select event handle
*
* @memberof MenuTag
*/
set onmenuselect(v: TagEventCallback<MenuEventData>) {
this._onmenuselect = v;
}
/**
* Setter:
*
* Set the default tag name of the menu item.
* If the tag is not specified in an item data,
* this value will be used
*
* Getter:
*
* Get the default menu entry tag name
*
* @memberof MenuTag
*/
set contentag(v: string) {
$(this).attr("contentag", v);
}
get contentag(): string {
return $(this).attr("contentag");
}
/**
* Get the reference to the function that triggers
* the menu select event
*
* @readonly
* @type {TagEventCallback}
* @memberof MenuTag
*/
get onmenuitemselect(): TagEventCallback<MenuEventData> {
return this.handleselect;
}
/**
* This function triggers the menu select event
*
* @private
* @param {TagEventType} e
* @memberof MenuTag
*/
private handleselect(e: TagEventType<MenuEventData>): void {
if (this.context) {
$(this).hide();
}
e.id = this.aid;
this._onmenuselect(e);
this.observable.trigger("menuselect", e);
}
/**
* Show the current menu. This function is called
* only if the current menu is a context menu
*
* @param {JQuery.MouseEventBase} e JQuery mouse event
* @returns {void}
* @memberof MenuTag
*/
show(e: JQuery.MouseEventBase): void {
if (!this.context) {
return;
}
$(this)
.css("top", e.clientY - 15 + "px")
.css("left", e.clientX - 5 + "px")
.show();
}
/**
* Test whether the current menu is the root menu
*
* @private
* @returns {boolean}
* @memberof MenuTag
*/
private is_root(): boolean {
return this.root === undefined;
}
/**
* Mount the menu tag and bind some basic events
*
* @protected
* @returns {void}
* @memberof MenuTag
*/
protected mount(): void {
$(this.refs.container).css("display", "contents");
if (!this.context) {
return;
}
$(this.refs.wrapper).on("mouseleave",(e) => {
if (!this.is_root()) {
return;
}
return $(this).hide();
});
}
/**
* Add a menu entry to the beginning of the current
* menu
*
* @param {GenericObject<any>} item menu entry data
* @memberof MenuTag
*/
unshift(item: GenericObject<any>): void {
this.push(item, true);
}
/**
* Delete a menu entry
*
* @param {MenuEntryTag} item reference to the DOM element of an menu entry
* @memberof MenuTag
*/
delete(item: MenuEntryTag): void {
const el = item.data;
const data = this.items;
if (data.includes(el)) {
data.splice(data.indexOf(el), 1);
}
$(item).remove();
}
/**
* Add an menu entry to the beginning or end of the menu
*
* @param {GenericObject<any>} item menu entry data
* @param {boolean} flag indicates whether the entry should be added to the beginning of the menu
* @returns {MenuEntryTag}
* @memberof MenuTag
*/
push(item: GenericObject<any>, flag: boolean): MenuEntryTag {
let tag = this.contentag;
if (item.tag) {
tag = item.tag;
}
const el = $(`<${tag}>`);
if (flag) {
$(this.refs.container).prepend(el[0]);
if (!this.items.includes(item)) {
this.items.unshift(item);
}
} else {
el.appendTo(this.refs.container);
if (!this.items.includes(item)) {
this.items.push(item);
}
}
const entry = el[0] as MenuEntryTag;
entry.uify(this.observable);
entry.parent = this.parent;
entry.root = this.parent ? this.parent.root : this;
entry.data = item;
item.domel = entry;
return entry;
}
/**
* Menu tag layout definition
*
* @returns
* @memberof MenuTag
*/
layout() {
return [
{
el: "ul",
ref: "wrapper",
children: [
{ el: "li", class: "afx-corner-fix" },
{ el: "div", ref: "container" },
{ el: "li", class: "afx-corner-fix" },
],
},
];
}
}
define("afx-menu", MenuTag);
define("afx-menu-entry", SimpleMenuEntryTag);
}
}
}

View File

@ -114,38 +114,6 @@ namespace OS {
* @memberof NSpinnerTag
*/
calibrate(): void {
$(this.refs.holder).css(
"width",
$(this).width() - 20 + "px"
);
$(this.refs.holder).css("height", $(this).height() + "px");
$(this.refs.spinner)
.css("width", "20px")
.css("height", $(this).height() + "px");
$(this.refs.incr)
.css("height", $(this).height() / 2 - 2 + "px")
.css("position", "relative");
$(this.refs.decr)
.css("height", $(this).height() / 2 - 2 + "px")
.css("position", "relative");
$(this.refs.spinner)
.find("li")
.css("display", "block")
.css("text-align", "center")
.css("vertical-align", "middle");
$(this.refs.spinner)
.find("i")
.css("font-size", "16px")
.css("position", "absolute");
const fn = function (ie: HTMLElement, pos: string) {
const el = $(ie).find("i");
el.css(
pos,
($(ie).height() - el.height()) / 2 + "px"
).css("left", ($(ie).width() - el.width()) / 2 + "px");
};
fn(this.refs.decr, "bottom");
fn(this.refs.incr, "top");
}
/**

View File

@ -0,0 +1,189 @@
namespace OS {
export namespace GUI {
export namespace tag {
/**
* Toast notification tag
*
* @export
* @class ToastNotificationTag
* @extends {AFXTag}
*/
export class ToastNotificationTag extends AFXTag {
/**
*Creates an instance of ToastNotificationTag.
* @memberof ToastNotificationTag
*/
constructor() {
super();
}
/**
* Mount the tag
*
* @protected
* @memberof ToastNotificationTag
*/
protected mount() {
$(this.refs.header).on('click',(e) => {
$(this).remove();
})
}
/**
* Init the tag before mounting
*
* @protected
* @memberof ToastNotificationTag
*/
protected init(): void {
};
/**
* Re-calibrate tag
*
* @protected
* @memberof ToastNotificationTag
*/
protected calibrate(): void {}
/**
* Update the current tag, do nothing in this tag
*
* @param {*} [d]
* @memberof ToastNotificationTag
*/
reload(d?: any): void {}
/**
* Tag layout definition
*
* @protected
* @returns {TagLayoutType[]}
* @memberof ToastNotificationTag
*/
protected layout(): TagLayoutType[] {
return [
{
el: "div", id: "toast_container", ref: "container",
children:[
{
el: "div",
ref: "header",
id: "toast_header",
},
{
el: "div",
ref: "yield",
id:"toast_content",
}
]
}
];
}
}
/**
* This tag manage all notification UI on the desktop
*
* @export
* @class NotificationTag
* @extends {AFXTag}
*/
export class NotificationTag extends AFXTag {
/**
*Creates an instance of NotificationTag.
* @memberof NotificationTag
*/
constructor() {
super();
}
/**
* Mount the tag
*
* @protected
* @memberof NotificationTag
*/
protected mount() {
}
/**
* Init the tag before mounting
*
* @protected
* @memberof NotificationTag
*/
protected init(): void {
};
/**
* Push anotification to a specific location
*
* @memberof NotificationTag
*/
push(tag: AFXTag, loc: ANCHOR = ANCHOR.NORTH): void
{
if(!this.refs[loc])
{
return;
}
switch(loc)
{
case ANCHOR.NORTH:
case ANCHOR.NORTH_EST:
case ANCHOR.NORTH_WEST:
$(this.refs[loc]).prepend(tag);
break;
case ANCHOR.SOUTH:
case ANCHOR.SOUTH_EST:
case ANCHOR.SOUTH_WEST:
$(this.refs[loc]).append(tag);
break;
default: break;
}
this.calibrate();
}
/**
* Re-calibrate tag
*
* @protected
* @memberof NotificationTag
*/
protected calibrate(): void {}
/**
* Update the current tag, do nothing in this tag
*
* @param {*} [d]
* @memberof NotificationTag
*/
reload(d?: any): void {}
/**
* Tag layout definition
*
* @protected
* @returns {TagLayoutType[]}
* @memberof NotificationTag
*/
protected layout(): TagLayoutType[] {
return [
{ el: "div", id: "north", ref: "NORTH" },
{ el: "div", id: "south", ref: "SOUTH" },
{ el: "div", id: "north_west", ref: "NORTH_WEST" },
{ el: "div", id: "south_west", ref: "SOUTH_WEST" },
{ el: "div", id: "north_est", ref: "NORTH_EST" },
{ el: "div", id: "south_est", ref: "SOUTH_EST" }
];
}
}
define("afx-notification", NotificationTag);
define("afx-toast-notification", ToastNotificationTag);
}
}
}

View File

@ -3,7 +3,7 @@ namespace OS {
export namespace tag {
/**
* An overlay tag is a layout tag that alway stay on top of
* the virtual desktop environment. Tile layout elements ([[VBoxTag]], [[HboxTag]])
* the virtual desktop environment. Tile layout elements ({@link OS.GUI.tag.VBoxTag}, {@link OS.GUI.tag.HBoxTag})
* can be used inside this tag to compose elements
*
* @export
@ -127,13 +127,16 @@ namespace OS {
* @memberof OverlayTag
*/
calibrate(): void {
$(this).css("width", this.width).css("height", this.height);
$(this)
.css("width", this.width)
.css("height", this.height);
return this.observable.trigger("resize", {
id: this.aid,
data: {
w: this.width,
h: this.height,
},
root: true
});
}

View File

@ -3,7 +3,7 @@ namespace OS {
export namespace tag {
/**
* A `resizer` tag is basically used to dynamically resize an element using mouse.
* It is usually put inside a [[TileLayoutTag]] an can be attached to any element. Example:
* It is usually put inside a {@link TileLayoutTag} an can be attached to any element. Example:
*
* The resizer tag in the following example will be attached to the first `afx-vbox`,
* and allows to resize this element using mouse
@ -41,8 +41,8 @@ namespace OS {
/**
* Reference to the parent tag of the current tag.
* The parent tag should be an instance of a [[TileLayoutTag]]
* such as [[VBoxTag]] or [[HBoxTag]]
* The parent tag should be an instance of a {@link TileLayoutTag}
* such as {@link VBoxTag} or {@link HBoxTag}
*
* @private
* @type {*}
@ -92,8 +92,8 @@ namespace OS {
* Setter:
*
* Set resize direction, two possible values:
* - `hz` - horizontal direction, resize by width
* - `ve` - vertical direction, resize by height
* - `row` - horizontal direction, resize by width
* - `column` - vertical direction, resize by height
*
* Getter:
*
@ -104,19 +104,15 @@ namespace OS {
set dir(v: string) {
let att: string;
$(this).attr("dir", v);
$(this).off("mousedown", null);
if (v === "hz") {
$(this).css("cursor", "col-resize");
$(this).addClass("horizontal");
$(this).off("pointerdown", null);
if (v === "row") {
if (this._resizable_el) {
att = $(this._resizable_el).attr("min-width");
if (att) {
this._minsize = parseInt(att);
}
}
} else if (v === "ve") {
$(this).css("cursor", "row-resize");
$(this).addClass("vertical");
} else if (v === "column") {
if (this._resizable_el) {
att = $(this._resizable_el).attr("min-height");
if (att) {
@ -187,14 +183,31 @@ namespace OS {
? $(this).prev()[0]
: undefined;
}
if (tagname === "AFX-HBOX") {
this.dir = "hz";
} else if (tagname === "AFX-VBOX") {
this.dir = "ve";
} else {
this.dir = "hz";
if(this.dir)
{
return;
}
if (tagname === "AFX-HBOX") {
this.dir = "row";
} else if (tagname === "AFX-VBOX") {
this.dir = "column";
} else {
this.dir = "row";
}
}
/**
* Setter Disable or enable the resize event
*
* @memberof ResizerTag
*/
set disable(v: boolean)
{
this.attsw(v, "disable");
}
get disable(): boolean
{
return this.hasattr("disable");
}
/**
@ -208,24 +221,28 @@ namespace OS {
if (!this.dir || this.dir == "none") {
return;
}
$(this).on("mousedown", (e) => {
$(this).on("pointerdown", (e) => {
e.preventDefault();
$(window).on("mousemove", (evt) => {
if(this.disable)
{
return;
}
$(window).on("pointermove", (evt) => {
if (!this._resizable_el) {
return;
}
if (this.dir === "hz") {
return this.horizontalResize(evt);
} else if (this.dir === "ve") {
return this.verticalResize(evt);
if (this.dir === "row") {
return this.horizontalResize(evt as JQuery.MouseEventBase);
} else if (this.dir === "column") {
return this.verticalResize(evt as JQuery.MouseEventBase);
}
});
return $(window).on("mouseup", function (evt) {
$(window).off("mousemove", null);
$(window).off("mouseup", null);
return $(window).on("pointerup", function (evt) {
$(window).off("pointermove", null);
$(window).off("pointerup", null);
return $(window).off("mouseup", null);
return $(window).off("pointerup", null);
});
});
}

View File

@ -65,6 +65,7 @@ namespace OS {
this.enable = true;
this._max = 100;
this._value = 0;
this.precision = false;
this._onchange = this._onchanging = () => {};
}
@ -98,7 +99,17 @@ namespace OS {
set onvaluechanging(f: TagEventCallback<number>) {
this._onchanging = f;
}
/**
* Setter/Getter: set and get precision reading
*
* @memberof SliderTag
*/
set precision(v: boolean) {
this.attsw(v, "precision");
}
get precision(): boolean {
return this.hasattr("precision");
}
/**
* Setter: Enable/disable the slider
*
@ -110,15 +121,15 @@ namespace OS {
this.attsw(v, "enable");
if (v) {
$(this)
.on("mouseover",() => {
.on("pointerover",() => {
return $(this.refs.point).show();
})
.on("mouseout",() => {
.on("pointerout",() => {
return $(this.refs.point).hide();
});
} else {
$(this.refs.point).hide();
$(this).off("mouseover").off("mouseout");
$(this).off("pointerover").off("pointerout");
}
}
get enable(): boolean {
@ -188,7 +199,16 @@ namespace OS {
*/
calibrate(): void {
if (this.value > this.max) {
this.value = this.max;
this._value = this.max;
}
if(! this.precision)
{
this._value = Math.round(this.value);
$(this.refs.point).text(this.value);
}
else
{
$(this.refs.point).text((Math.round(this.value * 100) / 100).toFixed(2));
}
$(this.refs.container).css("width", $(this).width() + "px");
const w =
@ -197,17 +217,17 @@ namespace OS {
$(this.refs.prg)
.css("width", w + "px")
.css("height", $(this.refs.container).height() + "px");
if (this.enable) {
const ow = w - $(this.refs.point).width() / 2;
const top = Math.floor(
($(this.refs.prg).height() -
$(this.refs.point).height()) /
2
);
$(this.refs.point)
.css("left", ow + "px")
.css("top", top + "px");
}
//if (this.enable) {
const ow = w - ($(this.refs.point).outerWidth() / 2.0);
const top = Math.floor(
($(this.refs.prg).height() +
$(this.refs.point).height()) /
2 + 3
);
$(this.refs.point)
.css("left", ow + "px")
.css("bottom", top + "px");
//}
}
/**
@ -220,10 +240,10 @@ namespace OS {
$(this.refs.point)
.css("user-select", "none")
.css("cursor", "default");
$(this.refs.point).on("mousedown", (e) => {
$(this).on("pointerdown", (e) => {
e.preventDefault();
const offset = $(this.refs.container).offset();
$(window).on("mousemove", (e) => {
$(window).on("pointermove", (e) => {
let left = e.clientX - offset.left;
left = left < 0 ? 0 : left;
const maxw = $(this.refs.container).width();
@ -236,13 +256,13 @@ namespace OS {
});
});
$(window).on("mouseup", (e) => {
$(window).on("pointerup", (e) => {
this._onchange({
id: this.aid,
data: this.value,
});
$(window).off("mousemove", null);
return $(window).off("mouseup", null);
$(window).off("pointermove", null);
return $(window).off("pointerup", null);
});
});
}

View File

@ -0,0 +1,573 @@
namespace OS {
export namespace GUI {
export namespace tag {
/**
* menu event data type definition
*/
export type StackMenuEventData = TagEventDataType<ListViewItemTag>;
/**
* The layout of a simple stack menu item
*
* @export
* @class SimpleStackMenuItemTag
* @extends {ListViewItemTag}
*/
export class SimpleStackMenuItemTag extends ListViewItemTag {
/**
*Creates an instance of SimpleStackMenuItemTag.
* @memberof SimpleStackMenuItemTag
*/
constructor() {
super();
}
/**
* Reset some property to default
*
* @protected
* @memberof SimpleStackMenuItemTag
*/
protected init(): void {
this.closable = false;
this.data = {};
this.switch = false;
this.radio = false;
this.checked = false;
}
/**
* Mount the current tag
*
* @protected
* @memberof SimpleStackMenuItemTag
*/
protected mount(): void {
super.mount();
(this.refs.switch as SwitchTag).enable = false;
}
/**
* Setter: Turn on/off the checker feature of the menu entry
*
* Getter: Check whether the checker feature is enabled on this menu entry
*
* @memberof SimpleStackMenuItemTag
*/
set switch(v: boolean) {
this.attsw(v, "switch");
if (this.radio || v) {
$(this.refs.switch).show();
} else {
$(this.refs.switch).hide();
}
}
get switch(): boolean {
return this.hasattr("switch");
}
/**
* Setter: select/unselect the current item
*
* Getter: Check whether the current item is selected
*
* @memberof SimpleStackMenuItemTag
*/
set selected(v: boolean) {
if(v)
{
if (this.switch) {
this.checked = !this.checked;
} else if (this.radio) {
// reset radio
const p = this.parentElement;
if (p) {
for(let item of Array.from(p.children))
{
const el = item as SimpleStackMenuItemTag;
if(el.radio)
{
el.checked = false;
}
}
}
this.checked = !this.checked;
}
}
super.selected = v;
}
get selected(): boolean {
return this.hasattr("selected");
}
/**
* Setter: Turn on/off the radio feature of the menu entry
*
* Getter: Check whether the radio feature is enabled
*
* @memberof SimpleStackMenuItemTag
*/
set radio(v: boolean) {
this.attsw(v, "radio");
if (this.switch || v) {
$(this.refs.switch).show();
} else {
$(this.refs.switch).hide();
}
}
get radio(): boolean {
return this.hasattr("radio");
}
/**
* Setter:
*
* Toggle the switch on the menu entry, this setter
* only works when the `checker` or `radio` feature is
* enabled
*
* Getter:
*
* Check whether the switch is turned on
*
* @memberof SimpleStackMenuItemTag
*/
set checked(v: boolean) {
this.attsw(v, "checked");
if (this.data) this.data.checked = v;
if (!this.radio && !this.switch) {
return;
}
(this.refs.switch as SwitchTag).swon = v;
}
get checked(): boolean {
return this.hasattr("checked");
}
/**
* Set the keyboard shortcut text
*
* @memberof SimpleStackMenuItemTag
*/
set shortcut(v: string) {
$(this.refs.shortcut).hide();
if (!v) {
return;
}
$(this.refs.shortcut).show();
$(this.refs.shortcut).text(v);
}
/**
* Do nothing
*
* @protected
* @memberof SimpleStackMenuItemTag
*/
protected calibrate(): void {
}
/**
* Refresh the inner label when the item data
* is changed
*
* @protected
* @returns {void}
* @memberof SimpleStackMenuItemTag
*/
protected ondatachange(): void {
const v = this.data;
if (!v) {
return;
}
if(v.nodes && v.nodes.length > 0)
{
$(this.refs.submenu).show();
}
else
{
$(this.refs.submenu).hide();
}
const label = this.refs.label as LabelTag;
this.set(v);
label.set(v);
if (v.selected) {
this.selected = v.selected;
}
}
/**
* Re-render the list item
*
* @protected
* @memberof SimpleStackMenuItemTag
*/
protected reload(): void {
this.data = this.data;
}
/**
* List item custom layout definition
*
* @protected
* @returns {TagLayoutType}
* @memberof SimpleStackMenuItemTag
*/
protected itemlayout(): TagLayoutType {
return {
el:"div",
children: [
{ el: "afx-switch", ref: "switch" },
{ el: "afx-label", ref: "label" },
{ el: "span", class: "shortcut", ref: "shortcut" },
{ el: "span", class: "afx-submenu", ref: "submenu" },
]
};
}
}
/**
* A stack menu is a multilevel menu that
* uses a single list view to navigate all menu levels
* instead of using a traditional cascade style menu
*
* @export
* @class StackMenuTag
* @extends {AFXTag}
*/
export class StackMenuTag extends AFXTag {
/**
* Data stack, the list always displays the
* element on the top of the stack
*
* @type {GenericObject<any>[][]}
* @memberof StackMenuTag
*/
private stack: GenericObject<any>[][];
/**
* Update the current tag, do nothing
*
* @protected
* @param {*} [d]
* @memberof StackMenuTag
*/
protected reload(d?: any): void {}
/**
* Placeholder of tab select event handle
*
* @private
* @type {TagEventCallback<TabEventData>}
* @memberof StackMenuTag
*/
private _onmenuselect: TagEventCallback<StackMenuEventData>;
/**
* Stack menu constructor
*
* @memberof StackMenuTag
*/
constructor() {
super();
}
/**
* Reset to default some property value
*
* @protected
* @memberof StackMenuTag
*/
protected init(): void {
this.stack = [];
this._onmenuselect = (_) => {};
this.context = false;
}
/**
* Recalcutate the menu coordinate in case of
* context menu
*
* @protected
* @memberof StackMenuTag
*/
protected calibrate(): void {
if(this.context)
{
const offset = $(this).position();
let left = offset.left;
let top = offset.top;
const ph = $(this).parent().height();
const pw = $(this).parent().width();
const dy = top + $(this).height() - ph;
const dx = left + $(this).width() - pw;
if(dx < 0 && dy < 0)
{
return;
}
top -= dy > 0?dy:0;
left -= dx > 0?dx:0;
$(this)
.css("top", top + "px")
.css("left", left + "px");
}
}
/**
* Reset the menu to its initial state
*
* @memberof StackMenuTag
*/
reset(): void {
const btn = this.refs.title as ButtonTag;
const list = this.refs.list as ListViewTag;
list.selected = -1;
btn.data = undefined;
if(this.stack.length > 0)
{
let arr = this.stack[0];
this.stack = [];
list.data = arr[1] as any;
$(btn).hide();
}
}
/**
* Mount the menu and bind some basic events
*
* @protected
* @memberof StackMenuTag
*/
protected mount(): void {
const btn = this.refs.title as ButtonTag;
const list = this.refs.list as ListViewTag;
list.itemtag = "afx-stack-menu-item";
btn.onbtclick = (_) => {
let arr = this.stack.pop();
if(this.stack.length == 0)
{
$(btn).hide();
btn.data = undefined;
}
else
{
btn.data = arr[0];
btn.iconclass = "bi bi-backspace";
}
list.data = arr[1] as any;
};
list.onlistselect = (e) => {
let data = e.data.item.data;
e.id = this.aid;
if(btn.data && btn.data.onchildselect)
{
btn.data.onchildselect(e);
}
if(data.onmenuselect)
{
data.onmenuselect(e);
}
this._onmenuselect(e);
this.observable.trigger("menuselect", e);
if(data.nodes && data.nodes.length > 0)
{
this.stack.push([btn.data, list.data]);
btn.data = data;
btn.iconclass = "bi bi-backspace";
$(btn).show();
list.selected = -1;
list.data = data.nodes;
if(this.context)
{
this.calibrate();
}
} else if (this.context) {
$(this).hide();
}
};
}
/**
* Setter: set current selected item index
*
* Getter: Get current selected item index
*
* @memberof StackMenuTag
*/
set selected(i: number | number[])
{
const list = this.refs.list as ListViewTag;
list.selected = i;
}
get selected(): number | number[]
{
const list = this.refs.list as ListViewTag;
return list.selected;
}
/**
* Setter: Set whether the current menu is a context menu
*
* Getter: Check whether the current menu is a context menu
*
* @memberof StackMenuTag
*/
set context(v: boolean) {
this.attsw(v, "context");
$(this).removeClass("context");
if (!v) {
return;
}
$(this).addClass("context");
$(this).hide();
}
get context(): boolean {
return this.hasattr("context");
}
/**
* Get the latest selected item
*
* @readonly
* @type {ListViewItemTag}
* @memberof StackMenuTag
*/
get selectedItem(): ListViewItemTag {
const list = this.refs.list as ListViewTag;
return list.selectedItem;
}
/**
* Get all the selected items
*
* @readonly
* @type {ListViewItemTag[]}
* @memberof StackMenuTag
*/
get selectedItems(): ListViewItemTag[] {
const list = this.refs.list as ListViewTag;
return list.selectedItems;
}
/**
* The following setter/getter are keep for backward compatible
* with the MenuTag interface
*
* Setter: Set the menu data
*
* Getter: Get the menu data
*
* @deprecated
* @memberof StackMenuTag
*/
set items(v: GenericObject<any>[]) {
this.nodes = v;
}
get items(): GenericObject<any>[] {
return this.nodes;
}
/**
* Setter: Set the menu data
*
* Getter: Get the menu data
*
* @memberof StackMenuTag
*/
set nodes(v: GenericObject<any>[]) {
this.stack = [];
this.reset();
(this.refs.list as ListViewTag).data = v;
$(this.refs.title).hide();
}
get nodes(): GenericObject<any>[] {
return (this.refs.list as ListViewTag).data;
}
/**
* Set the `menu entry select` event handle
*
* @memberof StackMenuTag
*/
set onmenuselect(v: TagEventCallback<StackMenuEventData>) {
this._onmenuselect = v;
}
/**
* Hide the current menu. This function is called
* only if the current menu is context menu
*
* @memberof StackMenuTag
*/
hide(): void
{
if (!this.context) {
return;
}
$(this)
.css("bottom", "unset")
.css("top", "unset")
.css("left", "unset")
.css("right", "unset")
.hide();
}
/**
* Show the current menu. This function is called
* only if the current menu is a context menu
*
* @param {JQuery.MouseEventBase} e JQuery mouse event
* @returns {void}
* @memberof StackMenuTag
*/
show(e?: JQuery.MouseEventBase): void {
const list = this.refs.list as ListViewTag;
const btn = this.refs.title as ButtonTag;
if (!this.context) {
return;
}
if(e)
{
const offset = $(this).parent().offset();
let top = e.clientY - offset.top - 15;
let left = e.clientX - offset.left - 5;
$(this)
.css("top", top + "px")
.css("left", left + "px")
.css("bottom", "unset")
.css("right", "unset");
}
const dropoff = (e) => {
if($(e.target).closest(`[list-id="${list.aid}"]`).length > 0)
{
return;
}
if($(e.target).closest(btn).length > 0)
{
return;
}
this.hide();
$(document).off("click", dropoff);
};
$(document).on("click", dropoff);
$(this).show();
this.calibrate();
}
/**
* Tag layout definition
*
* @protected
* @returns {TagLayoutType[]}
* @memberof StackMenuTag
*/
protected layout(): TagLayoutType[] {
return [
{
el: "afx-button",
ref: "title"
},
{
el: "afx-list-view",
ref: "list",
}
];
}
}
define("afx-stack-menu", StackMenuTag);
define("afx-stack-menu-item", SimpleStackMenuItemTag);
}
}
}

View File

@ -0,0 +1,76 @@
namespace OS {
export namespace GUI {
export namespace tag {
/**
* A stack pannel allows to navigate back and forth between pannels
* (container widget). Each container widget in the stack should be
* composed inside a {@link HBoxTag}
*
*
* @export
* @class StackPanelTag
* @extends {AFXTag}
*/
export class StackPanelTag extends TabContainerTag {
private _current_pannel_index: number;
/**
* Mount the tag and bind basic events
*
* @protected
* @memberof StackPanelTag
*/
protected mount(): void {
this._current_pannel_index = -1;
super.mount();
this.observable.one("mounted", (id) => {
this.tabbarheight = 0;
$(this.refs.bar).hide();
this.navigateNext();
});
}
/**
* Set the tab select event handle
*
* @memberof StackPanelTag
*/
set ontabselect(f: TagEventCallback<TabContainerTabType>) {
}
/**
* Navigate to the next panel
*
* @memberof StackPanelTag
*/
navigateNext(): void
{
if(this._current_pannel_index >= this.tabs.length)
return;
this._current_pannel_index++;
this.navigate();
}
/**
* Navigate back to the previous panel
*
* @memberof StackPanelTag
*/
navigateBack(): void
{
if(this._current_pannel_index <= 0)
return;
this._current_pannel_index--;
this.navigate()
}
/**
* Navigate to a custom panel
*
* @memberof StackPanelTag
*/
private navigate()
{
this.selectedIndex = this._current_pannel_index;
}
}
define("afx-stack-panel", StackPanelTag);
}
}
}

View File

@ -34,10 +34,19 @@ namespace OS {
* Store pending loading task
*
* @private
* @type {Promise<any>[]}
* @memberof SystemPanelTag
*/
private _pending_task: Promise<any>[];
/**
* Store the current attached service
*
* @private
* @type {number[]}
* @memberof SystemPanelTag
*/
private _pending_task: number[];
private _services: application.BaseService[];
/**
* Loading animation check timeout
@ -70,13 +79,15 @@ namespace OS {
constructor() {
super();
this._osmenu = {
text: __("Start"),
iconclass: "fa fa-circle",
text: "",
//iconclass: "fa fa-circle",
icon: "os://resources/themes/system/icons/antos-32x32.png"
};
this._view = false;
this._pending_task = [];
this._loading_toh = undefined;
this.app_list= [];
this._services = [];
}
/**
@ -97,7 +108,7 @@ namespace OS {
protected reload(d?: any): void { }
/**
* Attach a service to the system tray on the pannel,
* Attach a service to the system tray on the panel,
* this operation is performed when a service is started
*
* @param {BaseService} s
@ -105,8 +116,7 @@ namespace OS {
* @memberof SystemPanelTag
*/
attachservice(s: application.BaseService) {
(this.refs.systray as MenuTag).unshift(s);
return s.attach(this.refs.systray);
this._services.unshift(s);
}
/**
@ -123,9 +133,6 @@ namespace OS {
if (!el) {
return;
}
if (!el.data || el.data.dataid === "header") {
return;
}
this.toggle(false);
// launch the app or open the file
Ant.OS.GUI.openWith(el.data as AppArgumentsType);
@ -142,25 +149,25 @@ namespace OS {
*/
private search(e: JQuery.KeyboardEventBase): void {
const applist = this.refs.applist as ListViewTag;
const catlist = this.refs.catlist as ListViewTag;
const catlist = this.refs.catlist as TabBarTag;
switch (e.which) {
case 27:
// escape key
return this.toggle(false);
case 37:
applist.nav_prev();
return e.preventDefault();
case 38:
applist.selectPrev();
return e.preventDefault();
case 39:
applist.nav_next();
return e.preventDefault();
case 40:
applist.selectNext();
return e.preventDefault();
case 13:
e.preventDefault();
return this.open();
return applist.nav_commit();
default:
catlist.selected = 0;
var text = (this.refs.search as HTMLInputElement)
@ -187,9 +194,8 @@ namespace OS {
* @memberof SystemPanelTag
*/
detachservice(s: application.BaseService): void {
(this.refs.systray as MenuTag).delete(
s.domel as MenuEntryTag
);
const index = this._services.indexOf(s);
this._services.splice(index, 1);
}
/**
@ -206,23 +212,17 @@ namespace OS {
ref: "panel",
children: [
{
el: "afx-menu",
el: "afx-button",
ref: "osmenu",
class: "afx-panel-os-menu",
},
{
el: "afx-menu",
ref: "pinned",
class: "afx-panel-os-pinned-app",
el: "afx-apps-dock",
ref: "sysdock",
id: "sysdock"
},
{
el: "afx-menu",
id: "appmenu",
ref: "appmenu",
class: "afx-panel-os-app",
},
{
el: "afx-menu",
el: "afx-button",
id: "systray",
ref: "systray",
class: "afx-panel-os-stray",
@ -247,17 +247,13 @@ namespace OS {
],
},
{
el: "afx-hbox",
el: "afx-vbox",
children: [
{
el: "afx-list-view",
el: "afx-tab-bar",
id: "catlist",
ref: "catlist",
width:"40%"
},
{
el: "afx-resizer",
width: 3,
height:45
},
{
el: "afx-list-view",
@ -269,7 +265,7 @@ namespace OS {
{
el: "afx-hbox",
id: "btlist",
height: 30,
height: 40,
children: [
{
el: "afx-button",
@ -293,6 +289,7 @@ namespace OS {
},
],
},
];
}
@ -304,7 +301,7 @@ namespace OS {
* @memberof SystemPanelTag
*/
private refreshAppList(): void {
let catlist_el = (this.refs.catlist as tag.ListViewTag);
let catlist_el = (this.refs.catlist as tag.TabBarTag);
let k: string, v: API.PackageMetaType;
const catlist = new Set();
this.app_list = [];
@ -349,7 +346,7 @@ namespace OS {
iconclass: "bi bi-gear-wide"
});
});
catlist_el.data = cat_list_data;
catlist_el.items = cat_list_data;
catlist_el.selected = 0;
}
@ -369,7 +366,11 @@ namespace OS {
this.calibrate();
$(document).on("click", this._cb);
(this.refs.search as HTMLInputElement).value = "";
$(this.refs.search).trigger("focus");
if(!OS.mobile)
{
$(this.refs.search).trigger("focus");
}
} else {
$(this.refs.overlay).hide();
$(document).off("click", this._cb);
@ -386,34 +387,6 @@ namespace OS {
}px`;
}
/**
* Refresh the pinned applications menu
*
* @private
* @memberof SystemPanelTag
*/
private RefreshPinnedApp(): void
{
if(!setting.system.startup.pinned)
return;
(this.refs.pinned as GUI.tag.MenuTag).items =
setting.system.startup.pinned
.filter((el) =>{
const app = setting.system.packages[el];
return app && app.app
})
.map((name) => {
const app = setting.system.packages[name];
return {
icon: app.icon,
iconclass: app.iconclass,
app: app.app,
tooltip: `cb:${app.name}`
};
});
}
/**
* Check if the loading tasks ended,
* if it the case, stop the animation
@ -431,6 +404,18 @@ namespace OS {
clearTimeout(this._loading_toh);
this._loading_toh = undefined;
}
private show_systray(): void
{
const ctxmenu = $("#contextmenu")[0] as tag.StackMenuTag;
ctxmenu.hide();
ctxmenu.nodes = this._services;
$(ctxmenu)
.css("right", 0)
.css("bottom", $(this).height());
ctxmenu.show();
}
/**
* Mount the tag bind some basic event
*
@ -438,19 +423,16 @@ namespace OS {
* @memberof SystemPanelTag
*/
protected mount(): void {
(this.refs.osmenu as MenuTag).items = [this._osmenu];
const systray = this.refs.systray as GUI.tag.ButtonTag;
(this.refs.osmenu as ButtonTag).set(this._osmenu);
this._cb = (e) => {
if (
!$(e.target).closest($(this.refs.overlay)).length &&
!$(e.target).closest(this.refs.osmenu).length
) {
return this.toggle(false);
} else {
return $(this.refs.search).trigger("focus");
}
};
$(this.refs.appmenu).css("z-index", 1000000);
$(this.refs.systray).css("z-index", 1000000);
(this.refs.btscreen as ButtonTag).set({
iconclass: "fa fa-tv",
onbtclick: (e) => {
@ -475,17 +457,24 @@ namespace OS {
return Ant.OS.exit();
},
});
(this.refs.osmenu as MenuTag).onmenuselect = (e) => {
return this.toggle(true);
(this.refs.osmenu as ButtonTag).onbtclick = (e) => {
if($(this.refs.overlay).is(":hidden"))
{
this.toggle(true);
}
else
{
this.toggle(false);
}
};
$(this.refs.search).on("keyup", (e) => {
return this.search(e);
});
$(this.refs.applist).on("click", (e) => {
(this.refs.applist as ListViewTag).onlistselect = (_) => {
return this.open();
});
};
Ant.OS.GUI.bindKey("CTRL- ", (e) => {
if (this._view === false) {
return this.toggle(true);
@ -493,8 +482,13 @@ namespace OS {
return this.toggle(false);
}
});
const catlist = (this.refs.catlist as tag.ListViewTag);
catlist.onlistselect = (e) => {
Ant.OS.GUI.bindKey("ESC", (e) => {
if (this._view === true) {
return this.toggle(false);
}
});
const catlist = (this.refs.catlist as tag.TabBarTag);
catlist.ontabselect = (e) => {
const applist = (this.refs.applist as ListViewTag);
if(catlist.selected === 0)
{
@ -510,55 +504,57 @@ namespace OS {
applist.selected = -1;
};
$(this.refs.overlay)
.css("left", 0)
.css("top", `${$(this.refs.panel).height()}px`)
.css("bottom", "0")
.hide();
(this.refs.pinned as GUI.tag.MenuTag).onmenuselect = (e) => {
const app = e.data.item.data.app;
if(!app)
return;
GUI.launch(app, []);
};
this.refs.appmenu.contextmenuHandle = (e, m) => { }
this.refs.osmenu.contextmenuHandle = (e, m) => { }
this.refs.systray.contextmenuHandle = (e, m) => { }
this.refs.pinned.contextmenuHandle = (e, m) => { }
this.refs.panel.contextmenuHandle = (e, m) => {
let menu = [
{ text: __("Applications and services setting"), dataid: "app&srv" }
];
m.items = menu;
m.onmenuselect = function (
evt: TagEventType<tag.MenuEventData>
) {
GUI.launch("Setting",[]);
}
m.show(e);
};
announcer.observable.on("app-pinned", (d) => {
this.RefreshPinnedApp();
});
announcer.observable.on("loading", (o) => {
this._pending_task.push(o.id);
if(!$(this.refs.panel).hasClass("loading"))
this.refs.osmenu.contextmenuHandle = (e, m) => { };
systray.contextmenuHandle = (e, m) => { };
this.refs.panel.contextmenuHandle = (e, m) => { };
announcer.on("ANTOS-TASK-PENDING", (o: API.AnnouncementDataType<Promise<any>>) => {
if(this._pending_task.length == 0)
{
$(this.refs.panel).addClass("loading");
systray.iconclass = "fa-spin fa fa-cog";
}
this._pending_task.push(o.u_data);
$(GUI.workspace).css("cursor", "wait");
});
announcer.observable.on("loaded", (o) => {
const i = this._pending_task.indexOf(o.id);
systray.iconclass = "bi bi-sliders";
systray.onbtclick = (e) => {
e.data.stopPropagation();
this.show_systray();
};
const remove_task = (o: Promise<any>) => {
const i = this._pending_task.indexOf(o);
if (i >= 0) {
this._pending_task.splice(i, 1);
}
if (this._pending_task.length === 0) {
// set time out
systray.iconclass = "bi bi-sliders";
if(!this._loading_toh)
this._loading_toh = setTimeout(() => this.animation_check(),1000);
}
};
announcer.on("ANTOS-TASK-FULFILLED", (o: API.AnnouncementDataType<Promise<any>>) => {
remove_task(o.u_data);
});
this.RefreshPinnedApp();
Ant.OS.announcer.trigger("syspanelloaded", undefined);
announcer.on("ANTOS-TASK-REJECTED", (o: API.AnnouncementDataType<Promise<any>>) => {
remove_task(o.u_data);
});
announcer.on("DESKTOP-RESIZE", (e) => {
this.calibrate();
});
announcer.on("APP-SELECT", (e) => {
if(this._view)
this.toggle(false);
});
announcer.on("APP-LOADING", (e) => {
if(this._view)
this.toggle(false);
});
Ant.OS.announcer.trigger("SYS-PANEL-LOADED", undefined);
}
}

View File

@ -39,6 +39,13 @@ namespace OS {
*/
private _ontabselect: TagEventCallback<TabEventData>;
/**
* Cache of touch event
*
* @private
* @meberof TabBarTag
*/
private _previous_touch: {x: number, y: number};
/**
*Creates an instance of TabBarTag.
* @memberof TabBarTag
@ -57,6 +64,8 @@ namespace OS {
*/
protected init(): void {
this.selected = -1;
this.dir = "row";
this._previous_touch = {x: 0, y:0};
}
/**
@ -82,6 +91,30 @@ namespace OS {
return this.hasattr("closable");
}
/**
* Setter:
*
* Set the tab bar direction:
* - `row`: horizontal direction
* - `column`: vertical direction
*
* Getter:
*
* Get the tab bar direction
*
* @memberof TabBarTag
*/
set dir(v: string) {
$(this).attr("dir", v);
if (!v) {
return;
}
(this.refs.list as ListViewTag).dir = v;
}
get dir(): string {
return $(this).attr("dir") as any;
}
/**
* Add a tab in the end of the tab bar
*
@ -144,6 +177,16 @@ namespace OS {
get selected(): number | number[] {
return (this.refs.list as ListViewTag).selected;
}
/**
* Get the latest selected item
*
* @readonly
* @type {ListViewItemTag}
* @memberof TabBarTag
*/
get selectedItem(): ListViewItemTag {
return (this.refs.list as ListViewTag).selectedItem;
}
/**
* Set the tab close event handle
@ -179,6 +222,75 @@ namespace OS {
this._ontabselect(e);
return this.observable.trigger("tabselect", e);
};
const list_container = $(".list-container", this.refs.list);
list_container.each((i,el) => {
el.addEventListener("touchstart", e => {
this._previous_touch.x = e.touches[0].pageX;
this._previous_touch.y = e.touches[0].pageY;
}, {passive: true});
el.addEventListener("touchmove", e => {
const offset = {x:0, y:0};
offset.x = this._previous_touch.x - e.touches[0].pageX ;
offset.y = this._previous_touch.y - e.touches[0].pageY;
if(this.dir == "row")
{
el.scrollLeft += offset.x;
}
else
{
el.scrollTop += offset.y;
}
this._previous_touch.x = e.touches[0].pageX;
this._previous_touch.y = e.touches[0].pageY;
}, {passive: true});
el.addEventListener("wheel", (evt)=>{
if(this.dir == "row")
{
el.scrollLeft += (evt as WheelEvent).deltaY;
}
else
{
el.scrollTop += (evt as WheelEvent).deltaY;
}
}, {passive: true});
});
}
/**
* Scroll the tabbar to end
*
* @memberof TabBarTag
*/
scroll_to_end()
{
const list_container = $(".list-container", this.refs.list)[0];
if(this.dir == "column")
{
list_container.scrollTo({ top: list_container.scrollHeight, behavior: 'smooth' });
}
else
{
list_container.scrollTo({ left: list_container.scrollWidth, behavior: 'smooth' });
}
}
/**
* Scroll the tabbar to begin
*
* @memberof TabBarTag
*/
scroll_to_start()
{
const list_container = $(".list-container", this.refs.list)[0];
if(this.dir == "column")
{
list_container.scrollTo({ top: 0, behavior: 'smooth' });
}
else
{
list_container.scrollTo({ left: 0, behavior: 'smooth' });
}
}
/**

View File

@ -19,9 +19,9 @@ namespace OS {
}
export namespace tag {
/**
* A tab container allows to attach each tab on a [[TabBarTag]]
* A tab container allows to attach each tab on a {@link TabBarTag}
* with a container widget. The attached container widget should be
* composed inside a [[HBoxTag]]
* composed inside a {@link HBoxTag}
*
* The tab bar in a tab container can be configured to display tabs
* in horizontal (row) or vertical (column) order. Default to vertical order
@ -45,11 +45,11 @@ namespace OS {
/**
* Placeholder of the tab select event handle
*
* @private
* @protected
* @type {TagEventCallback<TabContainerTabType>}
* @memberof TabContainerTag
*/
private _ontabselect: TagEventCallback<TabContainerTabType>;
protected _ontabselect: TagEventCallback<TabContainerTabType>;
/**
*Creates an instance of TabContainerTag.
@ -102,13 +102,17 @@ namespace OS {
}
/**
* Select a tab by its index
*
* Setter: Select a tab by its index
* Getter: Get the current selected index
*
* @memberof TabContainerTag
*/
set selectedIndex(i: number) {
(this.refs.bar as TabBarTag).selected = i;
}
get selectedIndex(): number {
return (this.refs.bar as TabBarTag).selected as number;
}
/**
* Setter:
@ -129,6 +133,14 @@ namespace OS {
return;
}
(this.refs.wrapper as TileLayoutTag).dir = v;
if(v == "row")
{
(this.refs.bar as TabBarTag).dir = "column";
}
else
{
(this.refs.bar as TabBarTag).dir = "row";
}
}
get dir(): "row" | "column" {
return $(this).attr("dir") as any;
@ -157,7 +169,7 @@ namespace OS {
}
$(v.container).show();
this.observable.trigger("resize", undefined);
$(v.container).attr("tabindex",-1).css("outline", "none").trigger("focus");
$(v.container).trigger("focus");
}
get selectedTab(): TabContainerTabType {
return this._selectedTab;
@ -175,7 +187,7 @@ namespace OS {
return;
}
$(this.refs.bar).attr("data-width", `${v}`);
(this.refs.wrapper as TileLayoutTag).calibrate();
this.observable.trigger("resize", undefined);
}
/**
@ -185,8 +197,11 @@ namespace OS {
* @memberof TabContainerTag
*/
set tabbarheight(v: number) {
if (!v) {
return;
}
$(this.refs.bar).attr("data-height", `${v}`);
(this.refs.wrapper as TileLayoutTag).calibrate();
this.observable.trigger("resize", undefined);
}
/**
@ -215,6 +230,7 @@ namespace OS {
$(item.container)
.css("width", "100%")
.css("height", "100%")
.attr("tabindex",-1).css("outline", "none") // allow focus this tab
.hide();
const el = (this.refs.bar as TabBarTag).push(
item

View File

@ -1,6 +1,9 @@
namespace OS {
export namespace GUI {
export namespace tag {
type TileItemDirection = "row" | "column" | "row-reverse" | "column-reverse";
/**
* A tile layout organize it child elements
* in a fixed horizontal or vertical direction.
@ -22,6 +25,8 @@ namespace OS {
constructor() {
super();
}
private _padding: number;
/**
* Do nothing
@ -29,7 +34,9 @@ namespace OS {
* @protected
* @memberof TileLayoutTag
*/
protected init(): void {}
protected init(): void {
this.padding = 0;
}
/**
* Do nothing
*
@ -73,17 +80,66 @@ namespace OS {
*
* @memberof TileLayoutTag
*/
set dir(v: "row" | "column") {
set dir(v: TileItemDirection) {
if (!v) {
return;
}
$(this).attr("dir", v);
$(this.refs.yield).css("flex-direction", v);
this.reversed = this.reversed;
this.calibrate();
}
get dir(): "row" | "column" {
get dir(): TileItemDirection {
return $(this).attr("dir") as any;
}
/**
* Setter:
*
* SET content padding
*
* Getter:
*
* Get content padding
*
* @memberof TileLayoutTag
*/
set padding(v: number)
{
$(this).attr("padding", v);
this._padding = v;
}
get padding(): number
{
return this._padding;
}
/**
* setter: Reverse order of the content in the tile
*
* getter: return if the tile's content is in reversed order
*
* @meberof TileLayoutTags
*/
set reversed(v: boolean)
{
this.attsw(v, "reversed");
if(!this.dir)
{
return;
}
let newdir = "row";
if(this.dir.startsWith("column"))
{
newdir = "column"
}
if(v)
{
newdir += "-reverse";
}
$(this.refs.yield).css("flex-direction", newdir);
}
get reversed(): boolean
{
return this.hasattr("reversed");
}
/**
* Mount the element
@ -95,9 +151,7 @@ namespace OS {
protected mount(): void {
$(this).css("display", "block");
$(this.refs.yield)
.css("display", "flex")
.css("width", "100%")
.css("height", "100%");
.css("display", "flex");
this.observable.on("resize", (e) => this.calibrate());
return this.calibrate();
}
@ -109,6 +163,10 @@ namespace OS {
* @memberof TileLayoutTag
*/
calibrate(): void {
$(this.refs.yield)
.css("padding", this.padding)
.css("width", `${$(this).width() - this.padding*2}px`)
.css("height", `${$(this).height() - this.padding*2}px`);
if (this.dir === "row") {
return this.hcalibrate();
}
@ -128,30 +186,34 @@ namespace OS {
private hcalibrate(): void {
const auto_width = [];
let ocwidth = 0;
const avaiheight = $(this).height();
const avaiWidth = $(this).width();
//$(this.refs.yield).css("height", `${avaiheight}px`);
const avaiWidth = $(this).width() - this.padding * 2;
const avaiheight = $(this).innerHeight() - this.padding * 2;
$(this.refs.yield)
.children()
.each(function (e) {
$(this).css("height", "100%");
let attv = $(this).attr("data-width");
let dw = 0;
if (attv && attv !== "grow") {
if (attv[attv.length - 1] === "%") {
dw =
(parseInt(attv.slice(0, -1)) *
avaiWidth) /
100;
} else {
dw = parseInt(attv);
}
$(this).css("width", `${dw}px`);
ocwidth += dw;
} else {
if (!attv || attv == "grow") {
$(this).css("flex-grow", "1");
auto_width.push(this);
return;
}
if(attv == "content")
{
ocwidth += $(this).width();
return;
}
if (attv[attv.length - 1] === "%") {
dw =
(parseInt(attv.slice(0, -1)) *
avaiWidth) /
100;
} else {
dw = parseInt(attv);
}
$(this).css("width", `${dw}px`);
ocwidth += dw;
});
const csize = (avaiWidth - ocwidth) / auto_width.length;
@ -160,7 +222,7 @@ namespace OS {
$(v).css("width", `${csize}px`)
);
}
return this.observable.trigger("hboxchange", {
this.observable.trigger("hboxchange", {
id: this.aid,
data: { w: avaiWidth, h: avaiheight },
});
@ -177,30 +239,34 @@ namespace OS {
private vcalibrate(): void {
const auto_height = [];
let ocheight = 0;
const avaiheight = $(this).height();
const avaiwidth = $(this).width();
//$(this.refs.yield).css("height", `${avaiheight}px`);
const avaiheight = $(this).innerHeight() - this.padding * 2;
const avaiwidth = $(this).width() - this.padding * 2;
$(this.refs.yield)
.children()
.each(function (e) {
let dh = 0;
$(this).css("width", "100%");
let attv = $(this).attr("data-height");
if (attv && attv !== "grow") {
if (attv[attv.length - 1] === "%") {
dh =
(parseInt(attv.slice(0, -1)) *
avaiheight) /
100;
} else {
dh = parseInt(attv);
}
$(this).css("height", `${dh}px`);
ocheight += dh;
} else {
if (!attv || attv == "grow") {
$(this).css("flex-grow", "1");
auto_height.push(this);
return;
}
if(attv == "content")
{
ocheight += $(this).height();
return;
}
if (attv[attv.length - 1] === "%") {
dh =
(parseInt(attv.slice(0, -1)) *
avaiheight) /
100;
} else {
dh = parseInt(attv);
}
$(this).css("height", `${dh}px`);
ocheight += dh;
});
const csize = (avaiheight - ocheight) / auto_height.length;
@ -210,7 +276,7 @@ namespace OS {
);
}
return this.observable.trigger("vboxchange", {
this.observable.trigger("vboxchange", {
id: this.aid,
data: { w: avaiwidth, h: avaiheight },
});

View File

@ -111,7 +111,7 @@ namespace OS {
* Placeholder for the `fetch` function of the node.
* This function is used to fetch the child nodes of the
* current nodes. This function should return a promise on
* a list of [[TreeViewDataType]]
* a list of {@link TreeViewDataType}
*
* @memberof TreeViewItemPrototype
*/
@ -164,7 +164,7 @@ namespace OS {
* Setter:
*
* Set the data of the current node. This will trigger the
* [[ondatachange]] function
* {@link ondatachange} function
*
* Getter:
*
@ -182,7 +182,7 @@ namespace OS {
this.treepath = v.path;
}
this.selected = v.selected;
v.domel = this;
this.attach(v);
this.ondatachange();
}
get data(): TreeViewDataType {
@ -294,7 +294,8 @@ namespace OS {
.css("padding", 0)
.css("margin", 0)
.css("background-color", "transparent")
.css("width", v * 15 + "px");
.css("width", v * 15 + "px")
.css("flex-shrink", 0);
}
/**
@ -375,19 +376,23 @@ namespace OS {
$(this.refs.container)
.css("padding", 0)
.css("margin", 0)
.css("display","flex")
.css("flex-direction", "row")
.css("align-items", "center")
.css("white-space", "nowrap");
$(this.refs.itemholder).css("display", "inline-block");
//$(this.refs.itemholder).css("display", "inline-block");
$(this.refs.wrapper).on("click",(e) => {
this.selected = true;
});
$(this.refs.wrapper).on("dblclick", (e) => {
$(this.refs.wrapper).on(OS.mobile?"dbltap":"dblclick", (e) => {
this._evt.data.dblclick = true;
this.selected = true;
});
$(this.refs.toggle)
.css("display", "inline-block")
//.css("display", "inline-block")
.css("width", "15px")
.css("flex-shrink", 0)
.addClass("afx-tree-view-item")
.on("click",(e) => {
this.open = !this.open;
@ -400,7 +405,7 @@ namespace OS {
* Layout definition of a node. This function
* returns the definition of the base outer layout
* of a node. Custom inner layout of the node should
* be defined in the [[itemlayout]] function
* be defined in the {@link itemlayout} function
*
* @protected
* @returns {TagLayoutType[]}
@ -459,8 +464,8 @@ namespace OS {
}
/**
* SimpleTreeViewItem extends [[TreeViewItemPrototype]] and
* define it inner layout using a [[LabelTag]]
* SimpleTreeViewItem extends {@link TreeViewItemPrototype} and
* define it inner layout using a {@link LabelTag}
*
* @export
* @class SimpleTreeViewItem
@ -586,10 +591,10 @@ namespace OS {
* Private data object passing between dragndrop mouse event
*
* @private
* @type {{ from: TreeViewTag; to: TreeViewTag }}
* @type {{ from: TreeViewTag[]; to: TreeViewTag }}
* @memberof TreeViewTag
*/
private _dnd: { from: TreeViewTag; to: TreeViewTag };
private _dnd: { from: TreeViewTag[]; to: TreeViewTag };
/**
* Reference to parent tree of the current tree.
@ -636,9 +641,9 @@ namespace OS {
* Placeholder for the `fetch` function of the tree.
* This function is used to fetch the child nodes of the
* current tree. This function should return a promise on
* a list of [[TreeViewDataType]]
* a list of {@link TreeViewDataType}
*
* @memberof TreeViewItemPrototype
* @memberof TreeViewTag
*/
fetch: (
d: TreeViewItemPrototype
@ -920,7 +925,7 @@ namespace OS {
*/
protected mount(): void {
this._dnd = {
from: undefined,
from: [],
to: undefined,
};
this._treemousedown = (e) => {
@ -932,7 +937,7 @@ namespace OS {
if (el === this) {
return;
}
this._dnd.from = el;
this._dnd.from = [el];
this._dnd.to = undefined;
$(window).on("mouseup", this._treemouseup);
return $(window).on("mousemove", this._treemousemove);
@ -951,8 +956,8 @@ namespace OS {
el = el.parent;
}
if (
el === this._dnd.from ||
el === this._dnd.from.parent
el === this._dnd.from[0] ||
el === this._dnd.from[0].parent
) {
return;
}
@ -962,7 +967,7 @@ namespace OS {
data: this._dnd,
});
this._dnd = {
from: undefined,
from: [],
to: undefined,
};
};
@ -974,7 +979,7 @@ namespace OS {
if (!this._dnd.from) {
return;
}
const data = this._dnd.from.data;
const data = this._dnd.from[0].data;
const $label = $("#systooltip");
const top = e.clientY + 5;
const left = e.clientX + 5;

View File

@ -63,6 +63,14 @@ namespace OS {
*/
private _history: GenericObject<any>;
/**
* This placeholder store the callback for the menu open event
*
* @private
* @type {(el: StackMenuTag) => void}
* @memberof WindowTag
*/
private _onmenuopen: (el: StackMenuTag) => void;
/**
* This placeholder stores the offset of the virtual desktop element
*
@ -71,7 +79,7 @@ namespace OS {
* @memberof WindowTag
*/
private _desktop_pos: GenericObject<any>;
/**
* Creates an instance of WindowTag.
* @memberof WindowTag
@ -79,7 +87,33 @@ namespace OS {
constructor() {
super();
}
/**
* blur overlay: If active the window overlay will be shown
* on inactive (blur event)
*
* Setter: Enable the switch
*
* Getter: Check whether the switch is enabled
*
* @memberof WindowTag
*/
set blur_overlay(v: boolean) {
this.attsw(v, "blur-overlay");
}
get blur_overlay(): boolean {
return this.hasattr("blur-overlay");
}
/**
* Setter: set menu open event handler
*
* @memberof WindowTag
*/
set onmenuopen(f: (el: StackMenuTag) => void)
{
this._onmenuopen = f;
}
/**
* Init window tag
* - `shown`: false
@ -100,6 +134,7 @@ namespace OS {
this.minimizable = true;
this.resizable = true;
this.apptitle = "Untitled";
this._onmenuopen = undefined;
}
/**
@ -108,7 +143,7 @@ namespace OS {
* @protected
* @memberof WindowTag
*/
protected calibrate(): void {}
protected calibrate(): void { }
/**
* Do nothing
@ -117,7 +152,7 @@ namespace OS {
* @param {*} [d]
* @memberof WindowTag
*/
protected reload(d?: any): void {}
protected reload(d?: any): void { }
/**
* Setter: Set the window width
@ -157,6 +192,24 @@ namespace OS {
get height(): number {
return this._height;
}
/**
* Set the application menu content
*
* @memberof WindowTag
*/
set menu(v: GenericObject<any>[])
{
if(!v || v.length == 0)
{
$(this.refs.btnMenu).hide();
}
else
{
(this.refs.stackmenu as StackMenuTag).nodes = v;
$(this.refs.btnMenu).show();
}
}
/**
* Setter: enable/disable window minimizable
@ -188,14 +241,26 @@ namespace OS {
this.attsw(v, "resizable");
if (v) {
$(this.refs["maxbt"]).show();
$(this.refs["grip"]).show();
$(this.refs["grip_br"]).show();
$(this.refs["grip_tl"]).show();
$(this.refs["grip_tr"]).show();
$(this.refs["grip_bl"]).show();
$(this.refs["grip_bottom"]).show();
$(this.refs["grip_right"]).show();
$(this.refs["grip_top"]).show();
$(this.refs["grip_left"]).show();
} else {
$(this.refs["maxbt"]).hide();
$(this.refs["grip"]).hide();
$(this.refs["grip_br"]).hide();
$(this.refs["grip_tl"]).hide();
$(this.refs["grip_tr"]).hide();
$(this.refs["grip_bl"]).hide();
$(this.refs["grip_bottom"]).hide();
$(this.refs["grip_right"]).hide();
$(this.refs["grip_top"]).hide();
$(this.refs["grip_left"]).hide();
}
}
get resizable(): boolean {
@ -220,6 +285,15 @@ namespace OS {
return $(this).attr("apptitle");
}
/**
* Get the notification tag
*
* @memberof WindowTag
*/
get notification(): NotificationTag
{
return this.refs.notification as NotificationTag;
}
/**
* Resize all the children of the window based on its width and height
*
@ -245,30 +319,64 @@ namespace OS {
* @memberof WindowTag
*/
protected mount(): void {
this.contextmenuHandle = function (e) {};
$(this.refs["minbt"]).on("click",(e) => {
const btn_menu = (this.refs.btnMenu as ButtonTag);
const min_btn = (this.refs["minbt"] as ButtonTag);
const max_btn = (this.refs["maxbt"] as ButtonTag);
const close_btn = (this.refs["closebt"] as ButtonTag);
const stackmenu = (this.refs.stackmenu as StackMenuTag);
stackmenu.context = true;
btn_menu.iconclass = "bi bi-list";
min_btn.iconclass = "bi bi-dash";
max_btn.iconclass = "bi bi-stop";
close_btn.iconclass = "bi bi-x";
this.contextmenuHandle = function (e) { };
min_btn.onbtclick =(_) => {
return this.observable.trigger("hide", {
id: this.aid,
});
});
$(this.refs["maxbt"]).on("click",(e) => {
};
btn_menu.onbtclick = (e) => {
e.data.stopPropagation();
if($(stackmenu).is(":hidden"))
{
if(this._onmenuopen)
{
this._onmenuopen(stackmenu);
}
else
{
stackmenu.reset();
}
stackmenu.show();
}
else
{
$(stackmenu).hide();
}
};
max_btn.onbtclick = (_) => {
return this.toggle_window();
});
};
$(this.refs["closebt"]).on("click",(e) => {
close_btn.onbtclick = (_) => {
return this.observable.trigger("exit", {
id: this.aid,
});
});
};
stackmenu.onmenuselect = (e) => {
if(!e.data.item.data.nodes)
{
stackmenu.selected = -1;
}
}
const left = ($(this.desktop).width() - this.width) / 2;
const top = ($(this.desktop).height() - this.height) / 2;
$(this)
.css("position", "absolute")
.css("left", `${left}px`)
.css("top", `${top}px`)
.css("z-index", Ant.OS.GUI.zindex++);
$(this).on("mousedown", (e) => {
.css("z-index", 10);
$(this).on("pointerdown", (e) => {
if (this._shown) {
return;
}
@ -276,25 +384,26 @@ namespace OS {
id: this.aid,
});
});
$(this.refs["dragger"]).on("dblclick",(e) => {
$(this.refs["dragger"]).on("dblclick", (e) => {
return this.toggle_window();
});
this.observable.on("resize", (e) => this.resize());
this.observable.on("focus", () => {
Ant.OS.GUI.zindex++;
$(this)
.show()
.css("z-index", Ant.OS.GUI.zindex)
.removeClass("unactive");
this._shown = true;
$(this.refs.win_overlay).hide();
$(this).trigger("focus");
});
this.observable.on("blur", () => {
this._shown = false;
return $(this).addClass("unactive");
$(this).addClass("unactive");
if(this.blur_overlay)
$(this.refs.win_overlay).show();
});
this.observable.on("hide", () => {
$(this).hide();
@ -312,13 +421,42 @@ namespace OS {
});
}
});
this.observable.on("loaded", ()=>{
$(this.refs.panel).removeClass("loading");
$(this).css("cursor", "auto");
});
this.observable.on("loading", ()=>{
if(!$(this.refs.panel).hasClass("loading"))
$(this.refs.panel).addClass("loading");
$(this).css("cursor", "wait");
});
this.enable_dragging();
this.enable_resize();
this.setsize({
w: this.width,
h: this.height,
});
return this.observable.trigger("rendered", {
this.focusable = true
if(OS.mobile)
{
this.toggle_window();
//this.minimizable = false;
this.resizable = false;
}
this.observable.on("desktopresize", (e) => {
if(this._isMaxi)
{
this._isMaxi = false;
this.toggle_window(true);
}
/*else
{
const w = this.width > e.data.width ? e.data.width: this.width;
const h = this.height > e.data.height ? e.data.height: this.height;
this.setsize({ w: w, h: h });
}*/
});
this.observable.trigger("rendered", {
id: this.aid,
});
}
@ -342,6 +480,7 @@ namespace OS {
this.observable.trigger("resize", {
id: this.aid,
data: o,
root: true
});
}
@ -355,12 +494,12 @@ namespace OS {
$(this.refs["dragger"])
.css("user-select", "none")
.css("cursor", "default");
$(this.refs["dragger"]).on("mousedown", (e) => {
e.preventDefault();
$(this.refs.dragger).on("pointerdown", (e) => {
e.originalEvent.preventDefault();
const offset = $(this).offset();
offset.top = e.clientY - offset.top;
offset.left = e.clientX - offset.left;
$(window).on("mousemove", (e) => {
$(window).on("pointermove", (e) => {
$(this.refs.win_overlay).show();
let left: number, top: number;
if (this._isMaxi) {
@ -386,10 +525,10 @@ namespace OS {
.css("top", `${top}px`)
.css("left", `${left}px`);
});
return $(window).on("mouseup", (e) => {
return $(window).on("pointerup", (e) => {
$(this.refs.win_overlay).hide();
$(window).off("mousemove", null);
return $(window).off("mouseup", null);
$(window).off("pointermove", null);
return $(window).off("pointerup", null);
});
});
}
@ -407,50 +546,63 @@ namespace OS {
const mouse_move_hdl = (e) => {
let w = $(this).width();
let h = $(this).height();
let coord = $(this).offset();
$(this.refs.win_overlay).show();
if(target != this.refs.grip_bottom)
{
w += e.clientX - offset.left;
}
if(target != this.refs.grip_right)
{
if ((target == this.refs.grip_bottom) || (target == this.refs.grip_bl) || (target == this.refs.grip_br)) {
h += e.clientY - offset.top;
}
if ((target == this.refs.grip_right) || (target == this.refs.grip_br) || (target == this.refs.grip_tr)) {
w += e.clientX - offset.left;
}
if ((target == this.refs.grip_left) || (target == this.refs.grip_bl) || (target == this.refs.grip_tl)) {
w -= e.clientX - offset.left;
coord.left += e.clientX - offset.left;
}
if ((target == this.refs.grip_top) || (target == this.refs.grip_tr) || (target == this.refs.grip_tl)) {
h -= e.clientY - offset.top;
coord.top += e.clientY - offset.top;
}
w = w < 100 ? 100 : w;
h = h < 100 ? 100 : h;
coord.top = coord.top < 0 ? 0: coord.top;
coord.left = coord.left < 0 ? 0: coord.left;
offset.top = e.clientY;
offset.left = e.clientX;
this._isMaxi = false;
$(this)
.css("top", `${coord.top}px`)
.css("left", `${coord.left}px`);
this.setsize({ w, h });
}
const mouse_up_hdl = (e) => {
$(this.refs.win_overlay).hide();
$(window).off("mousemove", mouse_move_hdl);
return $(window).off("mouseup", mouse_up_hdl);
$(window).off("pointermove", mouse_move_hdl);
return $(window).off("pointerup", mouse_up_hdl);
}
$(this.refs["grip"]).on("mousedown", (e) => {
e.preventDefault();
offset.top = e.clientY;
offset.left = e.clientX;
target = this.refs.grip;
$(window).on("mousemove", mouse_move_hdl);
$(window).on("mouseup", mouse_up_hdl);
});
$(this.refs.grip_bottom).on("mousedown", (e) => {
e.preventDefault();
offset.top = e.clientY;
offset.left = e.clientX;
target = this.refs.grip_bottom;
$(window).on("mousemove", mouse_move_hdl);
$(window).on("mouseup", mouse_up_hdl);
});
$(this.refs.grip_right).on("mousedown", (e) => {
e.preventDefault();
offset.top = e.clientY;
offset.left = e.clientX;
target = this.refs.grip_right;
$(window).on("mousemove", mouse_move_hdl);
$(window).on("mouseup", mouse_up_hdl);
$.each(
[
this.refs.grip_bl,
this.refs.grip_br,
this.refs.grip_tl,
this.refs.grip_tr,
this.refs.grip_left,
this.refs.grip_bottom,
this.refs.grip_right,
this.refs.grip_top
], function(){
$(this).on("pointerdown", (e) => {
e.preventDefault();
offset.top = e.clientY;
offset.left = e.clientX;
target = this;
$(window).on("pointermove", mouse_move_hdl);
$(window).on("pointerup", mouse_up_hdl);
});
});
}
/**
@ -461,9 +613,9 @@ namespace OS {
* @returns {void}
* @memberof WindowTag
*/
private toggle_window(): void {
private toggle_window(force?: boolean): void {
let h: number, w: number;
if (!this.resizable) {
if (!this.resizable && !force) {
return;
}
if (this._isMaxi === false) {
@ -473,8 +625,8 @@ namespace OS {
width: $(this).css("width"),
height: $(this).css("height"),
};
w = $(this.desktop).width();
h = $(this.desktop).height();
w = $(this.desktop).width() - 2;
h = $(this.desktop).height() - 2;
$(this).css("top", "0").css("left", "0");
this.setsize({ w, h });
this._isMaxi = true;
@ -506,22 +658,17 @@ namespace OS {
children: [
{
el: "ul",
ref: 'panel',
class: "afx-window-top",
children: [
{
el: "li",
class: "afx-window-close",
ref: "closebt",
},
{
el: "li",
class: "afx-window-minimize",
ref: "minbt",
},
{
el: "li",
class: "afx-window-maximize",
ref: "maxbt",
children: [
{
el: "afx-button",
ref: "btnMenu",
},
],
},
{
el: "li",
@ -534,9 +681,38 @@ namespace OS {
},
],
},
{
el: "li",
class: "afx-window-minimize",
children: [
{
el: "afx-button",
ref: "minbt",
}
]
},
{
el: "li",
class: "afx-window-maximize",
children: [
{
el: "afx-button",
ref: "maxbt",
}
]
},
{
el: "li",
class: "afx-window-close",
children: [
{
el: "afx-button",
ref: "closebt",
}
]
},
],
},
{ el: "div", class: "afx-clear" },
{
el: "div",
ref: "yield",
@ -544,8 +720,23 @@ namespace OS {
},
{
el: "div",
ref: "grip",
class: "afx-window-grip",
ref: "grip_br",
class: "afx-window-grip-bottom-right",
},
{
el: "div",
ref: "grip_tl",
class: "afx-window-grip-top-left",
},
{
el: "div",
ref: "grip_bl",
class: "afx-window-grip-bottom-left",
},
{
el: "div",
ref: "grip_tr",
class: "afx-window-grip-top-right",
},
{
el: "div",
@ -557,11 +748,29 @@ namespace OS {
ref: "grip_right",
class: "afx-window-grip-right",
},
{
el: "div",
ref: "grip_top",
class: "afx-window-grip-top",
},
{
el: "div",
ref: "grip_left",
class: "afx-window-grip-left",
},
{
el: "div",
ref: "win_overlay",
class: "afx-window-overlay",
},
{
el: "afx-stack-menu",
ref: "stackmenu"
},
{
el: "afx-notification",
ref: "notification"
}
],
},
];

View File

@ -24,10 +24,10 @@ interface HTMLElement {
* defined on any child of this element will be ignored.
*
* @param {JQuery.MouseEventBase} e a mouse event
* @param {OS.GUI.tag.MenuTag} m The context menu element [[MenuTag]]
* @param {OS.GUI.tag.StackMenuTag} m The context menu element {@link OS.GUI.tag.StackMenuTag}
* @memberof HTMLElement
*/
contextmenuHandle(e: JQuery.MouseEventBase, m: OS.GUI.tag.MenuTag): void;
contextmenuHandle(e: JQuery.MouseEventBase, m: OS.GUI.tag.StackMenuTag): void;
/**
* Mount the element and all the children on its DOM subtree. This action
@ -51,7 +51,25 @@ interface HTMLElement {
afxml(o: OS.API.Announcer): void;
/**
* Perform DOM generation ([[afxml]]) then mount ([[sync]]) all the
* Enable the drag event dispatching on this
* element
*
* This will trigger the `dragging` and `drop` event on the enabled
* element when the mouse is down, move, then up, then move
*
* The event can be listened using the traditional way,
* Example:
* ```
* elem.addEventListener('dragging', (e) => { }, false);
* elem.addEventListener('drop', (e) => { }, false);
* ```
*
* @meberof HTMLElement
*/
enable_drag(): void;
/**
* Perform DOM generation ({@link afxml}) then mount ({@link sync}) all the
* elements.
*
* @param {OS.API.Announcer} o an AntOS observable object
@ -98,7 +116,7 @@ interface Document {
namespace OS {
export namespace GUI {
/**
* [[TagLayoutType]] interface using by AFX tags to defined
* TagLayoutType interface using by AFX tags to defined
* its internal DOM hierarchy
*
* @export
@ -139,7 +157,7 @@ namespace OS {
/**
* this is the `data-id` attribute of the element,
* can be query by the [[aid]] Tag API function.
* can be query by the {@link OS.GUI.AFXTag.aid} Tag API function.
* Not to be confused with the DOM `id` attribute
*
* @type {(string | number)}
@ -162,7 +180,7 @@ namespace OS {
* @type {number|string}
* @memberof TagLayoutType
*/
width?: number|string;
width?: number | string;
/**
** `data-height` of the element, not to be confused with
@ -171,7 +189,7 @@ namespace OS {
* @type {number|string}
* @memberof TagLayoutType
*/
height?: number|string;
height?: number | string;
}
/**
@ -217,6 +235,20 @@ namespace OS {
* @memberof TagEventType
*/
data: T;
/**
* Original event if any
*
* @type {any}
* @memberof TagEventType
*/
originalEvent?: any;
/**
* is root tag?
*
* @type {boolean}
* @memberof TagEventType
*/
root?: boolean;
}
/**
@ -233,7 +265,7 @@ namespace OS {
* @type {T}
* @memberof DnDEventDataType
*/
from: T;
from: T[];
/**
* Reference to the target DOM element
@ -245,12 +277,162 @@ namespace OS {
}
/**
* Tag event callback type
*
* @export
*/
export type TagEventCallback<T> = (e: TagEventType<T>) => void;
/**
* Top most element z index value, start by 10
* Tag responsive envent callback type
*
* @export
*/
export var zindex: number = 10;
export type TagResponsiveCallback = (fullfilled: boolean) => void;
/**
* A callback record with history of last fulfilled
*
* @interface TagResponsiveCallbackRecord
*/
interface TagResponsiveCallbackRecord {
/**
* Callback function
*
* @type {TagResponsiveCallback}
* @memberof TagResponsiveCallbackRecord
*/
callback: TagResponsiveCallback,
/**
* has the event been previously fulfilled?
*
* @type {boolean}
* @memberof TagResponsiveCallbackRecord
*/
fulfilled: boolean,
}
/**
* Tag responsive validator type
*
* @export
*/
export type TagResponsiveValidator = (w: number, h: number) => boolean;
/**
* Definitions of some default tag responsive validators
*
* @export
*/
export const RESPONSIVE = {
/**
* Extra small devices (phones, 600px and down)
*/
TINY: function(w: number,_h: number){
return w <= 600;
},
/*
Small devices (portrait tablets and large phones, 600px and up)
*/
SMALL: function(w: number, _h: number){
return w <= 768;
},
/*
Medium devices (landscape tablets, 768px and up)
*/
MEDIUM: function(w: number, _h: number){
return w > 768;
},
/**
* Large devices (laptops/desktops, 992px and up)
*/
LARGE: function(w: number, _h: number){
return w > 992;
},
/**
* Extra large devices (large laptops and desktops, 1200px and up)
*/
HUGE: function(w: number, _h: number){
return w > 1200;
},
/**
* Portrait mode
*/
PORTRAIT: function(w: number, h: number){
return h > w;
},
/**
* Landscape mode
*/
LANDSCAPE: function(w: number, h: number){
return h < w;
},
}
/**
* A custom map to handle responsive events, a responsive event
* consists of a validator and a callback
*
* When avalidator return true, its corresponding callback will
* be called
*/
class ResponsiveHandle extends Map<TagResponsiveValidator,TagResponsiveCallbackRecord> {
/**
* Register a responsive envent to this map
*
* @param {TagResponsiveValidator} a validator
* @param {TagResponsiveCallback} event callback
* @memberof ResponsiveHandle
*/
on(validator: TagResponsiveValidator, callback: TagResponsiveCallback)
{
let record: TagResponsiveCallbackRecord = {
callback: callback,
fulfilled: false,
}
this.set(validator, record);
}
/**
* unregister a responsive event
*
* @param {TagResponsiveValidator} a validator
* @memberof ResponsiveHandle
*/
off(validator: TagResponsiveValidator)
{
this.delete(validator);
}
/**
* Verify validators and execute callbacks
*
* @param {number} root tag width
* @param {number} root tag height
* @memberof ResponsiveHandle
*/
morph(rootw: number, rooth: number)
{
for (const [validator, record] of this.entries()) {
const val = validator(rootw, rooth);
if(record.fulfilled && val)
{
// the record has been previously fulfilled
// and the validator returns true
// nothing changed
continue;
}
if(!record.fulfilled && !val)
{
// the record has been previously unfulfilled
// and the validator returns false
// nothing changed
continue;
}
record.fulfilled =val;
record.callback(val);
}
}
}
/**
* Base abstract class for tag implementation, any AFX tag should be
@ -274,7 +456,7 @@ namespace OS {
* Reference to some of the tag's children
* element. This reference object is built
* based on the `ref` property found in the
* tag layout [[TagLayoutType]]
* tag layout {@link TagLayoutType}
*
* @protected
* @type {GenericObject<HTMLElement>}
@ -293,7 +475,17 @@ namespace OS {
protected _mounted: boolean;
/**
*Creates an instance of AFXTag.
* a {@link ResponsiveHandle} to handle all responsive event
* related to this tag
*
* @private
* @memberof AFXTag
*/
private _responsive_handle: ResponsiveHandle;
private _responsive_check: (evt: TagEventType<{w: number, h: number}>) => void;
/**
* Creates an instance of AFXTag.
* @memberof AFXTag
*/
constructor() {
@ -304,6 +496,8 @@ namespace OS {
}
this._mounted = false;
this.refs = {};
this._responsive_handle = new ResponsiveHandle();
this._responsive_check = undefined;
}
/**
@ -338,6 +532,19 @@ namespace OS {
}
$(this).attr("tooltip", v);
}
/**
* Setter to activate or deactivate focus on a Tag
*
* @memberof AFXTag
*/
set focusable(v: boolean) {
if(v) {
$(this).attr("tabindex", 0).css("outline", "none");
} else {
$(this).removeAttr("tabindex");
}
}
/**
*
@ -374,6 +581,79 @@ namespace OS {
return $(this).attr("data-id");
}
/**
* Setter: Enable/disable responsive tag
*
* Getter: get responsive status
*/
set responsive(v:boolean) {
if(!v)
{
this.attsw(false,"responsive");
this.observable.off("resize",this._responsive_check);
this._responsive_check = undefined;
return;
}
if(this._responsive_check)
{
return;
}
this._responsive_check = (evt: TagEventType<{w: number, h: number}>) => {
if(!evt || !evt.root || !evt.data.w || !evt.data.h )
{
return;
}
this._responsive_handle.morph(evt.data.w, evt.data.h);
}
this.observable.on("resize", this._responsive_check);
this.attsw(true, "responsive");
}
get responsive(): boolean {
return this.hasattr("responsive");
}
/**
* Register a responsive event to this tag
*
* @param {TagResponsiveValidator} responsive validator
* @param {TagResponsiveCallback} responsive callback
* @memberof AFXTag
*/
morphon(validator: TagResponsiveValidator, callback:TagResponsiveCallback)
{
this._responsive_handle.on(validator, callback);
}
/**
* Unregister a responsive event from this tag
*
* @param {TagResponsiveValidator} responsive validator
* @memberof AFXTag
*/
morphoff(validator: TagResponsiveValidator)
{
this._responsive_handle.off(validator);
}
/**
* Attach a data to this tag
*
* This function will define a getter `domel`
* in the attached data, this getter refers to the
* current tag
*
* @returns {void}
* @memberof AFXTag
*/
attach(data: GenericObject<any>): void {
const self = this;
Object.defineProperty(data, 'domel', {
get: function () { return self },
enumerable: false,
configurable: true
})
}
/**
* Implementation from HTMLElement interface,
* this function mount the current tag hierarchy
@ -448,7 +728,7 @@ namespace OS {
/**
* Init the current tag, this function
* is called before the [[mount]] function
* is called before the {@link mount} function
*
* @protected
* @abstract
@ -477,7 +757,7 @@ namespace OS {
/**
* Update only the current tag, this function is
* called by [[update]] before chaining the
* called by {@link update} before chaining the
* update process to its children
*
* @protected
@ -494,7 +774,7 @@ namespace OS {
* @protected
* @memberof AFXTag
*/
protected calibrate(): void {}
protected calibrate(): void { }
/**
* This function parses the input layout object
@ -594,11 +874,48 @@ namespace OS {
}
}
HTMLElement.prototype.enable_drag = function () {
$(this)
.on("pointerdown", (evt: JQuery.MouseEventBase) => {
const offset = $(this).offset();
offset.top = evt.clientY - offset.top;
offset.left = evt.clientX - offset.left;
const mouse_move = (
e: JQuery.MouseEventBase
) => {
const custom_event = new CustomEvent('dragging', {
detail: {
origin: evt,
current: e,
offset: offset
}
});
this.dispatchEvent(custom_event);
};
var mouse_up = (e: JQuery.MouseEventBase) => {
$(window).off("pointermove", mouse_move);
$(window).off("pointerup", mouse_up);
// trigger the drop event
const custom_event = new CustomEvent('drop', {
detail: {
origin: evt,
current: e,
offset: offset
}
});
this.dispatchEvent(custom_event);
};
$(window).on("pointermove", mouse_move);
$(window).on("pointerup", mouse_up);
});
}
HTMLElement.prototype.update = function (d): void {
$(this)
.children()
.each(function () {
if(this.update)
if (this.update)
return this.update(d);
});
};
@ -630,6 +947,12 @@ namespace OS {
* stored in the `customElements` registry of the browser
*/
export namespace tag {
/**
* Alias to all classes that extends {@link AFXTag}
*/
export type AFXTagTypeClass = {
new <T extends AFXTag>(): T;
};
/**
* Define an AFX tag as a custom element and add it to the
* global `customElements` registry. If the tag is redefined, i.e.
@ -637,14 +960,14 @@ namespace OS {
* new definition
*
* @export
* @template T all classes that extends [[AFXTag]]
* @template T all classes that extends {@link AFXTag}
* @param {string} name name of the tag
* @param {{ new (): T }} cls the class that defines the tag
* @returns {void}
*/
export function define<T extends AFXTag>(
name: string,
cls: { new (): T }
cls: { new(): T }
): void {
try {
customElements.define(name, cls);

View File

@ -21,7 +21,7 @@ interface String {
* Convert a string to VFS file handle.
*
* This function will create a file handle object from the string
* with the help of [[VFS.findHandles]]
* with the help of {@link OS.API.VFS.findHandles}
*
* @returns {OS.API.VFS.BaseFileHandle}
* @memberof String
@ -165,7 +165,9 @@ namespace OS {
*/
export namespace VFS {
declare var JSZip: any;
/** path for custom VFS protocol handles */
const VFSX_PATH = "pkg://vfsx/vfsx.js";
String.prototype.asFileHandle = function (): BaseFileHandle {
const list = this.split("://");
const handles = API.VFS.findHandles(list[0]);
@ -198,13 +200,33 @@ namespace OS {
): void {
handles[protos] = cls;
}
/**
* Load custom VFS handles if the package vfsx available
*
* @export
* @param {boolean} [force] force load the file
* @return {*} {Promise<any>}
*/
export function loadVFSX(force?: boolean): Promise<any>
{
return new Promise(async (resolve, reject) => {
try {
await API.requires(VFSX_PATH, force);
resolve(true);
}
catch(e)
{
console.warn("No custom VFS protocol loaded");
resolve(true);
}
})
}
/**
* Looking for a attached file handle class of a string protocol
*
* When converting a string to file handle, the system will look
* for a protocol pattern in the string, if the protocol found,
* its attached handle class (found in [[VFS.handles]]) will be
* its attached handle class (found in {@link VFS.handles}) will be
* used to initialize a file handle object from the string
*
* ```typescript
@ -250,11 +272,12 @@ namespace OS {
/**
* Once read, file content will be cached in this placeholder
*
*
* @private
* @type {*}
* @memberof BaseFileHandle
*/
cache: any;
private _cache: any;
/**
* Flag indicated whether the file meta-data is loaded
@ -299,7 +322,7 @@ namespace OS {
basename: string;
/**
* Once loaded, [[ready]] will be set to true and
* Once loaded, {@link ready} will be set to true and
* file meta-data will be stored in this place holder
*
* @type {FileInfoType}
@ -333,6 +356,8 @@ namespace OS {
this.setPath(path);
}
/**
* Set a file path to the current file handle
*
@ -355,7 +380,7 @@ namespace OS {
if (re === "") {
return;
}
this.genealogy = re.split("/").filter(s=> s!="");
this.genealogy = re.split("/").filter(s => s != "");
this.path = `${this.protocol}://${this.genealogy.join("/")}`;
if (!this.isRoot()) {
this.basename = this.genealogy[
@ -386,6 +411,22 @@ namespace OS {
set filename(v: string) {
this.basename = v;
}
/**
* Getter: Get the file cache
* Setter: set the file cache
*
* @returns {any}
* @memberof BaseFileHandle
*/
get cache(): any {
return this._cache;
}
set cache(v: any) {
this._cache = v;
this._cache_changed();
}
/**
* Set data to the file cache
*
@ -528,32 +569,29 @@ namespace OS {
/**
* Public read operation
*
* This function calls the [[_rd]] function to perform the operation.
* This function calls the {@link _rd} function to perform the operation.
*
* If the current file is a directory, then the operation
* will return the meta-data of all files inside of the directory.
* Otherwise, file content will be returned
*
* @param {string} t data type
* @param {any} formal t data type
* - jsonp: the response is an json object
* - script: the response is a javascript code
* - xml, html: the response is a XML/HTML object
* - text: plain text
* - binary
* - other user case may be user specific data
*
* @returns {Promise<any>} a promise on the file content
* @memberof BaseFileHandle
*/
read(t?: string): Promise<any> {
read(t?: any): Promise<any> {
return new Promise(async (resolve, reject) => {
try {
const r = await this.onready();
try {
const d = await this._rd(t);
return resolve(d);
} catch (e) {
return reject(__e(e));
}
const d = await this._rd(t);
resolve(d);
} catch (e_1) {
return reject(__e(e_1));
}
@ -563,7 +601,7 @@ namespace OS {
/**
* Write the file cache to the actual file
*
* This function calls the [[_wr]] function to perform the operation
* This function calls the {@link _wr} function to perform the operation
*
* @param {string} t data type
* - `base64`
@ -577,10 +615,7 @@ namespace OS {
return new Promise(async (resolve, reject) => {
try {
const r: RequestResult = await this._wr(t);
announcer.ostrigger("VFS", {
m: "write",
file: this,
});
announcer.ostrigger("VFS", "write",this);
return resolve(r);
} catch (e) {
return reject(__e(e));
@ -591,7 +626,7 @@ namespace OS {
/**
* Sub-directory creation
*
* This function calls the [[_mk]] function to perform the operation
* This function calls the {@link _mk} function to perform the operation
*
* @param {string} d sub directory name
* @returns {Promise<RequestResult>} promise on the operation result
@ -601,16 +636,9 @@ namespace OS {
return new Promise(async (resolve, reject) => {
try {
const r = await this.onready();
try {
const d_1 = await this._mk(d);
announcer.ostrigger("VFS", {
m: "mk",
file: this,
});
return resolve(d_1);
} catch (e) {
return reject(__e(e));
}
const d_1 = await this._mk(d);
announcer.ostrigger("VFS","mk",this);
return resolve(d_1);
} catch (e_1) {
return reject(__e(e_1));
}
@ -620,25 +648,18 @@ namespace OS {
/**
* Delete the file
*
* This function calls the [[_rm]] function to perform the operation
*
* This function calls the {@link _rm} function to perform the operation
* @param {any} d user data
* @returns {Promise<RequestResult>} promise on the operation result
* @memberof BaseFileHandle
*/
remove(): Promise<RequestResult> {
remove(data?: any): Promise<RequestResult> {
return new Promise(async (resolve, reject) => {
try {
const r = await this.onready();
try {
const d = await this._rm();
announcer.ostrigger("VFS", {
m: "remove",
file: this,
});
return resolve(d);
} catch (e) {
return reject(__e(e));
}
const d = await this._rm(data);
announcer.ostrigger("VFS", "remove",this);
return resolve(d);
} catch (e_1) {
return reject(__e(e_1));
}
@ -650,7 +671,7 @@ namespace OS {
*
* Only work when the current file is a directory
*
* This function calls the [[_up]] function to perform the operation
* This function calls the {@link _up} function to perform the operation
*
* @returns {Promise<RequestResult>} promise on the operation result
* @memberof BaseFileHandle
@ -659,16 +680,9 @@ namespace OS {
return new Promise(async (resolve, reject) => {
try {
const r = await this.onready();
try {
const d = await this._up();
announcer.ostrigger("VFS", {
m: "upload",
file: this,
});
return resolve(d);
} catch (e) {
return reject(__e(e));
}
const d = await this._up();
announcer.ostrigger("VFS", "upload", this);
return resolve(d);
} catch (e_1) {
return reject(__e(e_1));
}
@ -680,7 +694,7 @@ namespace OS {
*
* Only work with file
*
* This function calls the [[_pub]] function to perform the operation
* This function calls the {@link _pub} function to perform the operation
*
* @returns {Promise<RequestResult>} promise on operation result
* @memberof BaseFileHandle
@ -689,16 +703,9 @@ namespace OS {
return new Promise(async (resolve, reject) => {
try {
const r = await this.onready();
try {
const d = await this._pub();
announcer.ostrigger("VFS", {
m: "publish",
file: this,
});
return resolve(d);
} catch (e) {
return reject(__e(e));
}
const d = await this._pub();
announcer.ostrigger("VFS", "publish",this);
return resolve(d);
} catch (e_1) {
return reject(__e(e_1));
}
@ -710,7 +717,7 @@ namespace OS {
*
* Only work with file
*
* This function calls the [[_down]] function to perform the operation
* This function calls the {@link _down} function to perform the operation
*
* @returns {Promise<any>} Promise on the operation result
* @memberof BaseFileHandle
@ -719,16 +726,9 @@ namespace OS {
return new Promise(async (resolve, reject) => {
try {
const r = await this.onready();
try {
const d = await this._down();
announcer.ostrigger("VFS", {
m: "download",
file: this,
});
return resolve(d);
} catch (e) {
return reject(__e(e));
}
const d = await this._down();
announcer.ostrigger("VFS", "download",this);
return resolve(d);
} catch (e_1) {
return reject(__e(e_1));
}
@ -738,7 +738,7 @@ namespace OS {
/**
* Move the current file to another location
*
* This function calls the [[_mv]] function to perform the operation
* This function calls the {@link _mv} function to perform the operation
*
* @param {string} d destination location
* @returns {Promise<RequestResult>} promise on the operation result
@ -748,16 +748,10 @@ namespace OS {
return new Promise(async (resolve, reject) => {
try {
const r = await this.onready();
try {
const data = await this._mv(d);
announcer.ostrigger("VFS", {
m: "move",
file: d.asFileHandle(),
});
return resolve(data);
} catch (e) {
return reject(__e(e));
}
const data = await this._mv(d);
announcer.ostrigger("VFS", "move",d.asFileHandle());
return resolve(data);
} catch (e_1) {
return reject(__e(e_1));
}
@ -769,7 +763,7 @@ namespace OS {
*
* This action depends on each file protocol
*
* This function calls the [[_exec]] function to perform the operation
* This function calls the {@link _exec} function to perform the operation
*
* @returns {Promise<any>}
* @memberof BaseFileHandle
@ -778,16 +772,9 @@ namespace OS {
return new Promise(async (resolve, reject) => {
try {
const r = await this.onready();
try {
const d = await this._exec();
announcer.ostrigger("VFS", {
m: "execute",
file: this,
});
return resolve(d);
} catch (e) {
return reject(__e(e));
}
const d = await this._exec();
announcer.ostrigger("VFS", "execute", this);
return resolve(d);
} catch (e_1) {
return reject(__e(e_1));
}
@ -826,6 +813,17 @@ namespace OS {
});
}
/**
* trigger cache changed event
*
* This function triggered when the file cached changed
*
* @protected
* @memberof BaseFileHandle
*/
protected _cache_changed():void {
}
/**
* Low level protocol-specific read operation
*
@ -833,11 +831,11 @@ namespace OS {
* that supports the operation
*
* @protected
* @param {string} t data type, see [[read]]
* @param {any} t data type, see {@link read}
* @returns {Promise<RequestResult>}
* @memberof BaseFileHandle
*/
protected _rd(t: string): Promise<RequestResult> {
protected _rd(t: any): Promise<RequestResult> {
return this.unsupported("read");
}
@ -848,12 +846,11 @@ namespace OS {
* that supports the operation
*
* @protected
* @param {string} t data type, see [[write]]
* @param {*} [d]
* @param {string} t data type, see {@link write}
* @returns {Promise<RequestResult>}
* @memberof BaseFileHandle
*/
protected _wr(t: string, d?: any): Promise<RequestResult> {
protected _wr(t: string): Promise<RequestResult> {
return this.unsupported("write");
}
@ -877,10 +874,11 @@ namespace OS {
* This function should be overridden by the file handle class
* that supports the operation
*
* @param {*} [d] any user data
* @returns {Promise<RequestResult>}
* @memberof BaseFileHandle
*/
protected _rm(): Promise<RequestResult> {
protected _rm(d: any): Promise<RequestResult> {
return this.unsupported("remove");
}
@ -1027,7 +1025,7 @@ namespace OS {
* Otherwise, file content will be returned
*
* @protected
* @param {string} t data type see [[read]]
* @param {string} t data type see {@link read}
* @returns {Promise<any>}
* @memberof RemoteFileHandle
*/
@ -1059,15 +1057,15 @@ namespace OS {
* Write file cache to the remote file
*
* @protected
* @param {string} t data type see [[write]]
* @param {string} t data type see {@link write}
* @returns {Promise<RequestResult>}
* @memberof RemoteFileHandle
*/
protected _wr(t: string): Promise<RequestResult> {
// t is base64 or undefined
return new Promise(async (resolve, reject) => {
if (t === "base64") {
try {
try {
if (t === "base64") {
const d = await API.handle.write(
this.path,
this.cache
@ -1080,11 +1078,7 @@ namespace OS {
);
}
return resolve(d);
} catch (e) {
return reject(__e(e));
}
} else {
try {
} else {
const r = await this.b64(t);
const result = await API.handle.write(
this.path,
@ -1102,9 +1096,9 @@ namespace OS {
);
}
return resolve(result);
} catch (e_2) {
return reject(__e(e_2));
}
} catch (e) {
return reject(__e(e));
}
});
}
@ -1299,7 +1293,7 @@ namespace OS {
/**
* Package file is remote file ([[RemoteFileHandle]]) located either in
* Package file is remote file ({@link RemoteFileHandle}) located either in
* the local user packages location or system packages
* location, it should be in the following format:
*
@ -1369,11 +1363,17 @@ namespace OS {
constructor(path: string) {
super(path);
if (this.basename) {
let v: any = OS.setting.system.packages[this.basename];
v.type = "app";
v.mime = "antos/app";
v.size = 0;
this.info = v as FileInfoType;
let v = OS.setting.system.packages[this.basename];
this.info = {} as FileInfoType;
for(const p in v)
{
this.info[p] = v[p];
}
this.info.type = "app";
this.info.mime = "antos/app";
this.info.size = 0;
this.info.text = v.name;
}
this.ready = true;
}
@ -1436,6 +1436,8 @@ namespace OS {
register("^app$", ApplicationHandle);
var MEM_PARTITION: BufferFileHandle = undefined;
/**
* A buffer file handle represents a virtual file that is stored
* on the system memory. Its protocol pattern is defined as:
@ -1457,18 +1459,96 @@ namespace OS {
*/
constructor(path: string, mime: string, data: any) {
super(path);
if (data) {
this.cache = data;
}
this.info = {
mime: mime,
mime: mime?mime:'application/octet-stream',
path: path,
size: data ? data.length : 0,
name: this.basename,
type: "file",
filename:this.basename,
ctime: (new Date()).toGMTString(),
mtime: (new Date()).toGMTString(),
type: 'file',
};
if (data) {
this.cache = data;
}
return this.init_file_tree();
}
/**
* init the mem file tree if necessary
*/
private init_file_tree(): BufferFileHandle
{
if(this.isRoot())
{
if(!MEM_PARTITION)
{
this.cache = {};
MEM_PARTITION = this;
}
return MEM_PARTITION;
}
let curr_level = "mem://".asFileHandle();
for(let i = 0;i<this.genealogy.length - 1;i++)
{
const segment = this.genealogy[i];
let handle = undefined;
if(segment == "..")
{
curr_level = curr_level.parent();
continue;
}
handle = curr_level.cache[segment];
if(!handle)
{
handle = new BufferFileHandle(`${curr_level.path}/${segment}`, 'dir', {});
curr_level.cache[segment] = handle;
}
curr_level = handle;
if(!curr_level.cache)
{
curr_level.cache = {};
}
}
if(this.basename == "..")
{
return curr_level.parent() as BufferFileHandle;
}
if(!curr_level.cache[this.basename])
curr_level.cache[this.basename] = this;
return curr_level.cache[this.basename];
}
/**
* cache changed handle
*/
protected _cache_changed(): void
{
if(!this.cache)
{
return;
}
this.info.mime = (new Date()).toGMTString();
if(typeof this.cache === "string" || this.cache instanceof Blob || this.cache instanceof Uint8Array)
{
this.info.type = "file";
if(typeof this.cache === "string")
{
this.info.mime = "text/plain";
}
else
{
this.info.mime = "application/octet-stream";
}
}
else
{
this.info.type = "dir";
this.info.mime = "dir";
}
this.info.size = this.cache.length;
}
/**
* Read the file meta-data
*
@ -1477,23 +1557,58 @@ namespace OS {
*/
meta(): Promise<RequestResult> {
return new Promise((resolve, reject) =>
{
const data = {};
for(const k in this.info)
{
data[k] = this.info[k];
}
resolve({
result: this.info,
result: data,
error: false,
})
);
});
}
/**
* Load the file meta-data before performing
* any task
*
* @returns {Promise<FileInfoType>} a promise on file meta-data
* @memberof BufferFileHandle
*/
onready(): Promise<FileInfoType> {
// read meta data
return new Promise(async (resolve, reject) => {
try
{
const d = await this.meta();
resolve(d.result as FileInfoType);
}
catch(e)
{
reject(__e(e));
}
});
}
/**
* Read file content stored in the file cached
*
* @protected
* @param {string} t data type see [[read]]
* @param {string} t data type see {@link read}
* @returns {Promise<any>}
* @memberof BufferFileHandle
*/
protected _rd(t: string): Promise<any> {
return new Promise((resolve, reject) => {
// read dir
if (this.info.type === "dir") {
return resolve({
result: (Object.values(this.cache) as BufferFileHandle[]).map(o=>o.info),
error: false,
});
}
return resolve(this.cache);
});
}
@ -1502,13 +1617,11 @@ namespace OS {
* Write data to the file cache
*
* @protected
* @param {string} t data type, see [[write]]
* @param {*} d data
* @param {string} t data type, see {@link write}
* @returns {Promise<RequestResult>}
* @memberof BufferFileHandle
*/
protected _wr(t: string, d: any): Promise<RequestResult> {
this.cache = d;
protected _wr(t: string): Promise<RequestResult> {
return new Promise((resolve, reject) =>
resolve({
result: true,
@ -1516,6 +1629,105 @@ namespace OS {
})
);
}
/**
* Create sub directory
*
* Only work on directory file handle
*
* @protected
* @param {string} d sub directory name
* @returns {Promise<RequestResult>}
* @memberof BufferFileHandle
*/
protected _mk(d: string): Promise<RequestResult> {
return new Promise((resolve, reject) => {
if (this.info.type === "file") {
return reject(
API.throwe(
__("{0} is not a directory", this.path)
)
);
}
const handle = `${this.path}/${d}`.asFileHandle();
if(handle.info.type === "file")
{
handle.cache = {};
}
resolve({result: true, error: false});
});
}
/**
* Delete file/folder
*
* @protected
* @returns {Promise<RequestResult>}
* @memberof BufferFileHandle
*/
protected _rm(): Promise<RequestResult> {
return new Promise(async (resolve, reject) => {
try {
const parent = this.parent();
parent.cache[this.basename] = undefined;
delete parent.cache[this.basename];
return resolve({result: true, error: false});
} catch (e) {
return reject(__e(e));
}
});
}
setPath(p: string): void
{
super.setPath(p);
if(this.info)
this.info.path = this.path;
}
private updatePath(path: string)
{
this.setPath(path);
if(this.info.type == "file")
{
return;
}
for(const k in this.cache)
{
const child = this.cache[k];
child.updatePath(`${this.path}/${child.basename}`);
}
}
/**
* Move file/folder
*
* @protected
* @param {string} d
* @returns {Promise<RequestResult>}
* @memberof BufferFileHandle
*/
protected _mv(d: string): Promise<RequestResult> {
return new Promise(async (resolve, reject) => {
try {
if(d.includes(this.path))
{
return reject(API.throwe(__("Unable to move file/folder from {0} to {1}", this.path, d)));
}
const parent = this.parent()
parent.cache[this.basename] = undefined;
delete parent.cache[this.basename];
const dest = d.asFileHandle();
this.updatePath(dest.path);
dest.parent().cache[dest.basename] = this;
return resolve({result: true, error: false});
} catch (e) {
return reject(__e(e));
}
});
}
/**
* Download the buffer file
@ -1526,6 +1738,10 @@ namespace OS {
*/
protected _down(): Promise<void> {
return new Promise((resolve, reject) => {
if(this.info.type == "dir")
{
return reject(API.throwe(__("{0} is a directory", this.path)));
}
const blob = new Blob([this.cache], {
type: "octet/stream",
});
@ -1585,7 +1801,7 @@ namespace OS {
* Read URL content
*
* @protected
* @param {string} t data type see [[read]]
* @param {string} t data type see {@link read}
* @returns {Promise<any>}
* @memberof URLFileHandle
*/
@ -1639,7 +1855,7 @@ namespace OS {
* Read file content
*
* @protected
* @param {string} t data type, see [[read]]
* @param {string} t data type, see {@link read}
* @returns {Promise<any>}
* @memberof SharedFileHandle
*/
@ -1658,23 +1874,46 @@ namespace OS {
* write data to shared file
*
* @protected
* @param {string} t data type, see [[write]]
* @param {string} d file data
* @param {string} t data type, see {@link write}
* @returns {Promise<RequestResult>}
* @memberof SharedFileHandle
*/
protected _wr(t: string, d: string): Promise<RequestResult> {
protected _wr(t: string): Promise<RequestResult> {
return new Promise(async (resolve, reject) => {
try {
const r = await API.handle.write(this.path, d);
if (r.error) {
return reject(
API.throwe(
__("{0}: {1}", r.error, this.path)
)
if (t === "base64") {
const d = await API.handle.write(
this.path,
this.cache
);
if (d.error) {
return reject(
API.throwe(
__("{0}: {1}", d.error, this.path)
)
);
}
return resolve(d);
} else {
const r = await this.b64(t);
const result = await API.handle.write(
this.path,
r as string
);
if (result.error) {
return reject(
API.throwe(
__(
"{0}: {1}",
result.error,
this.path
)
)
);
}
return resolve(result);
}
return resolve(r);
} catch (e) {
return reject(__e(e));
}
@ -1855,7 +2094,6 @@ namespace OS {
promises.push(new Promise(async (resolve, reject) => {
try {
const file = path.asFileHandle();
const tof = `${to}/${file.basename}`.asFileHandle();
const meta = await file.onready();
if (meta.type === "dir") {
const desdir = to.asFileHandle();
@ -1871,6 +2109,7 @@ namespace OS {
}
}
else {
const tof = `${to}/${file.basename}`.asFileHandle();
const content = await file.read("binary");
await tof
.setCache(
@ -1945,14 +2184,12 @@ namespace OS {
try {
await API.requires("os://scripts/jszip.min.js");
const zip = new JSZip();
const fhd = src.asFileHandle();
const fhd = src.asFileHandle();
const meta = await fhd.onready();
if(meta.type === "file")
{
if (meta.type === "file") {
await aradd([src], zip, "/");
}
else
{
else {
const ret = await fhd.read();
await aradd(
ret.result.map((v: API.FileInfoType) => v.path), zip, "/");
@ -2032,17 +2269,26 @@ namespace OS {
const data = await zfile.asFileHandle().read("binary");
const zip = await JSZip.loadAsync(data);
const to = await dest_callback(zip);
const dirs = [];
const dirs = new Set<string>();
const files = [];
for (const name in zip.files) {
const file = zip.files[name];
for (const _name in zip.files) {
const name = _name.trimFromRight("/");
const file = zip.files[_name];
if (file.dir) {
dirs.push(to + "/" + name);
dirs.add(to + "/" + name);
} else {
const segs = name.split("/");
segs.pop();
let path = to;
for(let seg of segs)
{
path += "/" + seg;
dirs.add(path);
}
files.push(name);
}
}
await mkdirAll(dirs, true);
await mkdirAll(Array.from(dirs), true);
const promises = [];
for (const file of files) {
promises.push(new Promise(async (res, rej) => {

View File

@ -1,401 +0,0 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS202: Simplify dynamic range loops
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// AnTOS Web desktop is is licensed under the GNU General Public
// License v3.0, see the LICENCE file for more information
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
// You should have received a copy of the GNU General Public License
//along with this program. If not, see https://www.gnu.org/licenses/.
// GoogleDrive File Handle
let G_CACHE = {"gdv://":{ id: "root", mime: 'dir' } };
class GoogleDriveHandle extends this.OS.API.VFS.BaseFileHandle {
constructor(path) {
super(path);
const me = this;
this.setting = Ant.OS.setting.VFS.gdrive;
if (!this.setting) { return Ant.OS.announcer.oserror(__("Unknown API setting for {0}", "GAPI"), (Ant.OS.API.throwe("OS.VFS")), null); }
if (this.isRoot()) { this.gid = 'root'; }
this.cache = "";
}
oninit(f) {
const me = this;
if (!this.setting) { return; }
const fn = function(r) {
if (r) { return f(); }
// perform the login
G_CACHE = {"gdv://":{ id: "root", mime: 'dir' } };
return gapi.auth2.getAuthInstance().signIn();
};
if (Ant.OS.API.libready(this.setting.apilink)) {
gapi.auth2.getAuthInstance().isSignedIn.listen(r => fn(r));
return fn(gapi.auth2.getAuthInstance().isSignedIn.get());
} else {
return Ant.OS.API.require(this.setting.apilink, function() {
// avoid popup block
const q = Ant.OS.announcer.getMID();
return gapi.load("client:auth2", function() {
Ant.OS.API.loading(q, "GAPI");
return gapi.client.init({
apiKey: me.setting.API_KEY,
clientId: me.setting.CLIENT_ID,
discoveryDocs: me.setting.DISCOVERY_DOCS,
scope: me.setting.SCOPES
})
.then(function() {
Ant.OS.API.loaded(q, "OK");
gapi.auth2.getAuthInstance().isSignedIn.listen(r => fn(r));
return _GUI.openDialog("YesNoDialog", function(d) {
if (!d) { return Ant.OS.announcer.osinfo(__("User abort the authentication")); }
return fn(gapi.auth2.getAuthInstance().isSignedIn.get());
}
, __("Authentication")
, { text: __("Would you like to login to {0}?", "Google Drive") });})
.catch(function(err) {
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot init {0}: {1}", "GAPI",err.error), (Ant.OS.API.throwe("OS.VFS")), err);
});
});
});
}
}
meta(f) {
const me = this;
return this.oninit(function() {
const q = Ant.OS.announcer.getMID();
if (G_CACHE[me.path]) { me.gid = G_CACHE[me.path].id; }
if (me.gid) {
//console.log "Gid exists ", me.gid
Ant.OS.API.loading(q, "GAPI");
return gapi.client.drive.files.get({
fileId: me.gid,
fields: me.fields()
})
.then(function(r) {
Ant.OS.API.loaded(q, "OK");
if (!r.result) { return; }
r.result.mime = r.result.mimeType;
return f(r);}).catch(function(err) {
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot get meta data for {0}", me.gid), (Ant.OS.API.throwe("OS.VFS")), err);
});
} else {
//console.log "Find file in ", me.parent()
const fp = me.parent().asFileHandle();
return fp.meta(function(d) {
const file = d.result;
const q1 = Ant.OS.announcer.getMID();
Ant.OS.API.loading(q1, "GAPI");
G_CACHE[fp.path] = { id: file.id, mime: file.mimeType };
return gapi.client.drive.files.list({
q: `name = '${me.basename}' and '${file.id}' in parents and trashed = false`,
fields: `files(${me.fields()})`
})
.then(function(r) {
//console.log r
Ant.OS.API.loaded(q1, "OK");
if (!r.result.files || !(r.result.files.length > 0)) { return; }
G_CACHE[me.path] = { id: r.result.files[0].id, mime: r.result.files[0].mimeType };
r.result.files[0].mime = r.result.files[0].mimeType;
return f({ result: r.result.files[0] });})
.catch(function(err) {
Ant.OS.API.loaded(q1, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot get meta data for {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err);
});
});
}
});
}
fields() {
return "webContentLink, id, name,mimeType,description, kind, parents, properties, iconLink, createdTime, modifiedTime, owners, permissions, fullFileExtension, fileExtension, size";
}
isFolder() {
return this.info.mimeType === "application/vnd.google-apps.folder";
}
save(id, m, f) {
const me = this;
const user = gapi.auth2.getAuthInstance().currentUser.get();
const oauthToken = user.getAuthResponse().access_token;
const q = Ant.OS.announcer.getMID();
const xhr = new XMLHttpRequest();
const url = 'https://www.googleapis.com/upload/drive/v3/files/' + id + '?uploadType=media';
xhr.open('PATCH', url);
xhr.setRequestHeader('Authorization', 'Bearer ' + oauthToken);
xhr.setRequestHeader('Content-Type', m);
xhr.setRequestHeader('Content-Encoding', 'base64');
xhr.setRequestHeader('Content-Transfer-Encoding', 'base64');
Ant.OS.API.loading(q, "GAPI");
const error = function(e, s) {
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot save : {0}", me.path), e, s);
};
xhr.onreadystatechange = function() {
if ( xhr.readyState === 4 ) {
if ( xhr.status === 200 ) {
Ant.OS.API.loaded(q, "OK");
return f({ result: JSON.parse(xhr.responseText) });
} else {
return error(xhr, xhr.status);
}
}
};
xhr.onerror = () => error(xhr, xhr.status);
if (m === "base64") { return xhr.send(me.cache.replace(/^data:[^;]+;base64,/g, "")); }
return this.sendB64(m, data => xhr.send(data.replace(/^data:[^;]+;base64,/g, "")));
}
getlink() {
if (this.ready) { return this.info.webContentLink; }
return undefined;
}
action(n, p, f) {
const me = this;
const q = Ant.OS.announcer.getMID();
Ant.OS.API.loading(q, "GAPI");
switch (n) {
case "read":
if (!this.info.id) { return; }
if (this.isFolder()) {
return gapi.client.drive.files.list({
q: `'${me.info.id}' in parents and trashed = false`,
fields: `files(${me.fields()})`
})
.then(function(r) {
Ant.OS.API.loaded(q, "OK");
if (!r.result.files) { return; }
for (let file of Array.from(r.result.files)) {
file.path = me.child(file.name);
file.mime = file.mimeType;
file.filename = file.name;
file.type = "file";
file.gid = file.id;
if (file.mimeType === "application/vnd.google-apps.folder") {
file.mime = "dir";
file.type = "dir";
file.size = 0;
}
G_CACHE[file.path] = { id: file.gid, mime: file.mime };
}
return f({ result: r.result.files });})
.catch(function(err) {
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot read : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err);
});
} else {
return gapi.client.drive.files.get({
fileId: me.info.id,
alt: 'media'
})
.then(function(r) {
Ant.OS.API.loaded(q, "OK");
if (p !== "binary") { return f(r.body); }
return f(r.body.asUint8Array());}).catch(function(err) {
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot read : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err);
});
}
case "mk":
if (!this.isFolder()) { return f({ error: __("{0} is not a directory", this.path) }); }
var meta = {
name: p,
parents: [this.info.id],
mimeType: 'application/vnd.google-apps.folder'
};
gapi.client.drive.files.create({
resource: meta,
fields: 'id'
})
.then(function(r) {
Ant.OS.API.loaded(q, "OK");
//console.log r
if (!r || !r.result) { return Ant.OS.announcer.oserror(__("VFS cannot create : {0}", p), (Ant.OS.API.throwe("OS.VFS")), r); }
G_CACHE[me.child(p)] = { id: r.result.id, mime: "dir" };
return f(r);}).catch(function(err) {
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot create : {0}", p), (Ant.OS.API.throwe("OS.VFS")), err);
});
return;
case "write":
var gid = undefined;
if (G_CACHE[me.path]) { gid = G_CACHE[me.path].id; }
if (gid) {
Ant.OS.API.loaded(q, "OK");
return this.save(gid, p, f);
} else {
const dir = this.parent().asFileHandle();
return dir.onready(function() {
meta = {
name: me.basename,
mimeType: p,
parents: [dir.info.id]
};
return gapi.client.drive.files.create({
resource: meta,
fields: 'id'
})
.then(function(r) {
Ant.OS.API.loaded(q, "OK");
if (!r || !r.result) { return Ant.OS.announcer.oserror(__("VFS cannot write : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), r); }
G_CACHE[me.path] = { id: r.result.id, mime: p };
return me.save(r.result.id, p, f);}).catch(function(err) {
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot write : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err);
});
});
}
case "upload":
if (!this.isFolder()) { return; }
//insert a temporal file selector
var o = ($('<input>')).attr('type', 'file').css("display", "none");
Ant.OS.API.loaded(q, "OK");
o.change(function() {
//Ant.OS.API.loading q, p
const fo = o[0].files[0];
const file = (me.child(fo.name)).asFileHandle();
file.cache = fo;
file.write(fo.type, f);
return o.remove();
});
//Ant.OS.API.loaded q, p, "OK"
//Ant.OS.API.loaded q, p, "FAIL"
return o.click();
case "remove":
if (!this.info.id) { return; }
return gapi.client.drive.files.delete({
fileId: me.info.id
})
.then(function(r) {
//console.log r
Ant.OS.API.loaded(q, "OK");
if (!r) { return Ant.OS.announcer.oserror(__("VFS cannot delete : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), r); }
G_CACHE[me.path] = null;
return f({ result: true });})
.catch(function(err) {
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot delete : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err);
});
case "publish":
Ant.OS.API.loaded(q, "OK");
return;
case "download":
return gapi.client.drive.files.get({
fileId: me.info.id,
alt: 'media'
})
.then(function(r) {
Ant.OS.API.loaded(q, "OK");
if (!r.body) { return Ant.OS.announcer.oserror(__("VFS cannot download file : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), r); }
let bytes = [];
for (let i = 0, end = r.body.length - 1, asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) {
bytes.push(r.body.charCodeAt(i));
}
bytes = new Uint8Array(bytes);
const blob = new Blob([bytes], { type: "octet/stream" });
return Ant.OS.API.saveblob(me.basename, blob);}).catch(function(err) {
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot download file : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err);
});
case "move":
var dest = p.asFileHandle().parent().asFileHandle();
return dest.onready(function() {
const previousParents = me.info.parents.join(',');
return gapi.client.drive.files.update({
fileId: me.info.id,
addParents: dest.info.id,
removeParents: previousParents,
fields: "id"
})
.then(function(r) {
Ant.OS.API.loaded(q, "OK");
if (!r) { return Ant.OS.announcer.oserror(__("VFS cannot move : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), r); }
return f(r);}).catch(function(err) {
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.oserror(__("VFS cannot move : {0}", me.gid), (Ant.OS.API.throwe("OS.VFS")), err);
});
}
, err => Ant.OS.API.loaded(q, "FAIL"));
default:
Ant.OS.API.loaded(q, "FAIL");
return Ant.OS.announcer.osfail(__("VFS unknown action: {0}", n), (Ant.OS.API.throwe("OS.VFS")), n);
}
}
}
self.OS.API.VFS.register("^gdv$", GoogleDriveHandle);
// search the cache for file
self.OS.API.onsearch("Google Drive", function(t) {
const arr = [];
const term = new RegExp(t, "i");
for (let k in G_CACHE) {
const v = G_CACHE[k];
if ((k.match(term)) || (v && v.mime.match(term))) {
const file = k.asFileHandle();
file.text = file.basename;
file.mime = v.mime;
file.iconclass = "fa fa-file";
if (file.mime === "dir") { file.iconclass = "fa fa-folder"; }
file.complex = true;
file.detail = [{ text: file.path }];
arr.push(file);
}
}
return arr;
});
self.OS.onexit("cleanUpGoogleDrive", function() {
G_CACHE = { "gdv://": { id: "root", mime: 'dir' } };
if (!Ant.OS.setting.VFS.gdrive || !Ant.OS.API.libready(Ant.OS.setting.VFS.gdrive.apilink)) { return; }
const auth2 = gapi.auth2.getAuthInstance();
if (!auth2) { return; }
if (auth2.isSignedIn.get()) {
let el;
return el = $('<iframe/>', {
src: 'https://www.google.com/accounts/Logout',
frameborder: 0,
onload() {
//console.log("disconnect")
return auth2.disconnect();
}
//$(this).remove()
});
}
});
//($ "body").append(el)

View File

@ -22,8 +22,18 @@
<head>
<title>AntOS webOS</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<link rel="icon" href="resources/themes/system/icons/antos-16x16.png" type="image/png" />
<link rel="preload" href="resources/themes/system/fonts/ubuntu-regular-webfont.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="resources/themes/system/fonts/ubuntu-italic-webfont.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="resources/themes/system/fonts/ubuntu-bolditalic-webfont.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="resources/themes/system/fonts/ubuntu-bold-webfont.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="resources/themes/system/fonts/fontawesome-webfont.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="resources/themes/system/fonts/bootstrap-icons.woff2" as="font" type="font/woff2" crossorigin>
<link href="resources/themes/system/system.css" rel="stylesheet">
<!-- default theme-->
<link href="resources/themes/system/antos.css" rel="stylesheet">
<link id="ostheme" rel="stylesheet" href="">
<script src="scripts/jquery-3.4.1.min.js"></script>
<script src="scripts/antos.js"></script>

View File

@ -4,7 +4,7 @@ libfiles =
cssfiles = main.css
copyfiles = scheme.html package.json
copyfiles = scheme.html package.json README.md
PKG_NAME=Files

View File

@ -0,0 +1,12 @@
# Files
Base AntOS file explorer application.
This application is included in the AntOS delivery as system application and
cannot be removed/uinstalled by regular user
## Change logs
- v0.1.10-b: improve file favorite behaviour
- v0.1.9-b: use "column"/"row" for UI direction names
- v0.1.8-b: use system css variables in theme
- v0.1.7-b: fix - file grid view double click event hanling on diffent cells of a row

View File

@ -1,48 +1,56 @@
/*afx-app-window[data-id ='files-app-window'] afx-list-view{
border-top:1px solid #A6A6A6;
}*/
afx-app-window[data-id ='files-app-window'] afx-list-view[data-id='favouri']{
border-right: 0;
/* border-top:1px solid #A6A6A6; */
padding:0;
}
afx-app-window[data-id ='files-app-window'] afx-resizer{
background-color: transparent;
/* border-left: 1px solid #cbcbcb; */
}
afx-app-window[data-id ='files-app-window'] afx-file-view afx-list-view i:before{
font-size: 32px;
font-weight: normal;
font-style: normal;
text-align: center;
}
afx-app-window[data-id ='files-app-window'] afx-list-view[data-id='favouri'] li{
background-color: transparent;
}
afx-app-window[data-id ='files-app-window'] afx-list-view[data-id='favouri'] li.selected {
background-color: #cecece;
color:#414339;
}
afx-app-window[data-id ='files-app-window'] .afx-window-top{
border-bottom: 0;
}
afx-app-window[data-id ='files-app-window'] afx-vbox[data-id = "nav-bar"]{
background-color: #dfdfdf;
}
afx-app-window[data-id ='files-app-window'] afx-grid-view afx-grid-row.grid_row_header div{
border-top:1px solid #A6A6A6;
}
afx-app-window[data-id ='files-app-window'] afx-grid-view afx-grid-row afx-label span {
white-space: nowrap;
/*
afx-app-window[data-id ='files-app-window'] afx-list-view[data-id='favouri'] li.selected {
background-color: var(--background-quaternary) !important;
color:var(--text-secondary) !important;
}
*/
/*
afx-app-window[data-id ='files-app-window'] afx-file-view {
background-color: var(--background-tertiary);
}
*/
afx-app-window[data-id ='files-app-window'] button{
height: 23px;
border-radius: 0;
padding-top:2px;
}
afx-app-window[data-id ='files-app-window'] afx-tab-bar[data-id ='path-nav'] afx-list-view > div.list-container > ul li{
padding: 0;
padding-right: 7px;
padding-left: 7px;
border: 0;
}
afx-app-window[data-id ='files-app-window'] input{
border-radius: 3px;
afx-app-window[data-id ='files-app-window'] afx-hbox[data-id ='nav-bar'] {
border-bottom: 1px solid var(--border-primary);
}
afx-app-window[data-id ='files-app-window'] afx-tab-bar[data-id ='path-nav'] afx-list-view > div.list-container > ul li::before{
content: "\F27B";
font-family: "bootstrap-icons";
font-style: normal;
/* font-size: 41px; */
display: block;
position: absolute;
color: var(--text-secondary);
right: -7px
}
afx-app-window[data-id ='files-app-window'] afx-tab-bar[data-id ='path-nav'] afx-list-view > div.list-container > ul > afx-list-item > li:hover{
color: var(--text-secondary);
}
afx-app-window[data-id ='files-app-window'] afx-tab-bar[data-id ='path-nav'] afx-list-view > div.list-container > ul > afx-list-item > li.selected {
background-color: transparent;
border: 0;
}

View File

@ -21,7 +21,7 @@ namespace OS {
interface FilesClipboardType {
cut: boolean;
file: API.VFS.BaseFileHandle;
files: API.VFS.BaseFileHandle[];
}
interface FilesViewType {
icon: boolean;
@ -37,7 +37,6 @@ namespace OS {
*/
export class Files extends BaseApplication {
private view: GUI.tag.FileViewTag;
private navinput: HTMLInputElement;
private navbar: GUI.tag.HBoxTag;
private currdir: API.VFS.BaseFileHandle;
private favo: GUI.tag.ListViewTag;
@ -50,20 +49,14 @@ namespace OS {
/**
*
* main entry point
*
* @returns
* @memberof Files
*/
main(): void {
this.view = this.find("fileview") as GUI.tag.FileViewTag;
this.navinput = this.find("navinput") as HTMLInputElement;
this.navbar = this.find("nav-bar") as GUI.tag.HBoxTag;
if (this.args && this.args.length > 0) {
this.currdir = this.args[0].path.asFileHandle();
} else {
this.currdir = "home://".asFileHandle();
}
this.favo = this.find("favouri") as GUI.tag.ListViewTag;
this.clipboard = undefined;
this.viewType = this._api.switcher("icon", "list", "tree");
@ -71,106 +64,107 @@ namespace OS {
this.view.contextmenuHandle = (e, m) => {
const file = this.view.selectedFile;
if (!file) {
return;
}
const apps = [];
if (file.type === "dir") {
file.mime = "dir";
}
for (let v of this._gui.appsByMime(file.mime)) {
apps.push({
text: v.text,
app: v.app,
icon: v.icon,
iconclass: v.iconclass,
});
}
let ctx_menu = [
{
this.mnFile(),
];
if(file)
{
ctx_menu.push(this.mnEdit());
if (file.type === "dir") {
file.mime = "dir";
}
for (let v of this._gui.appsByMime(file.mime)) {
apps.push({
text: v.text,
app: v.app,
icon: v.icon,
iconclass: v.iconclass,
});
}
ctx_menu.unshift( {
text: "__(Open with)",
nodes: apps,
onchildselect: (e: GUI.TagEventType<GUI.tag.MenuEventData>) => {
onchildselect: (e: GUI.TagEventType<GUI.tag.StackMenuEventData>) => {
if (!e) {
return;
}
const it = e.data.item.data;
return this._gui.launch(it.app, [file]);
},
},
this.mnFile(),
this.mnEdit(),
];
if(file.mime === "application/zip")
{
ctx_menu = ctx_menu.concat([
{
text: "__(Extract Here)",
onmenuselect: (e: GUI.TagEventType<GUI.tag.MenuEventData>) => {
if (!e) {
return;
}
API.VFS.extractZip(file.path,
(z) => new Promise((r,e) => r(file.path.asFileHandle().parent().path)))
.catch((err) => this.error(__("Unable to extract file"), err));
},
},
{
text: "__(Extract to)",
onmenuselect: async (e: GUI.TagEventType<GUI.tag.MenuEventData>) => {
if (!e) {
return;
}
try {
OS.GUI.dialogs.FileDialog.last_opened = this.currdir.path;
const d = await this.openDialog("FileDialog", {
title: __("Select extract destination"),
type: "dir",
file: file.path.replace(".zip","").asFileHandle()
});
const path = `${d.file.path}/${d.name}`;
await API.VFS.mkdirAll([path]);
await API.VFS.extractZip(file.path,
(z) => new Promise((r,e) => r(path)));
} catch (error) {
this.error(__("Unable to extract file"), error);
}
},
},
]);
}
else
{
ctx_menu.push(
{
text: "__(Compress)",
onmenuselect: async (e: GUI.TagEventType<GUI.tag.MenuEventData>) => {
if (!e) {
return;
}
try {
OS.GUI.dialogs.FileDialog.last_opened = this.currdir.path;
const d = await this.openDialog("FileDialog", {
title: __("Save compressed file to"),
type: "dir",
file: `${this.currdir.path}/${file.name}.zip`.asFileHandle()
});
if(d.name.trim() === "")
{
return this.error(__("Invalid file name"));
});
if(file.mime === "application/zip")
{
ctx_menu = ctx_menu.concat([
{
text: "__(Extract Here)",
onmenuselect: (e: GUI.TagEventType<GUI.tag.StackMenuEventData>) => {
if (!e) {
return;
}
API.VFS.extractZip(file.path,
(z) => new Promise((r,e) => r(file.path.asFileHandle().parent().path)))
.catch((err) => this.error(__("Unable to extract file"), err));
},
},
{
text: "__(Extract to)",
onmenuselect: async (e: GUI.TagEventType<GUI.tag.StackMenuEventData>) => {
if (!e) {
return;
}
try {
OS.GUI.dialogs.FileDialog.last_opened = this.currdir.path;
const d = await this.openDialog("FileDialog", {
title: __("Select extract destination"),
type: "dir",
file: file.path.replace(".zip","").asFileHandle()
});
const path = `${d.file.path}/${d.name}`;
await API.VFS.mkdirAll([path]);
await API.VFS.extractZip(file.path,
(z) => new Promise((r,e) => r(path)));
} catch (error) {
this.error(__("Unable to extract file"), error);
}
},
},
]);
}
else
{
ctx_menu.push(
{
text: "__(Compress)",
onmenuselect: async (e: GUI.TagEventType<GUI.tag.StackMenuEventData>) => {
if (!e) {
return;
}
try {
OS.GUI.dialogs.FileDialog.last_opened = this.currdir.path;
const d = await this.openDialog("FileDialog", {
title: __("Save compressed file to"),
type: "dir",
file: `${this.currdir.path}/${file.name}.zip`.asFileHandle()
});
if(d.name.trim() === "")
{
return this.error(__("Invalid file name"));
}
const path = `${d.file.path}/${d.name}`;
await API.VFS.mkar(file.path, path);
this.toast(__("Archive file created: {0}",path ));
} catch (error) {
this.error(__("Unable to compress file, folder"), error);
}
const path = `${d.file.path}/${d.name}`;
await API.VFS.mkar(file.path, path);
this.notify(__("Archive file created: {0}",path ));
} catch (error) {
this.error(__("Unable to compress file, folder"), error);
}
}
}
);
);
}
}
m.items = ctx_menu;
m.nodes = ctx_menu;
m.show(e);
};
@ -185,6 +179,10 @@ namespace OS {
};
this.favo.onlistselect = (e) => {
if(this.currdir.path.startsWith(e.data.item.data.path))
{
return;
}
return this.view.path = e.data.item.data.path;
};
@ -197,12 +195,6 @@ namespace OS {
return this.view.path = p.path;
};
$(this.navinput).keyup((e) => {
if (e.keyCode === 13) {
return this.view.path = $(this.navinput).val() as string;
}
}); //enter
this.view.fetch = (path) => {
return new Promise((resolve, reject) => {
let dir = path.asFileHandle();
@ -212,65 +204,121 @@ namespace OS {
if (d.error) {
return reject(d.error);
}
if (!dir.isRoot()) {
const p = dir.parent();
p.filename = "[..]";
p.type = "dir";
d.result.unshift(p);
}
this.currdir = dir;
$(this.navinput).val(dir.path);
(this.scheme as GUI.tag.WindowTag).apptitle = dir.path;
return resolve(d.result);
})
.catch((e) => reject(__e(e)));
});
};
const p_list = this.find<GUI.tag.TabBarTag>("path-nav");
p_list.ontabselect = (e) => {
const handle: API.VFS.BaseFileHandle = e.data.item.data.handle;
if(this.currdir.path == handle.path)
{
return;
}
this.view.path = handle.path;
}
this.view.onchdir = (e) => {
const dir = (e.data.path as string).asFileHandle();
this.currdir = dir;
if(dir.genealogy)
{
(this.scheme as GUI.tag.WindowTag).apptitle = dir.filename;
}
else
{
(this.scheme as GUI.tag.WindowTag).apptitle = `${dir.protocol}://`;
}
// update the path-nav
let base_vfs = `${dir.protocol}://`.asFileHandle();
let segments = [{
text: base_vfs.path,
handle: base_vfs
}]
if(dir.genealogy)
{
segments = segments.concat(dir.genealogy.map((e)=> {
base_vfs = `${base_vfs.path}/${e}`.asFileHandle();
return {
text: e,
handle: base_vfs
}
}));
}
p_list.items = segments;
p_list.scroll_to_end();
// update the current favo
const matched = this.favo.data.map((e,i) => {
return {
path: e.path,
index:i,
}
})
.filter(e => {
return dir.path.startsWith(e.path as string);
})
.sort((x,y) => {
if(x.path.length < y.path.length) return 1;
if(x == y) return 0;
return -1;
});
if(matched.length != 0)
{
// get the longest matched
this.favo.selected = matched[0].index;
}
}
this.vfs_event_flag = true;
this.view.ondragndrop = (e) => {
this.view.ondragndrop = async (e) => {
if (!e) {
return;
}
const src = e.data.from.data;
const src = e.data.from;
const des = e.data.to.data;
if (des.type === "file") {
return;
}
const file = src.path.asFileHandle();
// ask to confirm
const r = await this.ask({
title: __("Move files"),
text: __("Move selected file to {0}?", des.text)
});
if(!r)
{
return;
}
// disable the vfs event on
// we update it manually
this.vfs_event_flag = false;
return file
.move(`${des.path}/${file.basename}`)
.then(() => {
if (this.view.view === "icon") {
this.view.path = this.view.path;
} else {
this.view.update(file.parent().path);
this.view.update(des.path);
}
//reenable the vfs event
return (this.vfs_event_flag = true);
})
.catch((e: Error) => {
// reenable the vfs event
this.vfs_event_flag = true;
return this.error(
const promises = [];
for(const item of src)
{
let file = item.data.path.asFileHandle();
promises.push(
file.move(`${des.path}/${file.basename}`));
}
try{
await Promise.all(promises);
if (this.view.view === "tree") {
this.view.update(src[0].data.path.asFileHandle().parent().path);
this.view.update(des.path);
} else {
this.view.path = this.view.path;
}
}
catch(error)
{
this.error(
__(
"Unable to move: {0} -> {1}",
src.path,
"Unable to move files to: {0}",
des.path
),
e
error
);
});
}
this.vfs_event_flag = true;
};
// application setting
if (this.setting.sidebar === undefined) {
this.setting.sidebar = true;
}
if (this.setting.nav === undefined) {
this.setting.nav = true;
}
@ -280,37 +328,76 @@ namespace OS {
this.applyAllSetting();
// VFS mount point and event
const mntpoints = [];
for(let v of this.systemsetting.VFS.mountpoints)
{
mntpoints.push({
text: v.text,
path: v.path,
icon: v.icon,
iconclass: v.iconclass,
const mntpoints = this.systemsetting.VFS.mountpoints.map(e => {
return {
text: e.text,
path: e.path,
icon: e.icon,
iconclass: e.iconclass,
selected: false
});
}
}
});
this.favo.data = mntpoints;
//@favo.set "selected", -1
if (this.setting.view) {
this.view.view = this.setting.view;
}
this.subscribe("VFS", (d) => {
this.subscribe("VFS", (d: API.AnnouncementDataType<API.VFS.BaseFileHandle>) => {
if (!this.vfs_event_flag) {
return;
}
if (["read", "publish", "download"].includes(d.data.m)) {
if (["read", "publish", "download"].includes(d.message as string)) {
return;
}
if (
d.data.file.hash() === this.currdir.hash() ||
d.data.file.parent().hash() === this.currdir.hash()
d.u_data.hash() === this.currdir.hash() ||
d.u_data.parent().hash() === this.currdir.hash()
) {
return this.view.path = this.currdir.path;
}
});
// register responsive event
this.morphon(GUI.RESPONSIVE.MEDIUM, (fulfilled:boolean) => {
/**
* If the Window is bigger than medium size, then
* we enable the side bar
*
* otherwise, use the top bar
*/
const box = this.find("container") as GUI.tag.TileLayoutTag;
const nav = this.find("nav-bar") as GUI.tag.TileLayoutTag;
const fav = this.find("favouri") as GUI.tag.ListViewTag;
const resizer = this.find("resizer") as GUI.tag.ResizerTag;
if(fulfilled)
{
box.name = "hbox";
box.dir = "row";
nav.reversed = true;
nav.name = "vbox";
nav.dir = "column";
fav.dropdown = false;
resizer.dir = "row";
resizer.disable = false;
}
else
{
box.name = "vbox";
box.dir = "column";
nav.reversed = false;
nav.name = "hbox";
nav.dir = "row";
fav.dropdown = true;
resizer.dir = "column";
resizer.disable = true;
}
});
// bind keyboard shortcut
this.bindKey("CTRL-F", () =>
this.actionFile(`${this.name}-mkf`)
@ -346,6 +433,14 @@ namespace OS {
this.bindKey("CTRL-ALT-R", ()=>{
this.view.path = this.currdir.path;
});
this.bindKey("CTRL-B", () => {
if (this.currdir.isRoot()) {
return;
}
const p = this.currdir.parent();
this.favo.selected = -1;
return this.view.path = p.path;
});
(this.find("btgrid") as GUI.tag.ButtonTag).onbtclick = (e) => {
this.view.view = "icon";
this.viewType.icon = true;
@ -355,6 +450,28 @@ namespace OS {
this.view.view = "list";
this.viewType.list = true;
};
// enable or disable multi-select by CTRL key
$(this.scheme).on("keydown", (evt)=>{
if(evt.ctrlKey && evt.which == 17)
{
this.view.multiselect = true;
}
else
{
this.view.multiselect = false;
}
});
$(this.scheme).on("keyup", (evt)=>{
if(!evt.ctrlKey)
{
this.view.multiselect = false;
}
});
if (this.args && this.args.length > 0) {
this.currdir = this.args[0].path.asFileHandle();
} else {
this.currdir = "home://".asFileHandle();
}
this.view.path = this.currdir.path;
}
@ -365,13 +482,10 @@ namespace OS {
return this.view.showhidden = this.setting.showhidden;
case "nav":
return this.toggleNav(this.setting.nav);
case "sidebar":
return this.toggleSidebar(this.setting.sidebar);
}
}
private mnFile(): GUI.BasicItemType{
//console.log file
const arr: GUI.BasicItemType = {
text: "__(File)",
nodes: [
@ -405,7 +519,7 @@ namespace OS {
shortcut: "C-I",
},
],
onchildselect: (e: GUI.TagEventType<GUI.tag.MenuEventData>) =>
onchildselect: (e: GUI.TagEventType<GUI.tag.StackMenuEventData>) =>
this.actionFile(e.data.item.data.dataid),
};
return arr;
@ -440,7 +554,7 @@ namespace OS {
shortcut: "C-P",
},
],
onchildselect: (e: GUI.TagEventType<GUI.tag.MenuEventData>) =>
onchildselect: (e: GUI.TagEventType<GUI.tag.StackMenuEventData>) =>
this.actionEdit(e.data.item.data.dataid),
};
}
@ -451,17 +565,15 @@ namespace OS {
{
text: "__(View)",
nodes: [
{
text: "__(Toggle responsive)",
dataid: `${this.name}-responsive`,
},
{
text: "__(Refresh)",
dataid: `${this.name}-refresh`,
shortcut: "C-A-R"
},
{
text: "__(Sidebar)",
switch: true,
checked: this.setting.sidebar,
dataid: `${this.name}-side`,
},
{
text: "__(Navigation bar)",
switch: true,
@ -499,28 +611,19 @@ namespace OS {
type: "tree",
},
],
onchildselect: (e: GUI.TagEventType<GUI.tag.MenuEventData>) => {
onchildselect: (e: GUI.TagEventType<GUI.tag.StackMenuEventData>) => {
const { type } = e.data.item.data;
this.view.view = type;
return (this.viewType[type] = true);
},
},
],
onchildselect: (e: GUI.TagEventType<GUI.tag.MenuEventData>) => this.actionView(e),
onchildselect: (e: GUI.TagEventType<GUI.tag.StackMenuEventData>) => this.actionView(e),
},
];
return menu;
}
private toggleSidebar(b: boolean): void {
if (b) {
$(this.favo).show();
} else {
$(this.favo).hide();
}
return this.trigger("resize");
}
private toggleNav(b: boolean): void {
if (b) {
$(this.navbar).show();
@ -530,22 +633,22 @@ namespace OS {
return this.trigger("resize");
}
private actionView(e: GUI.TagEventType<GUI.tag.MenuEventData>): void{
private actionView(e: GUI.TagEventType<GUI.tag.StackMenuEventData>): void{
const data = e.data.item.data;
switch (data.dataid) {
case `${this.name}-hidden`:
//@.view.set "showhidden", e.item.data.checked
return this.registry("showhidden", data.checked);
this.setting.showhidden = data.checked;
//@.setting.showhidden = e.item.data.checked
case `${this.name}-refresh`:
this.view.path = this.currdir.path;
return;
case `${this.name}-side`:
return this.registry("sidebar", data.checked);
//@setting.sidebar = e.item.data.checked
//@toggleSidebar e.item.data.checked
case `${this.name}-responsive`:
const win = (this.scheme as GUI.tag.WindowTag);
win.responsive = !win.responsive;
return;
case `${this.name}-nav`:
return this.registry("nav", data.checked);
this.setting.nav = data.checked;
}
}
//@setting.nav = e.item.data.checked
@ -585,20 +688,22 @@ namespace OS {
title: "__(Delete)",
iconclass: "fa fa-question-circle",
text: __(
"Do you really want to delete: {0}?",
file.filename
"Do you really want to delete selected files?"
),
}).then(async (d) => {
if (!d) {
return;
}
const promises = [];
for(const f of this.view.selectedFiles)
{
promises.push(f.path.asFileHandle().remove());
}
try {
return file.path
.asFileHandle()
.remove();
await Promise.all(promises);
}
catch (e) {
return this.error(__("Fail to delete: {0}", file.path), e);
return this.error(__("Fail to delete selected files"), e);
}
});
break;
@ -609,9 +714,9 @@ namespace OS {
}
this.clipboard = {
cut: true,
file: file.path.asFileHandle(),
files: this.view.selectedFiles.map(x => x.path.asFileHandle()),
};
return this.notify(__("File {0} cut", file.filename));
return this.toast(__("{0} files cut", this.clipboard.files.length));
case `${this.name}-copy`:
if (!file) {
@ -619,10 +724,10 @@ namespace OS {
}
this.clipboard = {
cut: false,
file: file.path.asFileHandle(),
files: this.view.selectedFiles.map(x => x.path.asFileHandle()),
};
return this.notify(
__("File {0} copied", file.filename)
return this.toast(
__("{0} files copied", this.clipboard.files.length)
);
case `${this.name}-paste`:
@ -630,29 +735,33 @@ namespace OS {
return;
}
if (this.clipboard.cut) {
this.clipboard.file
.move(
`${this.currdir.path}/${this.clipboard.file.basename}`
)
const promises = [];
for(const file of this.clipboard.files)
{
promises.push(file.move(
`${this.currdir.path}/${file.basename}`
));
}
Promise.all(promises)
.then((r) => {
return (this.clipboard = undefined);
})
.catch((e) => {
return this.error(
__(
"Fail to paste: {0}",
this.clipboard.file.path
"Fail to paste to: {0}",
this.currdir.path
),
e
);
});
} else {
API.VFS.copy([this.clipboard.file.path],this.currdir.path)
API.VFS.copy(this.clipboard.files.map(x => x.path),this.currdir.path)
.then(() => {
return (this.clipboard = undefined);
})
.catch((e) => {
return this.error(__("Fail to paste: {0}", this.clipboard.file.path), e);
return this.error(__("Fail to paste to: {0}", this.currdir.path), e);
});
}
break;
@ -716,8 +825,7 @@ namespace OS {
.publish()
.then((r) => {
return this.notify(
__("Shared url: {0}", r.result)
);
__("Shared url: {0}", r.result));
})
.catch((e) => {
return this.error(
@ -727,7 +835,7 @@ namespace OS {
});
break;
case `${this.name}-download`:
if (file.type !== "file") {
if (!file || file.type !== "file") {
return;
}
file.path

View File

@ -6,7 +6,7 @@
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com"
},
"version":"0.1.3-a",
"version":"0.1.11-b",
"category":"System",
"iconclass":"fa fa-hdd-o",
"mimes":["dir"],

View File

@ -1,19 +1,17 @@
<afx-app-window data-id = "files-app-window" apptitle="Files" width="600" height="400">
<afx-vbox>
<afx-hbox data-height = "23" data-id = "nav-bar">
<afx-button data-width = "23" data-id = "btback" iconclass = "fa fa-arrow-left"></afx-button>
<input type = "text" data-id = "navinput"></input>
<div data-width = "2"></div>
<afx-button data-width = "23" data-id = "btgrid" iconclass = "fa fa-th"></afx-button>
<div data-width = "2"></div>
<afx-button data-width = "23" data-id = "btlist" iconclass = "fa fa-th-list"></afx-button>
</afx-hbox>
<div data-height="5"></div>
<afx-hbox>
<afx-list-view data-id = "favouri" data-width = "150" min-width="100">
</afx-list-view>
<afx-resizer data-width = "3" ></afx-resizer>
<afx-app-window data-id = "files-app-window" apptitle="Files" width="600" height="500" responsive="true">
<afx-tile data-id="container" name="vbox" dir="column">
<afx-tile data-height = "35" min-width="120" data-width="180" data-id = "nav-bar" name="hbox" dir="row">
<afx-list-view data-id = "favouri" dropdown="true" focusable="true"></afx-list-view>
</afx-tile>
<afx-resizer data-id="resizer" dir="column" disable="true" data-width="3" data-height="0"></afx-resizer>
<afx-vbox>
<afx-hbox data-width="120" data-height="35" data-id="nav-bar">
<afx-tab-bar data-id = "path-nav" dir="row" ></afx-tab-bar>
<afx-button data-width = "40" data-id = "btback" iconclass = "fa fa-arrow-up"></afx-button>
<afx-button data-width = "40" data-id = "btgrid" iconclass = "fa fa-th"></afx-button>
<afx-button data-width = "40" data-id = "btlist" iconclass = "fa fa-th-list"></afx-button>
</afx-hbox>
<afx-file-view data-id = "fileview"></afx-file-view>
</afx-hbox>
</afx-vbox>
</afx-vbox>
</afx-tile>
</afx-app-window>

View File

@ -4,7 +4,7 @@ libfiles =
cssfiles = main.css
copyfiles = scheme.html package.json
copyfiles = scheme.html package.json README.md
PKG_NAME=MarketPlace

View File

@ -0,0 +1,11 @@
# Market Place
AntOS original application store.
This application is icluded in the AntOS delivery
and cannot be removed/uinstalled by regular user
## Change logs
- 0.2.8-b: use system css variables in theme
- 0.2.7-b: only launch application
- 0.2.6-b: improve install process
- v0.2.5-b: add README.md

View File

@ -5,23 +5,7 @@ afx-app-window[data-id="marketplace-win"] afx-list-view[data-id='repo'] div.list
afx-app-window[data-id="marketplace-win"] afx-vbox[data-id='container'] div[data-id="desc-container"]{
overflow-y: auto;
overflow-x: none;
}
afx-app-window[data-id="marketplace-win"] afx-vbox[data-id='container'] afx-hbox {
padding-left: 10px;
}
afx-app-window[data-id="marketplace-win"] afx-label[data-id='appname'] i.label-text{
font-weight: bold;
font-size: 20px;
padding: 10px;
}
afx-app-window[data-id="marketplace-win"] div[data-id='appname']:before {
content: "\f085";
font-family: "FontAwesome";
font-size: 25px;
font-style: normal;
margin-right: 10px;
overflow-x: hidden;
}
afx-app-window[data-id="marketplace-win"] p[data-id='app-desc'] {
text-align: justify;
@ -38,9 +22,11 @@ afx-app-window[data-id="marketplace-win"] ul[data-id='app-detail'] {
padding-left:10px;
display: table;
margin: 0;
font-size: 13px;
}
afx-app-window[data-id="marketplace-win"] afx-hbox[data-id="search-container"] {
border-bottom: 1px solid #afafaf;
afx-app-window[data-id="marketplace-win"] afx-tab-bar {
border-top: 1px solid var(--border-quaternary);
border-bottom: 1px solid var(--border-quaternary);
}
afx-app-window[data-id="marketplace-win"] afx-hbox[data-id="search-container"] input{
border: 0;
@ -48,13 +34,14 @@ afx-app-window[data-id="marketplace-win"] afx-hbox[data-id="search-container"] i
}
afx-app-window[data-id="marketplace-win"] div[data-id="searchicon"]:before{
content: "\f002";
display: block;
background-color:transparent;
color:#afafaf;
color: var(--text-disable);
font-family: "FontAwesome";
padding-top: 3px;
padding-left:3px;
/* font-size: 25px; */
padding-left:5px;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
afx-app-window[data-id="marketplace-win"] ul[data-id='app-detail'] li{
padding:0;
@ -70,9 +57,13 @@ afx-app-window[data-id="marketplace-win"] ul[data-id='app-detail'] span{
afx-app-window[data-id="marketplace-win"] span.info-header{
font-weight: bold;
}
afx-app-window[data-id="marketplace-win"] span.info-detail
{
word-break: break-word;
}
afx-app-window[data-id="marketplace-win"] afx-label[data-id="vstat"] {
font-size: 11px;
color: chocolate;
color: var(--text-warning);
}
afx-app-window[data-id="marketplace-win"] p.stat {
margin: 0;
@ -82,4 +73,78 @@ afx-app-window[data-id="marketplace-win"] p.stat {
afx-app-window[data-id = "repository-dialog-win"] afx-list-view[data-id="repo-list"] ul.complex-content > li{
font-size: 11px;
font-style: italic;
}
afx-app-window[data-id="marketplace-win"] afx-tab-bar[data-id="catlist"] i.label-text
{
font-weight: bold;
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"] > div.list-container {
margin: 0 auto;
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"] > div.list-container > ul
{
display: flex;
flex-flow: row wrap;
justify-content: center;
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"] > div.list-container > ul li{
/*display: block;*/
width: 90px;
text-align: center;
font-size: 40px;
/*justify-content: normal !important;*/
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"] i{
display: block;
width: 100%;
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"] i.label-text{
word-break: break-word;
font-size: 14px;
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"] i.icon-style {
width: 40px;
height: 40px;
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"] afx-label span
{
flex-direction: column;
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"] > div.list-container > ul li:hover {
background-color: transparent;
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"]> div.list-container > ul .afx-list-item li
{
background-color: transparent;
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"] > div.list-container > ul .afx-list-item li:hover
{
background-color: var(--item-bg-hover);
color: var(--text-primary);
border-radius: 10px;
}
afx-app-window[data-id="marketplace-win"] afx-list-view[data-id="applist"] > div.list-container > ul .afx-list-item li.selected
{
background-color: var(--item-bg-active);
color:var(--text-tertiary);
border-radius: 10px;
}
afx-app-window[data-id="marketplace-win"] afx-hbox[data-id="app-header"] afx-button button
{
border-radius: 0;
}
afx-app-window[data-id="marketplace-win"] afx-hbox[data-id="app-header"]
{
border-radius: 0;
border-bottom: 1px solid var(--border-quaternary);
}
afx-app-window[data-id="marketplace-win"] afx-hbox[data-id="app-header"] afx-button[data-id="appname"] i.label-text
{
font-weight: bold;
}

View File

@ -24,9 +24,9 @@ namespace OS {
private installdir: string;
private apps_meta: GenericObject<any>;
private applist: GUI.tag.ListViewTag;
private catlist: GUI.tag.ListViewTag;
private catlist: GUI.tag.TabBarTag;
private container: GUI.tag.VBoxTag;
private appname: GUI.tag.LabelTag;
private appname: GUI.tag.ButtonTag;
private appdetail: HTMLUListElement;
private appdesc: HTMLParagraphElement;
private btinstall: GUI.tag.ButtonTag;
@ -39,21 +39,37 @@ namespace OS {
}
main(): void {
const stack_panel = this.find("stack-panel") as GUI.tag.StackPanelTag;
this.container = this.find("container") as GUI.tag.VBoxTag;
this.appname = this.find("appname") as GUI.tag.ButtonTag;
this.appdesc = this.find("app-desc") as HTMLParagraphElement;
this.appdetail = this.find("app-detail") as HTMLUListElement;
this.btinstall = this.find("bt-install") as GUI.tag.ButtonTag;
this.btremove = this.find("bt-remove") as GUI.tag.ButtonTag;
this.btexec = this.find("bt-exec") as GUI.tag.ButtonTag;
this.searchbox = this.find("searchbox") as HTMLInputElement;
this.appname.onbtclick = (_) => {
this.applist.selected = -1;
stack_panel.navigateBack();
}
this.installdir = this.systemsetting.system.pkgpaths.user;
// test repository
this.apps_meta = [];
this.apps_meta = {};
this.applist = this.find("applist") as GUI.tag.ListViewTag;
this.catlist = this.find("catlist") as GUI.tag.ListViewTag;
this.catlist = this.find("catlist") as GUI.tag.TabBarTag;
this.applist.onlistselect = (e) => {
const data = e.data.item.data;
return this.appDetail(data);
this.appDetail(data);
stack_panel.navigateNext();
};
this.catlist.onlistselect = (e) => {
const selected = this.catlist.selected;
this.catlist.ontabselect = (e) => {
const selected = this.catlist.selected as number;
if(selected < 0)
return;
if(selected === 0)
{
return this.resetAppList();
@ -93,14 +109,7 @@ namespace OS {
this.applist.data = result;
};
this.container = this.find("container") as GUI.tag.VBoxTag;
this.appname = this.find("appname") as GUI.tag.LabelTag;
this.appdesc = this.find("app-desc") as HTMLParagraphElement;
this.appdetail = this.find("app-detail") as HTMLUListElement;
this.btinstall = this.find("bt-install") as GUI.tag.ButtonTag;
this.btremove = this.find("bt-remove") as GUI.tag.ButtonTag;
this.btexec = this.find("bt-exec") as GUI.tag.ButtonTag;
this.searchbox = this.find("searchbox") as HTMLInputElement;
$(this.container).css("visibility", "hidden");
this.btexec.onbtclick = (_e) => {
const el = this.applist.selectedItem;
@ -108,8 +117,8 @@ namespace OS {
return;
}
const app = el.data;
if (app.pkgname) {
return this._gui.launch(app.pkgname, []);
if (app.app) {
return this._gui.launch(app.app, []);
}
};
@ -117,10 +126,10 @@ namespace OS {
try {
if (this.btinstall.data.dirty) {
await this.updatePackage();
return this.notify(__("Package updated"));
return this.toast(__("Package updated"));
}
const n = await this.remoteInstall();
return this.notify(__("Package installed: {0}", n));
return this.toast(__("Package installed: {0}", n));
} catch (error) {
return this.error(error.toString(), error);
}
@ -129,7 +138,7 @@ namespace OS {
this.btremove.onbtclick = async () => {
try {
await this.uninstall();
return this.notify(__("Packaged uninstalled"));
return this.toast(__("Packaged uninstalled"));
} catch (e) {
return this.error(e.toString(), e);
}
@ -173,14 +182,18 @@ namespace OS {
switch (e.which) {
case 37:
return e.preventDefault();
/*
case 38:
this.applist.selectPrev();
return e.preventDefault();
*/
case 39:
return e.preventDefault();
/*
case 40:
this.applist.selectNext();
return e.preventDefault();
*/
case 13:
return e.preventDefault();
default:
@ -273,6 +286,7 @@ namespace OS {
if (this.apps_meta[name]) {
pkg.icon = this.apps_meta[name].icon;
pkg.iconclass = this.apps_meta[name].iconclass;
pkg.app = this.apps_meta[name].app;
}
this.apps_meta[name] = pkg;
}
@ -325,10 +339,28 @@ namespace OS {
iconclass: "bi bi-gear-wide"
});
});
this.catlist.data = cat_list_data;
this.catlist.items = cat_list_data;
this.catlist.selected = 0;
}
private add_meta_from(k:string, v: API.PackageMetaType)
{
const mt = {
pkgname: v.pkgname ? v.pkgname : v.app,
app: v.app,
name: v.name,
text: `${v.name} ${v.version}`,
icon: v.icon,
iconclass: v.iconclass,
category: v.category,
author: v.info.author,
version: v.version,
description: `${v.path}/README.md`,
dependencies: v.dependencies ? Array.from(v.dependencies) : [],
dependBy: []
};
this.apps_meta[`${k}@${v.version}`] = mt;
return mt;
}
fetchApps(): Promise<GenericObject<any>> {
return new Promise((resolve, _reject) => {
let v: API.PackageMetaType;
@ -336,19 +368,7 @@ namespace OS {
const pkgcache = this.systemsetting.system.packages;
for (let k in pkgcache) {
v = pkgcache[k];
this.apps_meta[`${k}@${v.version}`] = {
pkgname: v.pkgname ? v.pkgname : v.app,
name: v.name,
text: `${v.name} ${v.version}`,
icon: v.icon,
iconclass: v.iconclass,
category: v.category,
author: v.info.author,
version: v.version,
description: `${v.path}/README.md`,
dependencies: v.dependencies ? Array.from(v.dependencies) : [],
dependBy: []
};
this.add_meta_from(k,v);
}
const list: string[] = []
@ -368,6 +388,8 @@ namespace OS {
this.appname.text = d.name;
const status = this.find("vstat") as GUI.tag.LabelTag;
status.text = "";
$(this.appdesc).empty();
$(this.appdetail).empty();
if (d.description) {
d.description
.asFileHandle()
@ -379,13 +401,11 @@ namespace OS {
);
})
.catch((_e) => {
this.notify(
__("Unable to read package description")
this.error(
__("Unable to read package description"), _e
);
return $(this.appdesc).empty();
});
} else {
$(this.appdesc).empty();
}
const pkgcache = this.systemsetting.system.packages;
this.btinstall.text = "__(Install)";
@ -416,7 +436,6 @@ namespace OS {
$(this.btexec).hide();
}
$(this.appdetail).empty();
for (let k in d) {
const v = d[k];
if (k !== "name" && k !== "description" && k !== "domel") {
@ -425,7 +444,7 @@ namespace OS {
.append(
$("<span class= 'info-header'>").html(k)
)
.append($("<span>").html(v))
.append($("<span class = 'info-detail'>").html(v))
);
}
}
@ -448,7 +467,7 @@ namespace OS {
},
],
onchildselect: (
e: GUI.TagEventType<GUI.tag.MenuEventData>
e: GUI.TagEventType<GUI.tag.StackMenuEventData>
) => {
return this.menuOptionsHandle(e.data.item.data.id);
},
@ -468,7 +487,7 @@ namespace OS {
case "install":
this.localInstall()
.then((n) => {
return this.notify(
return this.toast(
__("Package installed: {0}", n)
);
})
@ -551,7 +570,8 @@ namespace OS {
return reject(this._api.throwe(__("Unable to find package: {0}", pkgname)));
}
try {
const n = await this.install(meta.download + "?_=" + new Date().getTime(), meta);
const mt = await this.install(meta.download + "?_=" + new Date().getTime(), meta);
meta.app = mt.app;
return resolve(meta);
} catch (e_1) {
return reject(__e(e_1));
@ -581,7 +601,7 @@ namespace OS {
const dep = this.checkDependencies(pkgname);
if (dep.notfound.size != 0) {
this.openDialog("TextDialog", {
disable: true,
disable: false,
title: __("Unresolved dependencies"),
value: __(
"Unable to install: The package `{0}` depends on these packages, but they are not found:\n{1}",
@ -621,13 +641,19 @@ namespace OS {
mimes: [".*/zip"],
});
const n = await this.install(d.file.path);
const name = n.pkgname?n.pkgname:n.app;
const apps = this.applist.data.map(
(v) => v.pkgname
);
const idx = apps.indexOf(n);
const idx = apps.indexOf(name);
if (idx >= 0) {
this.applist.selected = idx;
}
else
{
const mt = this.add_meta_from(name,n);
this.appDetail(mt);
}
return resolve(n.name);
} catch (error) {
reject(__e(error));
@ -638,7 +664,7 @@ namespace OS {
private install(
zfile: string,
meta?: GenericObject<any>
): Promise<GenericObject<any>> {
): Promise<API.PackageMetaType> {
return new Promise(async (resolve, reject) => {
try {
let v: API.PackageMetaType;
@ -657,22 +683,6 @@ namespace OS {
});
});
const app_meta = {
pkgname: v.pkgname ? v.pkgname : v.app,
name: v.name,
text: v.name,
icon: v.icon,
iconclass: v.iconclass,
category: v.category,
author: v.info.author,
version: v.version,
description: meta
? meta.description
: undefined,
download: meta
? meta.download
: undefined,
};
v.text = v.name;
v.filename = v.pkgname ? v.pkgname : v.app;
v.type = "app";
@ -684,11 +694,15 @@ namespace OS {
v.iconclass =
"fa fa-adn";
}
if(v.icon)
{
v.icon = `${pth}/${v.icon}`;
}
v.path = pth;
this.systemsetting.system.packages[
v.pkgname ? v.pkgname : v.app
] = v;
return resolve(app_meta);
return resolve(v);
} catch (error) {
reject(__e(error));
}
@ -704,22 +718,23 @@ namespace OS {
private uninstallPkg(pkgname: string): Promise<any> {
return new Promise(async (resolve, reject) => {
const meta = this.apps_meta[pkgname];
if (!meta) {
return reject(this._api.throwe(__("Unable to find application meta-data: {0}", pkgname)));
}
const app = this.systemsetting.system.packages[meta.pkgname];
if (!app) {
return reject(this._api.throwe(__("Application {0} is not installed", pkgname)));
}
// got the app meta
try {
if (!meta) {
throw __("Unable to find application meta-data: {0}", pkgname).__();
}
const app = this.systemsetting.system.packages[meta.pkgname];
if (!app) {
throw __("Application {0} is not installed", pkgname).__();
}
const r = await app.path
.asFileHandle()
.remove();
if (r.error) {
return reject(this._api.throwe(__("Cannot uninstall package: {0}", r.error)));
throw __("Cannot uninstall package: {0}", r.error).__();
}
this.notify(__("Package uninstalled"));
this.toast(__("Package uninstalled"));
// stop all the services if any
if (app.services) {
for (let srv of Array.from(app.services)) {
@ -727,7 +742,7 @@ namespace OS {
}
}
delete this.systemsetting.system.packages[meta.pkgname];
this._gui.unloadApp(meta.pkgname);
this._gui.unloadApp(meta.pkgname, true);
if (meta.download) {
this.appDetail(meta);
}
@ -735,6 +750,7 @@ namespace OS {
if (meta.domel)
this.applist.delete(meta.domel);
$(this.container).css("visibility", "hidden");
delete this.apps_meta[pkgname];
}
return resolve(meta);
}
@ -771,7 +787,7 @@ namespace OS {
}
this.bulkUninstall([...dep.uninstall])
.then((_b) => {
this.notify(__("Uninstall successfully"));
this.toast(__("Uninstall successfully"));
})
.catch((err) => {
this.error(__("Unable to uninstall package(s): {0}", err.toString()), err);

View File

@ -7,7 +7,7 @@
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com"
},
"version":"0.2.4-a",
"version":"0.2.8-b",
"category":"System",
"iconclass":"fa fa-shopping-bag",
"mimes":["none"],

View File

@ -1,31 +1,39 @@
<afx-app-window data-id = "marketplace-win" apptitle="MarketPlace" width="650" height="400">
<afx-hbox >
<afx-vbox data-width = "350">
<afx-hbox data-height= "23" data-id="search-container">
<div data-width="17" data-id="searchicon"></div>
<input data-id = "searchbox" ></input>
</afx-hbox>
<afx-hbox>
<afx-list-view data-id = "catlist" dropdown = "false" data-width="35%"></afx-list-view>
<afx-resizer data-width = "3" ></afx-resizer>
<afx-list-view data-id = "applist" dropdown = "false" ></afx-list-view>
</afx-hbox>
</afx-vbox>
<afx-resizer data-width = "3" ></afx-resizer>
<afx-vbox data-id = "container">
<afx-label data-id = "appname" data-height = "25"></afx-label>
<afx-hbox data-height = "50">
<div style = "text-align:left;">
<afx-button data-id = "bt-remove" text = "__(Uninstall)"></afx-button>
<afx-button data-id = "bt-exec" text = "__(Launch)"></afx-button>
<afx-button data-id = "bt-install" text = "__(Install)" ></afx-button>
<p class="stat"><afx-label data-id="vstat"></afx-label></p>
</div>
<afx-app-window data-id = "marketplace-win" apptitle="MarketPlace" width="650" height="500">
<afx-vbox >
<afx-stack-panel data-id = "stack-panel" dir = "column" tabbarheight= "40">
<afx-vbox>
<afx-hbox data-height= "30" data-id="search-container">
<div data-width="17" data-id="searchicon"></div>
<input data-id = "searchbox" ></input>
</afx-hbox>
<div data-id="desc-container">
<p data-id = "app-desc"></p>
<ul data-id = "app-detail"></ul>
</div>
</afx-vbox>
<afx-tab-bar data-id = "catlist" data-height="45"></afx-tab-bar>
<afx-list-view data-id = "applist" dropdown = "false"></afx-list-view>
</afx-vbox>
<afx-hbox>
<afx-vbox data-id = "container">
<afx-hbox data-height = "35" data-id="app-header">
<afx-button data-id = "appname" iconclass = "bi bi-backspace-fill"></afx-button>
<div style = "display: flex;justify-content: flex-end;">
<afx-button data-id = "bt-remove" text = "__(Uninstall)" iconclass = "bi bi-trash-fill"></afx-button>
<afx-button data-id = "bt-exec" text = "__(Launch)" iconclass = "fa fa-cog"></afx-button>
<afx-button data-id = "bt-install" text = "__(Install)" iconclass = "bi bi-cloud-download-fill" ></afx-button>
</div>
</afx-hbox>
<p class="stat" data-height="15"><afx-label data-id="vstat"></afx-label></p>
<div data-id="desc-container">
<p data-id = "app-desc"></p>
<ul data-id = "app-detail"></ul>
</div>
</afx-vbox>
</afx-hbox>
</afx-stack-panel>
</afx-hbox>
</afx-app-window>

View File

@ -4,3 +4,7 @@ Defaut text editor which is included in each AntOS release.
It has very barebone features: open/edit/save text file.
Text/Code editor with fancy features can be optionally installed via the Market Place
## Change logs
- v0.1.2-b: use system css variables in theme
- v0.1.1-b: update README

View File

@ -3,5 +3,5 @@ afx-app-window[data-id="NotePad"] textarea[data-id="editor"]
margin: 0;
padding:10px;
border: 0;
background-color: transparent;
background-color: var(--background-tertiary);
}

View File

@ -7,7 +7,7 @@
"author": "Xuan Sang LE",
"email": "mrsang@iohub.dev"
},
"version": "0.1.0-b",
"version": "0.1.2-b",
"category": "Utility",
"iconclass": "bi bi-pen",
"mimes": [

View File

@ -89,7 +89,7 @@ namespace OS {
this.applist.buttons = [
{
text: "+",
iconclass: "bi bi-plus",
onbtclick: () => {
const apps = (() => {
const result = [];
@ -101,6 +101,7 @@ namespace OS {
text: v.name,
app: k,
iconclass: v.iconclass,
icon: v.icon
});
}
}
@ -127,7 +128,7 @@ namespace OS {
},
},
{
text: "-",
iconclass: "bi bi-dash",
onbtclick: () => {
const item = this.applist.selectedItem;
if (!item) {
@ -160,7 +161,7 @@ namespace OS {
}
return result1;
})();
announcer.ostrigger("app-pinned", this.applist.data);
announcer.ostrigger("APP-PINNED", "APP-PINNED", this.applist.data);
}
}
App.AppAndServiceHandle = AppAndServiceHandle;

View File

@ -67,7 +67,7 @@ namespace OS {
this.wplist.buttons = [
{
text: "+",
iconclass: "bi bi-plus",
onbtclick: (e) => {
return this.parent
.openDialog("FileDialog", {

View File

@ -1,10 +1,10 @@
module_files = main.js AppearanceHandle.js AppAndServiceHandle.js VFSHandle.js LocaleHandle.js StartupHandle.js
module_files = main.js AppearanceHandle.js AppAndServiceHandle.js VFSHandle.js LocaleHandle.js StartupHandle.js VersionsHandle.js
libfiles =
cssfiles = main.css
copyfiles = scheme.html package.json
copyfiles = scheme.html package.json README.md
PKG_NAME=Setting

View File

@ -0,0 +1,16 @@
# Setting: AntOS system setting and configuration
GUI based system setting application for AntOS.
In-depth system settings can be found and modified in .settings.json file stored in HOME directory
of current user.
CAUTION:without using the Setting application, users can modify .settings.json with their own risk.
In case of system anormaly after the modification, the system settings can be reset to default
by simply removing the setting file
## Change logs
- v0.1.5-b: fix VFS setting dialog bugs
- v0.1.4-b: use system css variables in theme
- v0.1.2-b: minor bug fix on UI
- v0.1.2-b: add README

View File

@ -47,7 +47,7 @@ namespace OS {
this.applist = this.find("applist") as GUI.tag.ListViewTag;
this.srvlist.buttons = [
{
text: "+",
iconclass: "bi bi-plus",
onbtclick: () => {
let services = [];
for (var k in setting.system.packages) {
@ -75,7 +75,7 @@ namespace OS {
},
},
{
text: "-",
iconclass: "bi bi-dash",
onbtclick: () => {
const item = this.srvlist.selectedItem;
if (!item) {
@ -90,7 +90,7 @@ namespace OS {
this.applist.buttons = [
{
text: "+",
iconclass: "bi bi-plus",
onbtclick: () => {
const apps = (() => {
const result = [];
@ -102,6 +102,7 @@ namespace OS {
text: v.name,
app: k,
iconclass: v.iconclass,
icon: v.icon
});
}
}
@ -126,7 +127,7 @@ namespace OS {
},
},
{
text: "-",
iconclass: "bi bi-dash",
onbtclick: () => {
const item = this.applist.selectedItem;
if (!item) {

View File

@ -96,25 +96,18 @@ namespace OS {
}
VFSSettingDialog.scheme = `\
<afx-app-window width='250' height='180' apptitle = "__(Mount Points)">
<afx-vbox>
<afx-app-window width='250' height='200' apptitle = "__(Mount Points)">
<afx-vbox padding="10">
<afx-hbox>
<div data-width = "10" ></div>
<afx-vbox>
<div data-height="10" ></div>
<afx-label data-height="30" text = "__(Name)" ></afx-label>
<input type = "text" data-id= "txtName" ></input>
<div data-height="3" ></div>
<afx-label data-height="30" text = "__(Path)" ></afx-label>
<input type = "text" data-id= "txtPath" ></input>
<div data-height="10" ></div>
<afx-input label= "__(Name)" data-id="txtName"></afx-input>
<afx-input label= "__(Path)" data-id="txtPath"></afx-input>
<afx-hbox data-height="30">
<div ></div>
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "content" ></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "content" ></afx-button>
</afx-hbox>
</afx-vbox>
<div data-width = "10" ></div>
</afx-hbox>
</afx-vbox>
</afx-app-window>\
@ -144,7 +137,7 @@ namespace OS {
this.ppath = this.find("ppath") as GUI.tag.LabelTag;
this.mplist.buttons = [
{
text: "+",
iconclass: "bi bi-plus",
onbtclick: async () => {
const d = await this.parent.openDialog(
new VFSSettingDialog(),
@ -162,7 +155,7 @@ namespace OS {
},
},
{
text: "-",
iconclass: "bi bi-dash",
onbtclick: async () => {
const item = this.mplist.selectedItem;
if (!item) {

View File

@ -0,0 +1,80 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// AnTOS Web desktop is is licensed under the GNU General Public
// License v3.0, see the LICENCE file for more information
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
// You should have received a copy of the GNU General Public License
//along with this program. If not, see https://www.gnu.org/licenses/.
namespace OS {
const App = OS.application.Setting;
/**
*
*
* @class VFSHandle
* @extends {App.SettingHandle}
*/
class VersionsHandle extends App.SettingHandle {
private grid: GUI.tag.GridViewTag;
/**
*Creates an instance of VFSHandle.
* @param {HTMLElement} scheme
* @param {OS.application.Setting} parent
* @memberof VFSHandle
*/
constructor(scheme: HTMLElement, parent: OS.application.Setting) {
super(scheme, parent);
this.grid = this.find("grid-version") as GUI.tag.GridViewTag;
this.grid.resizable = true;
this.grid.header = [{ text: __("Component")}, { text: __("Version")}];
this.display_versions();
}
/**
* Display versions of all system components
*/
private async display_versions() : Promise<any>
{
try {
let result = await API.handle.versions();
if(result.error)
{
throw API.throwe(__("Unable to fetch system version information"));
}
let data = result.result as GenericObject<any>;
let records = [[{text:"AntOS"}, {text: `${OS.VERSION.version_string}`}]];
for (const key in data) {
const element = data[key];
records.push([
{ text: key },
{ text: `${element["version"]}-${element["ref"]}` }
])
}
this.grid.rows = records;
}
catch(e)
{
this.parent.error(e.toString());
}
}
}
App.VersionsHandle = VersionsHandle;
}

View File

@ -1,32 +1,17 @@
afx-app-window[data-id = "setting-window"] afx-tab-container afx-tab-bar afx-list-view > div > ul li{
float:none;
border-radius: 0;
font-weight: bold;
padding-top:3px;
padding-bottom: 3px;
border:0;
}
afx-app-window[data-id = "setting-window"] afx-tab-bar{
border-right: 1px solid #cbcbcb;
}
afx-app-window[data-id = "setting-window"] afx-label.header{
afx-app-window[data-id = "setting-window"] afx-label.header i.label-text{
font-weight: bold;
}
afx-app-window[data-id = "setting-window"] div.footer{
border-right: 1px solid #cbcbcb;
}
/*APPEARANCE*/
afx-app-window[data-id = "setting-window"] afx-hbox[data-id="appearance"] div[data-id = "wp-preview"]{
display: block;
border:1px solid #cbcbcb;
border:1px solid var(--border-quaternary);
border-radius: 10px;
}
afx-app-window[data-id = "setting-window"] afx-hbox[data-id="appearance"] afx-list-view[data-id="wplist"]
{
border:1px solid #cbcbcb;
padding:2px;
border:1px solid var(--border-quaternary);
}
afx-app-window[data-id = "setting-window"] afx-hbox[data-id="appearance"] afx-resizer{
border:0;
@ -35,7 +20,7 @@ afx-app-window[data-id = "setting-window"] afx-hbox[data-id="appearance"] afx-re
/*VFS*/
afx-app-window[data-id = "setting-window"] afx-hbox[data-id="vfs"] afx-list-view[data-id="mplist"]
{
border: 1px solid #cbcbcb;
border: 1px solid var(--border-quaternary);
}
afx-app-window[data-id = "setting-window"] afx-hbox[data-id="vfs"] afx-button.btnsel button{
@ -46,15 +31,20 @@ afx-app-window[data-id = "setting-window"] afx-hbox[data-id="vfs"] afx-button.bt
/*LANGUAGES*/
afx-app-window[data-id = "setting-window"] afx-hbox[data-id="locale"] afx-list-view[data-id="lglist"]
{
border: 1px solid #cbcbcb;
border: 1px solid var(--border-quaternary);
}
/*STARTUP*/
afx-app-window[data-id = "setting-window"] afx-hbox[data-id="startup"] afx-list-view
{
border: 1px solid #cbcbcb;
border: 1px solid var(--border-quaternary);
}
afx-app-window[data-id = "setting-window"] afx-hbox[data-id="app-services"] afx-list-view
{
border: 1px solid #cbcbcb;
border: 1px solid var(--border-quaternary);
}
afx-app-window[data-id = "setting-window"] afx-hbox[data-id="app-about"] afx-grid-view
{
border: 1px solid var(--border-quaternary);
}

View File

@ -82,6 +82,7 @@ namespace OS {
static StartupHandle: typeof SettingHandle;
static SettingHandle: typeof SettingHandle;
static AppAndServiceHandle: typeof SettingHandle;
static VersionsHandle: typeof SettingHandle;
/**
*Creates an instance of Setting.
@ -98,14 +99,15 @@ namespace OS {
* @memberof Setting
*/
main(): void{
//this.containter = this.find("container") as GUI.tag.TabContainerTag;
const containter = this.find("container") as GUI.tag.TabContainerTag;
new Setting.AppearanceHandle(this.find("appearance"), this);
new Setting.VFSHandle(this.find("vfs"), this);
new Setting.LocaleHandle(this.find("locale"), this);
new Setting.StartupHandle(this.find("startup"), this);
new Setting.AppAndServiceHandle(this.find("app-services"), this);
new Setting.VersionsHandle(this.find("app-about"), this);
containter.selectedIndex = 0;
(this.find("btnsave") as GUI.tag.ButtonTag ).onbtclick = (e) => {
this._api
.setting()
@ -130,6 +132,17 @@ namespace OS {
);
});
};
this.morphon(GUI.RESPONSIVE.MEDIUM, (fulfilled:boolean) => {
if(fulfilled)
{
containter.dir = "row";
}
else
{
containter.dir = "column";
}
});
}
}
Setting.singleton = true;

View File

@ -6,7 +6,7 @@
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com"
},
"version":"0.1.1-a",
"version":"0.2.2-b",
"category":"System",
"iconclass":"fa fa-wrench",
"mimes":["none"]

View File

@ -1,100 +1,89 @@
<afx-app-window data-id = "setting-window" apptitle="Setting" width="650" height="400">
<afx-app-window data-id = "setting-window" apptitle="Setting" width="600" height="500" responsive="true">
<afx-vbox>
<afx-tab-container data-id = "container" dir = "row" tabbarwidth= "150">
<afx-tab-container data-id = "container" dir = "column" tabbarheight= "40" tabbarwidth="160">
<afx-hbox tabname="__(Appearance)" data-id="appearance" iconclass = "fa fa-paint-brush">
<div data-width="10"></div>
<afx-hbox tabname="__(Appearance)" data-id="appearance" iconclass = "fa fa-paint-brush" padding="10">
<afx-vbox>
<div data-height="5"></div>
<afx-label text = "__(Wallpaper)" iconclass = "fa fa-image" class = "header" data-height="23"></afx-label>
<afx-label text = "__(Wallpaper)" iconclass = "fa fa-image" class = "header" data-height="30"></afx-label>
<afx-hbox>
<afx-list-view data-width="150" data-id="wplist"></afx-list-view>
<afx-list-view data-width="150" data-id="wplist" focusable="true"></afx-list-view>
<afx-resizer data-width="2"></afx-resizer>
<afx-vbox>
<div data-id = "wp-preview"></div>
<div data-height="5"></div>
<afx-hbox data-height="25">
<afx-list-view data-id = "wpsize" dropdown="true"></afx-list-view>
<div data-width="5"></div>
<afx-list-view data-id = "wprepeat" dropdown="true"></afx-list-view>
</afx-hbox>
</afx-vbox>
<div data-id = "wp-preview"></div>
</afx-hbox>
<div data-height="5"></div>
<afx-label text = "__(Theme)" iconclass = "fa fa-window-restore" class = "header" data-height="23"></afx-label>
<afx-list-view data-height="30" data-id="theme-list" dropdown="true"></afx-list-view>
<afx-hbox data-height="35">
<afx-list-view data-id = "wpsize" dropdown="true"></afx-list-view>
<div data-width="5"></div>
<afx-list-view data-id = "wprepeat" dropdown="true"></afx-list-view>
</afx-hbox>
<afx-label text = "__(Theme)" iconclass = "fa fa-window-restore" class = "header" data-height="30"></afx-label>
<afx-list-view data-height="35" data-id="theme-list" dropdown="true"></afx-list-view>
<div data-height="5"></div>
</afx-vbox>
<div data-width="10"></div>
</afx-hbox>
<afx-hbox data-id="vfs" tabname = "__(VFS)" iconclass = "fa fa-inbox">
<div data-width="10"></div>
<afx-hbox data-id="vfs" tabname = "__(VFS)" iconclass = "fa fa-inbox" padding="10">
<afx-vbox>
<div data-height="5"></div>
<afx-label text = "__(Mount points)" iconclass = "fa fa-folder" class = "header" data-height="23"></afx-label>
<afx-list-view data-id="mplist"></afx-list-view>
<div data-height="5"></div>
<afx-label text = "__(Desktop path)" iconclass = "fa fa-desktop" class = "header" data-height="23"></afx-label>
<afx-hbox data-height = "25" >
<afx-label text = "__(Mount points)" iconclass = "fa fa-folder" class = "header" data-height="30"></afx-label>
<afx-list-view data-id="mplist" focusable="true"></afx-list-view>
<afx-label text = "__(Desktop path)" iconclass = "fa fa-desktop" class = "header" data-height="30"></afx-label>
<afx-hbox data-height = "40" >
<div data-width="16"></div>
<afx-label data-id="dpath"></afx-label>
<afx-button text="" iconclass = "fa fa-arrow-up" data-id="btndpath" data-width="20" class="btnsel"></afx-button>
<afx-button text="" iconclass = "fa fa-arrow-up" data-id="btndpath" data-width="40" class="btnsel"></afx-button>
</afx-hbox>
<div data-height="5"></div>
<afx-label text = "__(Local packages path)" iconclass = "fa fa-cube" class = "header" data-height="23"></afx-label>
<afx-hbox data-height = "25" >
<afx-label text = "__(Local packages path)" iconclass = "fa fa-cube" class = "header" data-height="30"></afx-label>
<afx-hbox data-height = "40" >
<div data-width="16"></div>
<afx-label data-id="ppath"></afx-label>
<afx-button text="" data-id="btnppath" iconclass = "fa fa-arrow-up" data-width="20" class="btnsel"></afx-button>
<afx-button text="" data-id="btnppath" iconclass = "fa fa-arrow-up" data-width="40" class="btnsel"></afx-button>
</afx-hbox>
<div data-height="10"></div>
</afx-vbox>
<div data-width="10"></div>
</afx-hbox>
<afx-hbox data-id="locale" tabname = "__(Languages)"iconclass = "fa fa-globe">
<div data-width="10"></div>
<afx-hbox data-id="locale" tabname = "__(Languages)"iconclass = "fa fa-globe" padding="10">
<afx-vbox>
<div data-height="5"></div>
<afx-label text = "__(System locale)" iconclass = "fa fa-globe" class = "header" data-height="23"></afx-label>
<afx-list-view data-id="lglist"></afx-list-view>
<afx-label text = "__(System locale)" iconclass = "fa fa-globe" class = "header" data-height="30"></afx-label>
<afx-list-view data-id="lglist" focusable="true"></afx-list-view>
<div data-height="10"></div>
</afx-vbox>
<div data-width="10"></div>
</afx-hbox>
<afx-hbox data-id="startup" tabname = "__(Startup)" iconclass = "fa fa-cog">
<div data-width="10"></div>
<afx-hbox data-id="startup" tabname = "__(Startup)" iconclass = "fa fa-cog" padding="10">
<afx-vbox>
<afx-label text = "__(Startup services)" iconclass = "fa fa-tasks" class = "header" data-height="23"></afx-label>
<afx-list-view data-id="srvlist"></afx-list-view>
<afx-label text = "__(Startup services)" iconclass = "fa fa-tasks" class = "header" data-height="30"></afx-label>
<afx-list-view data-id="srvlist" focusable="true"></afx-list-view>
<div data-height="5"></div>
<afx-label text = "__(Startup applications)" iconclass = "fa fa-adn" class = "header" data-height="23"></afx-label>
<afx-list-view data-id="applist"></afx-list-view>
<afx-label text = "__(Startup applications)" iconclass = "fa fa-adn" class = "header" data-height="30"></afx-label>
<afx-list-view data-id="applist" focusable="true"></afx-list-view>
<div data-height="10"></div>
</afx-vbox>
<div data-width="10"></div>
</afx-hbox>
<afx-hbox data-id="app-services" tabname = "__(Apps. and Services)" iconclass = "fa fa-adn">
<div data-width="10"></div>
<afx-hbox data-id="app-services" tabname = "__(Apps. and Services)" iconclass = "fa fa-adn" padding="10">
<afx-vbox>
<afx-label text = "__(Services)" iconclass = "fa fa-tasks" class = "header" data-height="23"></afx-label>
<afx-list-view data-id="sys-srvlist"></afx-list-view>
<afx-label text = "__(Services)" iconclass = "fa fa-tasks" class = "header" data-height="30"></afx-label>
<afx-list-view data-id="sys-srvlist" focusable="true"></afx-list-view>
<div data-height="5"></div>
<afx-label text = "__(Pinned applications)" iconclass = "fa fa-adn" class = "header" data-height="23"></afx-label>
<afx-list-view data-id="sys-applist"></afx-list-view>
<afx-label text = "__(Pinned applications)" iconclass = "fa fa-adn" class = "header" data-height="30"></afx-label>
<afx-list-view data-id="sys-applist" focusable="true"></afx-list-view>
<div data-height="10"></div>
</afx-vbox>
<div data-width="10"></div>
</afx-hbox>
<afx-hbox data-id="app-about" tabname = "__(Versions)" iconclass = "bi bi-info-square" padding="10">
<afx-vbox>
<afx-label text = "__(System versions)" iconclass = "bi bi-gear-wide-connected" class = "header" data-height="30"></afx-label>
<afx-grid-view data-id="grid-version" focusable="true"></afx-grid-view>
</afx-vbox>
</afx-hbox>
</afx-tab-container>
<afx-hbox data-height="35">
<div data-width = "150" class = "footer"></div>
<afx-hbox data-height="45">
<div></div>
<div style="text-align:right" >
<afx-button text="__(Save)" data-id="btnsave" iconclass="fa fa-save" style="margin-right:10px;" ></afx-button>
<afx-button text="__(Save)" data-id="btnsave" iconclass="fa fa-save" style="margin-right:5px;" ></afx-button>
</div>
</afx-hbox>

View File

@ -1,11 +0,0 @@
module_files = Calendar.js PushNotification.js Syslog.js
libfiles =
cssfiles = main.css
copyfiles = package.json scheme.html
PKG_NAME=Syslog
include ../pkg.mk

View File

@ -1,86 +0,0 @@
afx-overlay[data-id = "notifyzone"]{
/*opacity: 0.85;*/
overflow-y: auto;
overflow-x: hidden;
padding:3px;
margin: 0;
}
afx-overlay[data-id = "notifyzone"] afx-button button{
width: 100%;
border-radius: 0;
}
afx-list-view[data-id = "notifylist"]
{
padding:0;
}
afx-list-view[data-id = "notifylist"] > div.list-container > ul li{
border:1px solid #464646;
border-radius: 3px;
margin-bottom: 5px;
-ms-word-break: break-all;
word-break: break-all;
word-break: break-word;
padding-top: 5px;
padding-bottom: 5px;
}
afx-overlay[data-id = "feedzone"]{
overflow: hidden;
background-color:transparent;
right:5px;
margin: 0;
padding:0;
top:0;
}
afx-list-view[data-id = "notifeed"]
{
padding:0;
margin:0;
}
afx-list-view[data-id = "notifeed"] li{
box-shadow: 0px 3px 6px 0px rgba(0,0,0,0.65);
border:1px solid #262626;
border-radius: 6px;
margin-bottom: 2px;
z-index: 99999;
-ms-word-break: break-all;
word-break: break-all;
word-break: break-word;
}
afx-app-window[data-id ='Syslog'] div[data-id ='container']{
overflow: auto;
}
afx-app-window[data-id ='Syslog'] .afx-bug-list-item-error {
display: block;
}
afx-app-window[data-id ='Syslog'] .afx-bug-list-item-error i::before {
color: chocolate;
}
afx-app-window[data-id ='Syslog'] .afx-bug-list-item-time{
display: block;
padding-left: 10px;
}
afx-app-window[data-id ='Syslog'] .afx-bug-list-item-time i.label-text{
font-size: 10px;
font-style: italic;
}
afx-app-window[data-id ='Syslog'] afx-bug-list-item li.selected {
background-color: #116cd6;
color: white;
}
afx-app-window[data-id ='Syslog'] pre {
padding: 10px;
margin:0;
user-select: text;
cursor: text;
}
afx-app-window[data-id ='Syslog'] input{
height: 100%;
}

View File

@ -1,20 +0,0 @@
{
"app": "Syslog",
"pkgname": "Syslog",
"services": [
"Calendar",
"PushNotification"
],
"name": "System log",
"description": "Core services and system log",
"info": {
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com",
"credit": "dedicated to some one here",
"licences": "GPLv3"
},
"version": "0.1.1-a",
"category": "System",
"iconclass": "fa fa-bug",
"mimes": []
}

View File

@ -0,0 +1,11 @@
module_files = SystemReport.js
libfiles =
cssfiles = main.css
copyfiles = package.json scheme.html README.md
PKG_NAME=SystemReport
include ../pkg.mk

View File

@ -0,0 +1,8 @@
# SystemReport: System notification management and service
Provide system wise notification service (Push Notification)
## Change logs
- v0.1.4-b: use system css variables in theme
- v0.1.3-b: Rename from Syslog to SystemReport, move all services to SystemServices package
- v0.1.2-b: add README

View File

@ -110,33 +110,33 @@ detail:
*
*
* @export
* @class Syslog
* @class SystemReport
* @extends {BaseApplication}
*/
export class Syslog extends BaseApplication {
export class SystemReport extends BaseApplication {
private loglist: TAG.ListViewTag;
private logdetail: HTMLElement;
private srv: PushNotification;
constructor(args: AppArgumentsType[]) {
super("Syslog", args);
super("SystemReport", args);
}
/**
*
*
* @memberof Syslog
* @memberof SystemReport
*/
/**
*
*
* @memberof Syslog
* @memberof SystemReport
*/
main(): void {
this.loglist = this.find("loglist") as TAG.ListViewTag;
this.logdetail = this.find("logdetail");
this._gui
.pushService("Syslog/PushNotification")
.pushService("SystemServices/PushNotification")
.then((srv) => {
this.srv = srv as PushNotification;
@ -231,7 +231,7 @@ detail:
*
*
* @param {GenericObject<any>} log
* @memberof Syslog
* @memberof SystemReport
*/
addLog(log: GenericObject<any>): void {
this.loglist.push(log);
@ -241,7 +241,7 @@ detail:
*
*
* @returns {void}
* @memberof Syslog
* @memberof SystemReport
*/
cleanup(): void {
if (this.srv) {
@ -250,6 +250,6 @@ detail:
}
}
Syslog.singleton = true;
SystemReport.singleton = true;
}
}

View File

@ -0,0 +1,36 @@
afx-app-window[data-id ='SystemReport'] div[data-id ='container']{
overflow: auto;
}
afx-app-window[data-id ='SystemReport'] .afx-bug-list-item-error {
display: block;
}
afx-app-window[data-id ='SystemReport'] .afx-bug-list-item-error i::before {
color: var(--text-error);
}
afx-app-window[data-id ='SystemReport'] .afx-bug-list-item-time{
display: block;
padding-left: 10px;
}
afx-app-window[data-id ='SystemReport'] .afx-bug-list-item-time i.label-text{
font-size: 10px;
font-style: italic;
}
/*
afx-app-window[data-id ='SystemReport'] afx-bug-list-item li.selected {
background-color: #116cd6;
color: white;
}*/
afx-app-window[data-id ='SystemReport'] pre {
padding: 10px;
margin:0;
user-select: text;
cursor: text;
}
afx-app-window[data-id ='SystemReport'] input{
height: 100%;
}

View File

@ -0,0 +1,15 @@
{
"app": "SystemReport",
"pkgname": "SystemReport",
"name": "System report",
"description": "System reports",
"info": {
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com",
"licences": "GPLv3"
},
"version": "0.1.5-b",
"category": "System",
"iconclass": "fa fa-bug",
"mimes": []
}

View File

@ -1,17 +1,17 @@
<afx-app-window data-id="Syslog" width='500' height='350' apptitle = "__(System error log)" >
<afx-app-window data-id="SystemReport" width='600' height='450' apptitle = "__(System error log)" responsive="true">
<afx-hbox>
<afx-list-view data-id = "loglist" data-width="200"> </afx-list-view>
<afx-list-view data-id = "loglist" data-width="200" focusable="true"> </afx-list-view>
<afx-resizer data-width = "2" ></afx-resizer>
<afx-vbox>
<div data-id = "container">
<pre><code data-id="logdetail"></code></pre>
</div>
<div data-height="10" ></div>
<afx-hbox style="text-align:right;" data-height = "27">
<afx-button data-width ="20"
<afx-hbox style="text-align:right;" data-height = "35">
<afx-button data-width ="content"
tooltip = "ct:__(Clear all logs)" iconclass = "fa fa-trash-o" data-id = "btclean" ></afx-button>
<input type = "text" data-id = "txturi" ></input>
<afx-button data-width ="80" text = "__(Report)"
<afx-button data-width ="content" text = "__(Report)"
iconclass = "fa fa-bug" data-id = "btnreport" ></afx-button>
<div data-width="10" ></div>
</afx-hbox>

View File

@ -26,9 +26,8 @@ namespace OS {
export class Calendar extends BaseService {
constructor(args: AppArgumentsType[]) {
super("Calendar", args);
//@iconclass = "fa fa-commenting"
this.text = "";
this.iconclass = "fa fa-calendar";
this.iconclass = "bi bi-calendar3";
}
init(): void {
@ -36,11 +35,11 @@ namespace OS {
this.watch(1000, () => {
const now = new Date();
this.text = now.toString();
(this.domel as GUI.tag.SimpleMenuEntryTag).text = this.text;
this.update();
});
}
awake(e: GUI.TagEventType<GUI.tag.MenuEventData>): void {
awake(e: GUI.TagEventType<GUI.tag.StackMenuEventData>): void {
this.openDialog("CalendarDialog").then((d) => console.log(d));
}
// do nothing

View File

@ -0,0 +1,11 @@
module_files = Calendar.js PushNotification.js
libfiles =
cssfiles = main.css
copyfiles = package.json README.md
PKG_NAME=SystemServices
include ../pkg.mk

View File

@ -31,12 +31,10 @@ namespace OS {
private cb: (e: JQuery.ClickEvent) => void;
private view: boolean;
private mlist: TAG.ListViewTag;
private mfeed: TAG.ListViewTag;
private nzone: TAG.OverlayTag;
private fzone: TAG.OverlayTag;
logs: GenericObject<any>[];
logmon: Syslog;
logmon: SystemReport;
/**
*Creates an instance of PushNotification.
@ -48,6 +46,7 @@ namespace OS {
this.iconclass = "fa fa-bars";
this.cb = undefined;
this.logs = [];
this.text = __("Notification");
this.logmon = undefined;
}
@ -69,9 +68,7 @@ namespace OS {
*/
main(): void {
this.mlist = this.find("notifylist") as TAG.ListViewTag;
this.mfeed = this.find("notifeed") as TAG.ListViewTag;
this.nzone = this.find("notifyzone") as TAG.OverlayTag;
this.fzone = this.find("feedzone") as TAG.OverlayTag;
(this.find("btclear") as TAG.ButtonTag).onbtclick = (e) =>
(this.mlist.data = []);
(this.find("bterrlog") as TAG.ButtonTag).onbtclick = (e) =>
@ -82,18 +79,12 @@ namespace OS {
this.subscribe("info", (o) => this.pushout("INFO", o));
this.nzone.height = "100%";
this.fzone.height = "100%";
$(this.nzone)
.css("right", 0)
.css("top", "0")
.css("bottom", "0")
.hide();
$(this.fzone)
//.css("z-index", 99999)
.css("bottom", "0")
.css("bottom", "0")
.hide();
}
/**
@ -104,7 +95,7 @@ namespace OS {
* @memberof PushNotification
*/
private showLogReport(): void {
this._gui.launch("Syslog", []);
this._gui.launch("SystemReport", []);
}
/**
@ -112,19 +103,19 @@ namespace OS {
*
* @private
* @param {string} s
* @param {GenericObject<any>} o
* @param {API.AnnouncementDataType} o
* @memberof PushNotification
*/
private addLog(s: string, o: GenericObject<any>): void {
private addLog(s: string, o: API.AnnouncementDataType<any>): void {
const logtime = new Date();
const log = {
type: s,
name: o.name,
text: `${o.data.m}`,
text: `${o.message}`,
id: o.id,
icon: o.data.icon,
iconclass: o.data.iconclass,
error: o.data.e,
icon: o.icon,
iconclass: o.iconclass,
error: o.u_data,
time: logtime,
closable: true,
tag: "afx-bug-list-item",
@ -141,14 +132,14 @@ namespace OS {
*
* @private
* @param {string} s
* @param {GenericObject<any>} o
* @param {API.AnnouncementDataType} o
* @memberof PushNotification
*/
private pushout(s: string, o: GenericObject<any>): void {
private pushout(s: string, o: API.AnnouncementDataType<any>): void {
const d = {
text: `[${s}] ${o.name} (${o.id}): ${o.data.m}`,
icon: o.data.icon,
iconclass: o.data.iconclass,
text: `[${s}] ${o.name} (${o.id}): ${o.message}`,
icon: o.icon,
iconclass: o.iconclass,
closable: true,
};
if (s !== "INFO") {
@ -166,16 +157,7 @@ namespace OS {
* @memberof PushNotification
*/
private notifeed(d: GenericObject<any>): void {
let timer: number;
this.mfeed.unshift(d);
$(this.fzone).show();
timer = window.setTimeout(() => {
this.mfeed.delete(d.domel);
if (this.mfeed.data.length === 0) {
$(this.fzone).hide();
}
return clearTimeout(timer);
}, 3000);
GUI.toast(d,{timeout: 3, location: GUI.ANCHOR.NORTH});
}
/**
@ -184,21 +166,25 @@ namespace OS {
* @param {GUI.TagEventType} evt
* @memberof PushNotification
*/
awake(evt: GUI.TagEventType<GUI.tag.MenuEventData>): void {
awake(evt: GUI.TagEventType<GUI.tag.StackMenuEventData>): void {
if (this.view) {
$(this.nzone).hide();
} else {
$(this.nzone).show();
}
console.log(evt, evt.data.item.aid);
this.view = !this.view;
if (!this.cb) {
this.cb = (e) => {
const list_id = $(evt.data.item).attr("list-id");
console.log(e.target, list_id);
if (
!$(e.target).closest($(this.nzone)).length &&
!$(e.target).closest(evt.data.item).length
!$(e.target).closest(`[list-id="${list_id}"]`).length
//!$(e.target).closest($(evt.data.item)).length
) {
$(this.nzone).hide();
$(document).unbind("click", this.cb);
$(document).off("click", this.cb);
this.view = !this.view;
}
};
@ -206,7 +192,7 @@ namespace OS {
if (this.view) {
$(document).on("click", this.cb);
} else {
$(document).unbind("click", this.cb);
$(document).off("click", this.cb);
}
}
@ -223,16 +209,12 @@ namespace OS {
const scheme = `\
<div>
<afx-overlay data-id = "notifyzone" width = "250px">
<afx-hbox data-height="30">
<afx-hbox data-height="35">
<afx-button text = "__(Clear all)" data-id = "btclear" ></afx-button>
<afx-button iconclass = "fa fa-bug" data-id = "bterrlog" data-width = "25"></afx-button>
<afx-button iconclass = "fa fa-bug" data-id = "bterrlog" data-width = "40"></afx-button>
</afx-hbox>
<afx-list-view data-id="notifylist"></afx-list-view>
</afx-overlay>
<afx-overlay data-id = "feedzone" width = "250">
<afx-list-view data-id = "notifeed">
</afx-list-view>
</afx-overlay>
</div>\
`;
}

View File

@ -0,0 +1,10 @@
## System services
Basic services:
- Push notification
- Calendar service
### change logs
- 0.1.2-b: fix show/hide animation bug
- 0.1.1-a: use system css variables in theme

View File

@ -0,0 +1,27 @@
afx-overlay[data-id = "notifyzone"]{
/*opacity: 0.85;*/
overflow-y: auto;
overflow-x: hidden;
padding:3px;
margin: 0;
}
afx-overlay[data-id = "notifyzone"] afx-button button{
width: 100%;
border-radius: 0;
}
afx-list-view[data-id = "notifylist"]
{
padding:0;
}
afx-list-view[data-id = "notifylist"] > div.list-container > ul li{
border:1px solid var(--border-quaternary);
background-color: var(--item-bg-odd);
border-radius: 3px;
margin-bottom: 5px;
-ms-word-break: break-all;
word-break: break-all;
word-break: break-word;
padding-top: 5px;
padding-bottom: 5px;
}

View File

@ -0,0 +1,15 @@
{
"pkgname": "SystemServices",
"services": ["PushNotification", "Calendar"],
"name": "System services",
"description": "System services",
"info": {
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com",
"licences": "GPLv3"
},
"version": "0.1.2-b",
"category": "System",
"iconclass": "fa fa-cog",
"mimes": []
}

View File

@ -1,56 +0,0 @@
afx-app-window div.afx-window-wrapper{
border:1px solid #262626;
box-shadow: 0px 3px 6px 0px rgba(0,0,0,0.65);
border-radius: 0px;
background-color:#363636;
}
afx-app-window.unactive > div.afx-window-wrapper{
background-color: #464646;
}
afx-app-window ul.afx-window-top{
height: 20px;
border-bottom: 1px solid #262626;
}
afx-app-window div.afx-window-overlay {
top: 22px;
}
afx-app-window ul.afx-window-top li{
margin-left: 3px;
margin-top:4px;
}
afx-app-window ul.afx-window-top .afx-window-close,.afx-window-minimize,.afx-window-maximize{
width: 10px;
height: 10px;
border-radius: 0;
}
afx-app-window ul li.afx-window-close{
background-color: #Fc605b;
float:left;
}
afx-app-window ul li.afx-window-minimize{
background-color: #fec041;
float:left;
}
afx-app-window ul li.afx-window-maximize{
background-color: #35cc4b;
float:left;
}
afx-app-window ul li.afx-window-title{
margin-top:1px;
text-align: center;
}
afx-app-window div.afx-window-content
{
background-color:#363636;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
afx-app-window.unactive div.afx-window-content
{
background-color:#464646;
}

View File

@ -1,22 +0,0 @@
afx-button button{
padding: 4px;
border: 1px solid #262626;
background-color: #464646;
color: white;
border-radius: 3px;
font-family: "Ubuntu";
}
afx-button button[disabled]{
color: #868686;
}
afx-button i.icon-style {
width: 16px;
height: 16px;
display: inline-block;
}
afx-button button:active, afx-button button.btactive {
background-color: #2786F3;
color: white;
border: 1px solid #363636;
}

View File

@ -1,53 +0,0 @@
afx-calendar-view div{
text-align: center;
}
afx-calendar-view > div {
font-weight: bold;
}
afx-calendar-view i.prevmonth, afx-calendar-view i.nextmonth{
display: inline-block;
width: 16px;
height: 16px;
cursor: pointer;
}
afx-calendar-view i.prevmonth{
margin-right: 20px;
}
afx-calendar-view i.nextmonth{
margin-left: 20px;
}
afx-calendar-view i.prevmonth:before{
content: "\f104";
font-family: "FontAwesome";
font-size: 16px;
font-style: normal;
/*position:absolute;
top:25%;
right:5px;*/
}
afx-calendar-view i.nextmonth:before{
content: "\f105";
font-family: "FontAwesome";
font-size: 16px;
font-style: normal;
margin-left: 20px;
/*position:absolute;
top:25%;
right:5px;*/
}
afx-calendar-view afx-grid-view afx-grid-row.afx-grid-row-selected afx-grid-cell
{
background-color: transparent;
}
afx-calendar-view afx-grid-view afx-grid-row.afx-grid-row-selected afx-grid-cell.afx-grid-cell-selected
{
background-color: #116cd6;
color:white;
border-radius: 6px;
}

View File

@ -1,35 +0,0 @@
afx-apps-dock{
bottom: 0px;
top: 0px;
width: 32px;
background-color:#363636;
padding:0;
padding-top: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border:1px solid #262626;
box-shadow: none;
}
afx-apps-dock afx-button button{
width: 32px;
height: 32px;
font-size: 19px;
margin-bottom: 0;
padding:0px;
background-color: transparent;
border:0;
border-radius: 0;
}
afx-apps-dock afx-button afx-label i.icon-style{
width: 24px;
height: 24px;
margin-left: 2px;
margin-bottom: 0px;
border:0;
}
afx-apps-dock afx-button.selected > button {
background-color: #464646;
color: white;
border: 1px solid #464646;
}

View File

@ -1,24 +0,0 @@
afx-grid-view afx-grid-row:nth-child(even) afx-grid-cell
{
background-color: #3b3b3b;
}
afx-grid-view afx-grid-row:nth-child(odd) afx-grid-cell
{
background-color: #363636;
}
afx-grid-view afx-grid-row.afx-grid-row-selected afx-grid-cell
{
background-color: #116cd6;
color:white;
}
afx-grid-view afx-grid-row.afx-grid-row-selected afx-grid-cell.afx-grid-cell-selected
{
font-weight: normal;
}
afx-grid-view .grid_row_header afx-grid-cell{
border:0;
}

View File

@ -1,90 +0,0 @@
afx-list-view > div.list-container > ul li{
padding: 5px;
padding-top:3px;
padding-bottom: 3px;
padding-right: 10px;
background-color: #363636;
}
afx-list-view > div.list-container > ul afx-list-item:nth-child(even) li{
background-color:#3b3b3b;
}
afx-list-view i.closable{
width: 16px;
height: 16px;
}
afx-list-view i.closable:before{
font-size: 10px;
margin-left: 10px;
color: #868686;
}
afx-list-view > div.list-container > ul li > i {
margin-right: 3px;
}
afx-list-view > div.list-container > ul > afx-list-item > li.selected{
background-color: #116cd6;
color:white;
}
afx-list-view.dropdown > div.list-container > ul{
border:1px solid #262626;
box-shadow: 0px 3px 6px 0px rgba(0,0,0,0.65);
border-radius: 3px;
max-height: 150px;
background-color: #363636;
border-top-left-radius: 0px;
z-index: 10;
}
afx-list-view.dropdown div.list-container div{
color: white;
padding-top:3px;
padding-bottom: 3px;
border:1px solid #262626;
border-radius: 3px;
background-color: transparent;
height: 17px;
}
afx-list-view.dropdown div.list-container div > afx-label{
padding-left:3px;
}
afx-list-view.dropdown div.list-container div:before {
content: "\f107";
font-family: "FontAwesome";
font-size: 11px;
font-style: normal;
position: absolute;
top:25%;
right: 5px;
}
afx-list-view.dropdown > div.list-container > ul li:hover{
background-color: #464646;
}
afx-list-view ul.complex-content{
padding: 0;
margin: 0;
background-color: transparent;
}
afx-list-view ul.complex-content li{
padding:0;
background-color: transparent;
color:#5e5f59;
list-style: none;
}
afx-list-view > div.list-container > ul li.selected ul.complex-content li{
color:white;
}
afx-list-view div.button_container afx-button{
margin-right: 2px;
}
afx-list-view div.button_container afx-button button{
border-radius: 0;
padding-left:5px;
padding-top:1px;
padding-bottom: 1px;
padding-right: 5px;
}

View File

@ -1,58 +0,0 @@
afx-menu afx-switch span{
width: 20px;
height: 16px;
font-size: 16px;
/*margin-top:5px;*/
}
afx-menu span.shortcut{
text-align: right;
}
afx-menu li:hover > a afx-switch span:before{
color:white;
}
afx-menu afx-menu ul {
padding: 0;
border:1px solid #262626;
border-radius: 5px;
border-top-left-radius: 0px;
/*box-shadow: 2px 2px 2px #cbcbcb;*/
box-shadow: 0px 3px 6px 0px rgba(0,0,0,0.65);
background-color: #363636;
}
afx-menu ul li /*, afx-menu ul >afx-menu-entry > li*/{
padding:3px;
padding-left: 5px;
padding-right: 5px;
}
afx-menu afx-menu li{
min-width: 150px;
}
afx-menu li:hover {
background-color: #2786F3;
}
afx-menu li:hover > a {
color: white;
}
afx-menu afx-menu .afx_submenu:before, afx-menu ul.context .afx_submenu:before{
content: "\f054";
font-family: "FontAwesome";
font-size: 10px;
right:5px;
color: #414339;
position:absolute;
top:25%;
}
afx-menu ul.context{
border:1px solid #262626;
border-radius: 5px;
border-top-left-radius: 0px;
box-shadow: 0px 3px 6px 0px rgba(0,0,0,0.65);
background-color: #363636;
}
afx-menu ul.context li{
min-width: 150px;
}

View File

@ -1,3 +0,0 @@
afx-overlay {
background-color: rgba(54, 54, 54, 0.7);
}

Some files were not shown because too many files have changed in this diff Show More