diff --git a/.gitignore b/.gitignore index f138d8c..6de2706 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ tools/assets/App_Resources/Android/src/main/assets/ionicWebStart tools/assets/App_Resources/iOS/ionicWebModal tools/assets/App_Resources/Android/src/main/assets/ionicWebModal .nx/cache -.nx/workspace-data \ No newline at end of file +.nx/workspace-data +packages/widgets/platforms/android/widgets.aar diff --git a/README.md b/README.md index 8e5b2d8..fb6266f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ - [@nativescript/rive](packages/rive/README.md) - [@nativescript/swift-ui](packages/swift-ui/README.md) - [@nativescript/ui-charts](packages/ui-charts/README.md) +- [@nativescript/widgets](packages/widgets/README.md) # How to use? diff --git a/apps/demo-angular/package.json b/apps/demo-angular/package.json index e18630a..612b790 100644 --- a/apps/demo-angular/package.json +++ b/apps/demo-angular/package.json @@ -8,7 +8,8 @@ "@nativescript/jetpack-compose": "file:../../dist/packages/jetpack-compose", "@nativescript/rive": "file:../../dist/packages/rive", "@nativescript/swift-ui": "file:../../dist/packages/swift-ui", - "@nativescript/ui-charts": "file:../../dist/packages/ui-charts" + "@nativescript/ui-charts": "file:../../dist/packages/ui-charts", + "@nativescript/widgets": "file:../../dist/packages/widgets" }, "devDependencies": { "@nativescript/android": "~8.9.0", diff --git a/apps/demo-angular/src/app-routing.module.ts b/apps/demo-angular/src/app-routing.module.ts index 330b4a4..a201e98 100644 --- a/apps/demo-angular/src/app-routing.module.ts +++ b/apps/demo-angular/src/app-routing.module.ts @@ -14,6 +14,7 @@ const routes: Routes = [ { path: 'rive', loadChildren: () => import('./plugin-demos/rive.module').then((m) => m.RiveModule) }, { path: 'swift-ui', loadChildren: () => import('./plugin-demos/swift-ui.module').then((m) => m.SwiftUiModule) }, { path: 'ui-charts', loadChildren: () => import('./plugin-demos/ui-charts.module').then((m) => m.UiChartsModule) }, + { path: 'widgets', loadChildren: () => import('./plugin-demos/widgets.module').then((m) => m.WidgetsModule) }, ]; @NgModule({ diff --git a/apps/demo-angular/src/home.component.ts b/apps/demo-angular/src/home.component.ts index bf915b4..3a312d1 100644 --- a/apps/demo-angular/src/home.component.ts +++ b/apps/demo-angular/src/home.component.ts @@ -27,5 +27,8 @@ export class HomeComponent { { name: 'ui-charts', }, + { + name: 'widgets', + }, ]; } diff --git a/apps/demo-angular/src/plugin-demos/widgets.component.html b/apps/demo-angular/src/plugin-demos/widgets.component.html new file mode 100644 index 0000000..865ee30 --- /dev/null +++ b/apps/demo-angular/src/plugin-demos/widgets.component.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/demo-angular/src/plugin-demos/widgets.component.ts b/apps/demo-angular/src/plugin-demos/widgets.component.ts new file mode 100644 index 0000000..0cf7651 --- /dev/null +++ b/apps/demo-angular/src/plugin-demos/widgets.component.ts @@ -0,0 +1,17 @@ +import { Component, NgZone } from '@angular/core'; +import { DemoSharedWidgets } from '@demo/shared'; +import {} from '@nativescript/widgets'; + +@Component({ + selector: 'demo-widgets', + templateUrl: 'widgets.component.html', +}) +export class WidgetsComponent { + demoShared: DemoSharedWidgets; + + constructor(private _ngZone: NgZone) {} + + ngOnInit() { + this.demoShared = new DemoSharedWidgets(); + } +} diff --git a/apps/demo-angular/src/plugin-demos/widgets.module.ts b/apps/demo-angular/src/plugin-demos/widgets.module.ts new file mode 100644 index 0000000..61f4e76 --- /dev/null +++ b/apps/demo-angular/src/plugin-demos/widgets.module.ts @@ -0,0 +1,10 @@ +import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core'; +import { NativeScriptCommonModule, NativeScriptRouterModule } from '@nativescript/angular'; +import { WidgetsComponent } from './widgets.component'; + +@NgModule({ + imports: [NativeScriptCommonModule, NativeScriptRouterModule.forChild([{ path: '', component: WidgetsComponent }])], + declarations: [WidgetsComponent], + schemas: [NO_ERRORS_SCHEMA], +}) +export class WidgetsModule {} diff --git a/apps/demo-angular/tsconfig.json b/apps/demo-angular/tsconfig.json index 0408fed..a1ee810 100644 --- a/apps/demo-angular/tsconfig.json +++ b/apps/demo-angular/tsconfig.json @@ -11,7 +11,8 @@ "@nativescript/jetpack-compose": ["../../packages/jetpack-compose/index.d.ts"], "@nativescript/rive": ["../../packages/rive/index.d.ts"], "@nativescript/swift-ui": ["../../packages/swift-ui/index.d.ts"], - "@nativescript/ui-charts": ["../../packages/ui-charts/index.d.ts"] + "@nativescript/ui-charts": ["../../packages/ui-charts/index.d.ts"], + "@nativescript/widgets": ["../../packages/widgets/index.d.ts"] } }, "files": ["./references.d.ts", "./src/main.ts", "./src/polyfills.ts"], diff --git a/apps/demo/package.json b/apps/demo/package.json index 18b0f20..28fa4a1 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -11,7 +11,8 @@ "@nativescript/jetpack-compose": "file:../../packages/jetpack-compose", "@nativescript/rive": "file:../../packages/rive", "@nativescript/swift-ui": "file:../../packages/swift-ui", - "@nativescript/ui-charts": "file:../../packages/ui-charts" + "@nativescript/ui-charts": "file:../../packages/ui-charts", + "@nativescript/widgets": "file:../../packages/widgets" }, "devDependencies": { "@nativescript/android": "~8.9.0", diff --git a/apps/demo/src/app.ts b/apps/demo/src/app.ts index 1c53e3f..b8be8f4 100644 --- a/apps/demo/src/app.ts +++ b/apps/demo/src/app.ts @@ -1,5 +1,5 @@ -import { Application } from '@nativescript/core'; - +import { Application, Http, ImageSource } from '@nativescript/core'; +import { registerWidgetListener, LinearLayout, ImageView, Image, Button, VStack, List, Text, updateWidget, Root, HStack, ButtonView, Flipper, Chronometer, Clock, Grid, ProgressBar, Stack, Spacer, Switch, CheckBox, RadioButton } from '@nativescript/widgets'; // uncomment to test Flutter // import { init } from '@nativescript/flutter'; // init(); @@ -17,4 +17,271 @@ import { Application } from '@nativescript/core'; // IonicPortalManager.register(''); // }); +// 1. Countdown timer widget +function countdownWidget(provider: string, ids: number[]) { + const targetTime = Date.now() + 3600000; // 1 hour from now + const root = Root(() => + VStack(() => [ + Text('⏱️ Countdown').setColor('#1a1a2e').setTextSize(18), + Spacer(12), + Chronometer({ + targetTime: targetTime, // Use targetTime for Unix timestamps + countDown: true, + started: true, + }) + .setColor('#e74c3c') + .setTextSize(32), + Spacer(8), + Text('until deadline').setColor('#6c757d').setTextSize(12), + ]), + ) + .setBackgroundColor('#ffffff') + .setPadding(16); + for (const id of ids) updateWidget(provider, root, id); +} + +// 2. Clock widget +function clockWidget(provider: string, ids: number[]) { + const root = Root(() => VStack(() => [Text('🕐 Current Time').setColor('#ecf0f1').setTextSize(14), Spacer(12), Clock({ color: '#3498db' }).setTextSize(42), Spacer(8), Text('Local timezone').setColor('#7f8c8d').setTextSize(11)])) + .setBackgroundColor('#1a1a2e') + .setPadding(16); + for (const id of ids) updateWidget(provider, root, id); +} + +// 3. Progress indicator +function progressWidget(provider: string, ids: number[]) { + const root = Root(() => VStack(() => [HStack(() => [Text('⬇️ Downloading').setColor('#2c3e50').setTextSize(16), Spacer(), Text('65%').setColor('#27ae60').setTextSize(16)]), Spacer(12), ProgressBar({ progress: 65, max: 100 }), Spacer(8), Text('file_backup.zip • 650 MB / 1 GB').setColor('#7f8c8d').setTextSize(11)])) + .setBackgroundColor('#ffffff') + .setPadding(16); + for (const id of ids) updateWidget(provider, root, id); +} + +// 4. List with click handlers +function listWidget(provider: string, ids: number[]) { + const items = [ + { icon: '📥', name: 'Inbox', count: 12 }, + { icon: '⭐', name: 'Starred', count: 3 }, + { icon: '📤', name: 'Sent', count: 0 }, + { icon: '📝', name: 'Drafts', count: 2 }, + { icon: '🗑️', name: 'Trash', count: 0 }, + ]; + const root = Root(() => + VStack(() => [ + Text('📧 Mail').setColor('#2c3e50').setTextSize(18), + Spacer(8), + List( + items.length, + (index) => HStack(() => [Text(`${items[index].icon} ${items[index].name}`).setColor('#34495e').setTextSize(14), Spacer(), items[index].count > 0 ? Text(`${items[index].count}`).setColor('#3498db').setTextSize(12) : Text('').setTextSize(12)]).setMargin(4, 8, 4, 8), + () => Text('No folders').setColor('#95a5a6'), + ), + ]), + ) + .setBackgroundColor('#ffffff') + .setPadding(12); + for (const id of ids) updateWidget(provider, root, id); +} + +// 5. Image flipper/slideshow +async function slideshowWidget(provider: string, ids: number[], loadImages = false) { + const imageUrls = ['https://picsum.photos/seed/1/400/300', 'https://picsum.photos/seed/2/400/300', 'https://picsum.photos/seed/3/400/300', 'https://picsum.photos/seed/4/400/300']; + + const images = loadImages ? await Promise.all(imageUrls.map((url) => ImageSource.fromUrl(url))) : imageUrls; + + const root = Root(() => VStack(() => [Text('📷 Gallery').setColor('#ffffff').setTextSize(16), Spacer(8), Flipper(images as never[], 3000, (src) => Image(src)), Spacer(8), Text('Auto-advances every 3s').setColor('#adb5bd').setTextSize(11)])) + .setBackgroundColor('#2c3e50') + .setPadding(12); + + for (const id of ids) updateWidget(provider, root, id); +} + +// 6. Grid layout +function gridWidget(provider: string, ids: number[]) { + const actions = [ + { icon: '📞', label: 'Call', color: '#3498db', bg: '#ebf5fb' }, + { icon: '💬', label: 'Message', color: '#9b59b6', bg: '#f5eef8' }, + { icon: '📧', label: 'Email', color: '#e74c3c', bg: '#fdedec' }, + { icon: '📅', label: 'Calendar', color: '#27ae60', bg: '#eafaf1' }, + { icon: '📷', label: 'Camera', color: '#f39c12', bg: '#fef9e7' }, + { icon: '🎵', label: 'Music', color: '#e91e63', bg: '#fce4ec' }, + ]; + + const root = Root(() => + VStack(() => [ + HStack(() => [Text('⚡ Quick Actions').setColor('#1a1a2e').setTextSize(18), Spacer(), Text('6 apps').setColor('#95a5a6').setTextSize(12)]), + Spacer(16), + Grid(3, 12, () => + actions.map((action) => + VStack(() => [ + VStack(() => [Text(action.icon).setTextSize(28), Spacer(6), Text(action.label).setColor(action.color).setTextSize(11)]) + .setBackgroundColor(action.bg) + .setPadding(12), + Spacer(4), + ]), + ), + ), + ]), + ) + .setBackgroundColor('#ffffff') + .setPadding(16); + for (const id of ids) updateWidget(provider, root, id); +} + +// 6b. Toggle widget (single Switch) +function toggleWidget(provider: string, ids: number[]) { + const root = Root(() => VStack(() => [HStack(() => [Text('🔔 Notifications').setTextSize(16), Spacer(), Text('On').setTextSize(12)]), Spacer(12), HStack(() => [Text('Push alerts').setTextSize(14), Spacer(), Switch(true)]), Spacer(8), Text('Tap the switch to toggle').setColor('#95a5a6').setTextSize(11)])) + .setBackgroundColor('#ffffff') + .setPadding(12); + + for (const id of ids) updateWidget(provider, root, id); +} + +// 6c. Checklist widget (multiple checkboxes) +function checklistWidget(provider: string, ids: number[]) { + const items = [ + { label: 'Buy groceries', checked: true }, + { label: 'Send invoices', checked: false }, + { label: 'Workout', checked: false }, + ]; + + const root = Root(() => VStack(() => [Text('🗒️ Today').setTextSize(18), Spacer(8), ...items.map((it) => HStack(() => [CheckBox(it.checked).onCheck('toggle', { label: it.label }), Spacer(8), Text(it.label).setTextSize(14)]).setMargin(6, 8, 6, 8))])) + .setBackgroundColor('#ffffff') + .setPadding(12); + + for (const id of ids) updateWidget(provider, root, id); +} + +// 6d. Radio group widget (select one) +function radioWidget(provider: string, ids: number[]) { + const options = [ + { label: 'Home', checked: true }, + { label: 'Work', checked: false }, + { label: 'Travel', checked: false }, + ]; + + const root = Root(() => VStack(() => [Text('📍 Mode').setTextSize(18), Spacer(8), ...options.map((opt) => HStack(() => [RadioButton(opt.checked), Spacer(8), Text(opt.label).setTextSize(14)]).setMargin(6, 6, 6, 6))])) + .setBackgroundColor('#ffffff') + .setPadding(12); + + for (const id of ids) updateWidget(provider, root, id); +} + +// 7. StackView (swipeable cards) +function stackWidget(provider: string, ids: number[]) { + const cards = [ + { title: 'Meeting Notes', subtitle: 'Project sync @ 2pm', color: '#3498db' }, + { title: 'Shopping List', subtitle: '5 items remaining', color: '#e74c3c' }, + { title: 'Workout Plan', subtitle: 'Leg day tomorrow', color: '#27ae60' }, + ]; + const root = Root(() => + VStack(() => [ + Text('📚 Cards').setColor('#ffffff').setTextSize(16), + Spacer(8), + Stack(cards.length, (index) => + VStack(() => [Text(cards[index].title).setColor('#2c3e50').setTextSize(16), Spacer(4), Text(cards[index].subtitle).setColor('#7f8c8d').setTextSize(12)]) + .setBackgroundColor('#ffffff') + .setPadding(12), + ), + Spacer(8), + Text('Swipe to browse').setColor('#adb5bd').setTextSize(11), + ]), + ) + .setBackgroundColor('#34495e') + .setPadding(12); + for (const id of ids) updateWidget(provider, root, id); +} + +// 8. Complex combined layout +function dashboardWidget(provider: string, ids: number[]) { + const tasks = [ + { name: 'Review PR #142', done: true }, + { name: 'Update documentation', done: true }, + { name: 'Fix login bug', done: false }, + { name: 'Deploy to staging', done: false }, + { name: 'Team standup', done: false }, + ]; + const completed = tasks.filter((t) => t.done).length; + const progress = Math.round((completed / tasks.length) * 100); + + const root = Root(() => + VStack(() => [ + // Header row + HStack(() => [Text('📊 Dashboard').setColor('#1a1a2e').setTextSize(18), Spacer(), Clock({ color: '#6c757d' }).setTextSize(14)]), + + Spacer(12), + + // Progress section + Text('Sprint Progress').setColor('#495057').setTextSize(12), + Spacer(4), + HStack(() => [ProgressBar({ progress, max: 100, flex: true }), Spacer(8), Text(`${progress}%`).setColor('#27ae60').setTextSize(14)]), + + Spacer(16), + + // Tasks section + HStack(() => [Text('Tasks').setColor('#495057').setTextSize(12), Spacer(), Text(`${completed}/${tasks.length}`).setColor('#6c757d').setTextSize(11)]), + Spacer(4), + List(tasks.length, (i) => + HStack(() => [ + CheckBox(tasks[i].done).onCheck('toggleTask', { name: tasks[i].name }), + Spacer(8), + Text(tasks[i].name) + .setColor(tasks[i].done ? '#27ae60' : '#212529') + .setTextSize(13), + ]), + ), + ]), + ) + .setBackgroundColor('#ffffff') + .setPadding(16); + for (const id of ids) updateWidget(provider, root, id); +} + +registerWidgetListener('org.nativescript.plugindemo.PluginDemoWidgetProvider', { + onClick(event) { + console.log('Widget clicked', event); + }, + onCheck(event) { + console.log('Check toggled', event); + }, + onUpdate: (event) => { + dashboardWidget(event.provider, event.appWidgetIds); + //countdownWidget(event.provider, event.appWidgetIds); + // clockWidget(event.provider, event.appWidgetIds); + // progressWidget(event.provider, event.appWidgetIds); + // listWidget(event.provider, event.appWidgetIds); + // slideshowWidget(event.provider, event.appWidgetIds); + //gridWidget(event.provider, event.appWidgetIds); + //stackWidget(event.provider, event.appWidgetIds); + //toggleWidget(event.provider, event.appWidgetIds); + // checklistWidget(event.provider, event.appWidgetIds); + // radioWidget(event.provider, event.appWidgetIds); + /* const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + const list = List( + data.length, + (index) => + VStack(() => + HStack(() => { + const img = Image('').setSize(20, 20) as ImageView; + const ret = [img, Text(`Item ${index}`), Button('Click', () => console.log(`Clicked item ${index}`))]; + rows.push({ row: ret as never, src: `https://picsum.photos/seed/${data[index]}/300/300` }); + return ret; + }), + ), + () => Text('No content'), + ).setBackgroundColor('white'); + + const root = Root(() => list) + .setBackgroundColor('lightgray') + .setPadding(20); + + rootView = root; + for (const id of event.appWidgetIds) { + updateWidget(event.provider, root, id); + } + */ + }, + async onUpdateAsync(event) { + // slideshowWidget(event.provider, event.appWidgetIds, true); + }, +}); + Application.run({ moduleName: 'app-root' }); diff --git a/apps/demo/src/main-page.xml b/apps/demo/src/main-page.xml index eec59c8..f652756 100644 --- a/apps/demo/src/main-page.xml +++ b/apps/demo/src/main-page.xml @@ -12,6 +12,7 @@