diff --git a/apps/toolbox/package.json b/apps/toolbox/package.json
index 331ac9ac73..45a7effdd7 100644
--- a/apps/toolbox/package.json
+++ b/apps/toolbox/package.json
@@ -17,6 +17,7 @@
"@nativescript/visionos": "~9.0.0",
"@nativescript/vite": "file:../../dist/packages/vite",
"@nativescript/webpack": "file:../../dist/packages/webpack5",
+ "@nativescript/windows": "0.1.0-alpha.122",
"typescript": "~5.8.0"
}
}
diff --git a/apps/toolbox/src/_app-platform.windows.css b/apps/toolbox/src/_app-platform.windows.css
new file mode 100644
index 0000000000..2a82ae04f6
--- /dev/null
+++ b/apps/toolbox/src/_app-platform.windows.css
@@ -0,0 +1,53 @@
+/* Import theme rules for Windows and include additional core rules so Windows gets the same
+ look as other platforms without modifying the NativeScript runtime. */
+@import '../../../node_modules/nativescript-theme-core/css/core.light.windows.css';
+
+/* Only include the Windows-specific theme file here. Importing iOS/Android theme
+ files in the Windows platform CSS can introduce conflicting rules and
+ unexpected colors; platform-specific CSS should be kept separate. */
+
+/* Fallbacks / platform-specific tweaks for Windows host environments */
+
+/* Ensure pages default to white background like the theme */
+.page, Page {
+ background-color: #fff;
+}
+
+/* Visibility helper used across the theme */
+.invisible {
+ visibility: collapse;
+}
+
+/* Simple shadow/gradient fallbacks — some Windows hosts may not fully support composition */
+.shadow {
+ box-shadow: 0 2 6 rgba(0,0,0,0.2);
+ border-radius: 4;
+}
+.gradient {
+ background: linear-gradient(to bottom, #f12711, #f5af19);
+}
+
+/* Make list items readable on Windows hosts */
+.list-group .list-group-item {
+ padding: 16;
+ color: #212121;
+}
+
+/* TabView / SegmentedBar defaults */
+.tab-view {
+ selected-color: #30bcff;
+ tabs-background-color: #fff;
+}
+.tab-view .tab-view-item {
+ background-color: #fff;
+}
+
+/* Ensure ScrollView / ListView backgrounds don't hide content */
+ScrollView, ListView, Repeater {
+ background-color: transparent;
+}
+
+/* Make sure controls have a sensible default enabled/disabled appearance */
+Button[isEnabled=false], TextField[isEnabled=false], TextView[isEnabled=false] {
+ opacity: 0.6;
+}
diff --git a/apps/toolbox/src/pages/a11y.ts b/apps/toolbox/src/pages/a11y.ts
index 6c82fb442f..f265c833b0 100644
--- a/apps/toolbox/src/pages/a11y.ts
+++ b/apps/toolbox/src/pages/a11y.ts
@@ -17,7 +17,7 @@ export class AccessibilityModel extends Observable {
accessibilityLiveRegions = AccessibilityLiveRegion;
accessibilityRole = AccessibilityRole;
accessibilityState = AccessibilityState;
- largeImageSrc = 'https://i.picsum.photos/id/669/5000/5000.jpg?hmac=VlpchW0ODhflKm0SKOYQrc8qysLWbqKmDS1MGT9apAc';
+ largeImageSrc = 'https://picsum.photos/seed/VlpchW0ODhflKm0SKOYQrc8qysLWbqKmDS1MGT9apAc/5000/5000';
constructor() {
super();
@@ -30,8 +30,8 @@ export class AccessibilityModel extends Observable {
// prettier-ignore
this.notifyPropertyChange('largeImageSrc', checked ?
- 'https://i.picsum.photos/id/669/5000/5000.jpg?hmac=VlpchW0ODhflKm0SKOYQrc8qysLWbqKmDS1MGT9apAc' :
- 'https://i.picsum.photos/id/684/5000/5000.jpg?hmac=loiXO_OQ-y86XY_hc7p3qJdY39fSd9CuDM0iA_--P4Q');
+ 'https://picsum.photos/seed/VlpchW0ODhflKm0SKOYQrc8qysLWbqKmDS1MGT9apAc/5000/5000' :
+ 'https://picsum.photos/seed/loiXO_OQ-y86XY_hc7p3qJdY39fSd9CuDM0iA_--P4Q/5000/5000');
}
openModal() {
diff --git a/apps/toolbox/src/pages/css-playground.ts b/apps/toolbox/src/pages/css-playground.ts
index c2d99fdf4b..a336474873 100644
--- a/apps/toolbox/src/pages/css-playground.ts
+++ b/apps/toolbox/src/pages/css-playground.ts
@@ -36,7 +36,7 @@ export class CssPlaygroundModel extends Observable {
applyCSS(args) {
this.resetCSS();
- addTaggedAdditionalCSS(`#play { ${this.currentCSS}`, CSSTag);
+ addTaggedAdditionalCSS(`#play { ${this.currentCSS} }`, CSSTag);
playLabel._onCssStateChange();
playLabel.requestLayout();
}
diff --git a/apps/toolbox/src/pages/list-page-model-sticky.ts b/apps/toolbox/src/pages/list-page-model-sticky.ts
index cf8b3cb9c1..0ed74e3787 100644
--- a/apps/toolbox/src/pages/list-page-model-sticky.ts
+++ b/apps/toolbox/src/pages/list-page-model-sticky.ts
@@ -1392,7 +1392,18 @@ export class ListPageModelSticky extends Observable {
}
itemLoading(args: EventData): void {
- (args.object as View).backgroundColor = 'transparent';
+ const view = (args as any).view as View;
+ try {
+ const idx = (args as any).index;
+ const section = (args as any).section;
+ const argsBC = (args as any).bindingContext;
+ const obj = (args as any).object;
+ const viewCtor = view && (view as any).constructor ? (view as any).constructor.name : 'unknown';
+ const viewBC = view && (view as any).bindingContext;
+ try { console.log('[ListPageSticky] itemLoading index=', idx, 'section=', section, 'args.bindingContextType=', typeof argsBC, 'args.object=', obj ? (obj.constructor?.name || 'object') : 'null', 'viewType=', viewCtor, 'view.bindingContextType=', typeof viewBC); } catch (_e) {}
+ try { if (viewBC && typeof viewBC === 'object' && (viewBC as any).name) console.log('[ListPageSticky] item name=', (viewBC as any).name); } catch (_e) {}
+ } catch (_e) {}
+ try { view.backgroundColor = 'transparent'; } catch (_e) {}
}
onSearchTextChange(evt: SearchEventData): void {
diff --git a/apps/toolbox/src/pages/list-page.xml b/apps/toolbox/src/pages/list-page.xml
index cce6c49c71..e5e61dc874 100644
--- a/apps/toolbox/src/pages/list-page.xml
+++ b/apps/toolbox/src/pages/list-page.xml
@@ -23,6 +23,9 @@
+
+
+
@@ -47,6 +50,9 @@
+
+
+
diff --git a/apps/toolbox/src/split-view/split-view-primary.ts b/apps/toolbox/src/split-view/split-view-primary.ts
index 3f5748cb28..6bf982c72e 100644
--- a/apps/toolbox/src/split-view/split-view-primary.ts
+++ b/apps/toolbox/src/split-view/split-view-primary.ts
@@ -4,7 +4,9 @@ let page: Page;
export function navigatingTo(args: EventData) {
page = args.object;
- page.bindingContext = new SplitViewPrimaryModel();
+ const model = new SplitViewPrimaryModel();
+ page.bindingContext = model;
+ try { console.log('[Primary] navigatingTo, items.length=', model.items?.length); } catch (_e) {}
}
export class SplitViewPrimaryModel extends Observable {
diff --git a/apps/toolbox/tsconfig.json b/apps/toolbox/tsconfig.json
index 0d0dc494b0..90dddd3baa 100644
--- a/apps/toolbox/tsconfig.json
+++ b/apps/toolbox/tsconfig.json
@@ -5,5 +5,6 @@
"paths": {
"~/*": ["src/*"]
}
- }
+ },
+ "exclude": ["node_modules", "tmp", "platforms", "__tests__", "vite.config.ts"]
}
diff --git a/apps/toolbox/webpack.config.js b/apps/toolbox/webpack.config.js
index c47db7c6f6..5ac42bc263 100644
--- a/apps/toolbox/webpack.config.js
+++ b/apps/toolbox/webpack.config.js
@@ -1,4 +1,5 @@
const webpack = require("@nativescript/webpack");
+const path = require("path");
module.exports = (env) => {
webpack.init(env);
@@ -11,6 +12,11 @@ module.exports = (env) => {
return args
})
+
+ // Stub Windows-unsupported packages
+ if (env.windows) {
+ config.resolve.alias.set('@nativescript/imagepicker', path.resolve(__dirname, 'src/stubs/imagepicker.js'));
+ }
})
return webpack.resolveConfig();
diff --git a/apps/ui/package.json b/apps/ui/package.json
index 0168a45c97..3eddeced4b 100644
--- a/apps/ui/package.json
+++ b/apps/ui/package.json
@@ -15,6 +15,7 @@
"@nativescript/ios": "~9.0.0",
"@nativescript/visionos": "~9.0.0",
"@nativescript/webpack": "file:../../dist/packages/webpack5",
+ "@nativescript/windows": "0.1.0-alpha.122",
"typescript": "~5.8.0"
},
"gitHead": "8ab7726d1ee9991706069c1359c552e67ee0d1a4",
diff --git a/packages/core/animation-frame/animation-native.windows.ts b/packages/core/animation-frame/animation-native.windows.ts
new file mode 100644
index 0000000000..4d13033a2b
--- /dev/null
+++ b/packages/core/animation-frame/animation-native.windows.ts
@@ -0,0 +1,3 @@
+export function getTimeInFrameBase(): number {
+ return Date.now();
+}
diff --git a/packages/core/application-settings/index.windows.ts b/packages/core/application-settings/index.windows.ts
new file mode 100644
index 0000000000..4cba40932a
--- /dev/null
+++ b/packages/core/application-settings/index.windows.ts
@@ -0,0 +1,108 @@
+import * as Common from './application-settings-common';
+
+export function hasKey(key: string): boolean | undefined {
+ if (!Common.checkKey(key)) {
+ return;
+ }
+
+ const localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
+ return localSettings.Values.HasKey(key);
+}
+
+export function getBoolean(key: string, defaultValue?: boolean): boolean | undefined {
+ if (!Common.checkKey(key)) {
+ return undefined;
+ }
+ if (hasKey(key)) {
+ const localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
+ return localSettings.Values[key] as never;
+ }
+
+ return defaultValue;
+}
+
+export function getString(key: string, defaultValue?: string): string | undefined {
+ if (!Common.checkKey(key)) {
+ return;
+ }
+ if (hasKey(key)) {
+ const localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
+ return localSettings.Values[key] as never;
+ }
+
+ return defaultValue;
+}
+
+export function getNumber(key: string, defaultValue?: number): number | undefined {
+ if (!Common.checkKey(key)) {
+ return;
+ }
+ if (hasKey(key)) {
+ const localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
+ return localSettings.Values[key] as never;
+ }
+
+ return defaultValue;
+}
+
+export function setBoolean(key: string, value: boolean): void {
+ if (!Common.checkKey(key)) {
+ return;
+ }
+ if (!Common.ensureValidValue(value, 'boolean')) {
+ return;
+ }
+ const localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
+ localSettings.Values[key] = value;
+}
+
+export function setString(key: string, value: string): void {
+ if (!Common.checkKey(key)) {
+ return;
+ }
+ if (!Common.ensureValidValue(value, 'string')) {
+ return;
+ }
+ const localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
+ localSettings.Values[key] = value;
+}
+
+export function setNumber(key: string, value: number): void {
+ if (!Common.checkKey(key)) {
+ return;
+ }
+ if (!Common.ensureValidValue(value, 'number')) {
+ return;
+ }
+ const localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
+ localSettings.Values[key] = value;
+}
+
+export function remove(key: string): void {
+ if (!Common.checkKey(key)) {
+ return;
+ }
+ const localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
+ localSettings.Values.Remove(key);
+}
+
+export function clear(): void {
+ Windows.Storage.ApplicationData.Current.LocalSettings.Values.Clear();
+}
+
+export function flush(): boolean {
+ // no-op since Windows.Storage.ApplicationData is automatically flushed by the system
+ return true;
+}
+
+export function getAllKeys(): Array {
+ const Values = Windows.Storage.ApplicationData.Current.LocalSettings.Values;
+ const first = Values.First();
+ const ret: string[] = [];
+ while (first && first.HasCurrent) {
+ ret.push(first.Current.Key);
+ first.MoveNext();
+ }
+
+ return ret;
+}
diff --git a/packages/core/application/application-common.ts b/packages/core/application/application-common.ts
index 50b27b6f8d..123bef2c07 100644
--- a/packages/core/application/application-common.ts
+++ b/packages/core/application/application-common.ts
@@ -415,6 +415,7 @@ export class ApplicationCommon {
object: this,
ios: this.ios,
android: this.android,
+ windows: this.windows,
...additionalLanchEventData,
};
this.notify(launchArgs);
@@ -746,6 +747,10 @@ export class ApplicationCommon {
return undefined;
}
+ get windows() {
+ return undefined;
+ }
+
get AndroidApplication() {
return this.android;
}
diff --git a/packages/core/application/application.windows.ts b/packages/core/application/application.windows.ts
new file mode 100644
index 0000000000..1520e2993d
--- /dev/null
+++ b/packages/core/application/application.windows.ts
@@ -0,0 +1,181 @@
+import { ApplicationCommon } from './application-common';
+import type { NavigationEntry } from '../ui/frame/frame-interfaces';
+import { setAppMainEntry, setToggleApplicationEventListenersCallback, setApplicationPropertiesCallback, setA11yUpdatePropertiesCallback } from './helpers-common';
+import type { View } from '../ui/core/view';
+import { setRootView } from './helpers-common';
+import { CoreTypes } from 'index';
+
+export class WindowsApplication extends ApplicationCommon {
+
+ private _rootView: View;
+
+ getRootView(): View {
+ return this._rootView;
+ }
+
+ run(entry?: string | NavigationEntry): void {
+ if (this.started) {
+ throw new Error('Application is already started.');
+ }
+
+ this.setupLifecycleEvents();
+
+ this.started = true;
+ setAppMainEntry(typeof entry === 'string' ? { moduleName: entry } : entry);
+ this.setWindowContent();
+ }
+
+
+ private setWindowContent(view?: View): void {
+ if (this._rootView) {
+ this._rootView._onRootViewReset();
+ }
+ const rootView = this.createRootView(view, true);
+ if (!rootView) return;
+
+ this._rootView = rootView;
+ setRootView(rootView);
+
+ rootView._setupAsRootView({});
+
+ const win = Windows.UI.Xaml.Window.Current;
+ win.Content = rootView.nativeViewProtected;
+ win.Activate();
+
+ this.initRootView(rootView);
+ rootView.callLoaded();
+ }
+
+ getNativeApplication() {
+ return Windows?.UI?.Xaml?.Application?.Current ?? Windows?.ApplicationModel?.Core?.CoreApplication;
+ }
+
+ protected getOrientation(): 'portrait' | 'landscape' | 'unknown' {
+ try {
+ const displayInfo = Windows.Graphics.Display.DisplayInformation.GetForCurrentView();
+ const orientation = displayInfo?.CurrentOrientation;
+ const DisplayOrientations = Windows.Graphics.Display.DisplayOrientations;
+
+ if (orientation === DisplayOrientations.Portrait || orientation === DisplayOrientations.PortraitFlipped) {
+ return 'portrait';
+ }
+
+ if (orientation === DisplayOrientations.Landscape || orientation === DisplayOrientations.LandscapeFlipped) {
+ return 'landscape';
+ }
+ } catch (e) { }
+
+ return 'unknown';
+ }
+
+ protected getSystemAppearance(): 'dark' | 'light' | null {
+ try {
+ const content: any = Windows.UI.Xaml.Window.Current.Content;
+
+ const actualTheme = content?.ActualTheme;
+ const ElementTheme = Windows.UI.Xaml.ElementTheme;
+ if (typeof actualTheme === 'number' && ElementTheme) {
+ if (actualTheme === ElementTheme.Dark) return 'dark';
+ if (actualTheme === ElementTheme.Light) return 'light';
+ }
+
+ const appTheme = Windows.UI.Xaml.Application.Current.RequestedTheme;
+ const ApplicationTheme = Windows.UI.Xaml.ApplicationTheme;
+ if (appTheme === ApplicationTheme.Dark) return 'dark';
+ if (appTheme === ApplicationTheme.Light) return 'light';
+
+ const UISettings = Windows.UI.ViewManagement.UISettings;
+ const ui = new UISettings();
+ const foreground = ui.GetColorValue(Windows.UI.ViewManagement.UIColorType.Foreground);
+ if (!foreground) {
+ return null;
+ }
+
+ // In dark mode Windows uses a light (near-white) foreground color
+ return foreground.R > 128 ? 'dark' : 'light';
+ } catch (e) { }
+
+ return null;
+ }
+
+ protected getLayoutDirection(): CoreTypes.LayoutDirectionType {
+ const content = Windows.UI.Xaml.Window.Current.Content as Windows.UI.Xaml.FrameworkElement | null;
+ if (!content) {
+ return null as never;
+ }
+
+
+ const direction = content.FlowDirection;
+
+ if (direction === Windows.UI.Xaml.FlowDirection.RightToLeft) {
+ return 'rtl';
+ }
+ return 'ltr';
+ }
+
+ private setupLifecycleEvents(): void {
+ const coreApp = Windows.ApplicationModel.Core.CoreApplication;
+
+ if (coreApp) {
+ coreApp.Suspending = (_sender: any, args: any) => this.setSuspended(true, { win: args });
+ coreApp.Resuming = (_sender: any, args: any) => this.setSuspended(false, { win: args });
+ coreApp.EnteredBackground = (_sender: any, args: any) => this.setInBackground(true, { win: args });
+ coreApp.LeavingBackground = (_sender: any, args: any) => this.setInBackground(false, { win: args });
+ coreApp.Exiting = (_sender: any, args: any) => this.notify({ eventName: this.exitEvent, object: this, win: args });
+ coreApp.UnhandledErrorDetected = (_sender: any, args: any) => this.notify({ eventName: this.uncaughtErrorEvent, object: this, win: args });
+ }
+
+ const displayInfo = Windows.Graphics.Display.DisplayInformation.GetForCurrentView();
+ const onOrientationChanged = () => {
+ const newValue = this.getOrientation();
+ this.setOrientation(newValue);
+ };
+
+ if (displayInfo && displayInfo.OrientationChanged !== undefined) {
+ displayInfo.OrientationChanged = () => onOrientationChanged();
+ }
+
+ const ui = new Windows.UI.ViewManagement.UISettings();
+ const onColorValuesChanged = () => {
+ const newSys = this.getSystemAppearance();
+ if (newSys !== null) {
+ this.setSystemAppearance(newSys);
+ }
+ };
+
+ if (ui && ui.ColorValuesChanged !== undefined) {
+ ui.ColorValuesChanged = () => onColorValuesChanged();
+ }
+ }
+}
+
+export const Application: WindowsApplication = new WindowsApplication();
+
+function updateAccessibilityProperties(view: any): void {
+ if (!view || !view.nativeViewProtected) {
+ return;
+ }
+
+ // todo
+}
+
+setA11yUpdatePropertiesCallback(updateAccessibilityProperties);
+
+const applicationEvents: string[] = [Application.orientationChangedEvent, Application.systemAppearanceChangedEvent];
+function toggleApplicationEventListeners(toAdd: boolean, callback: (args: any) => void) {
+ for (const eventName of applicationEvents) {
+ if (toAdd) {
+ Application.on(eventName, callback);
+ } else {
+ Application.off(eventName, callback);
+ }
+ }
+}
+setToggleApplicationEventListenersCallback(toggleApplicationEventListeners);
+
+setApplicationPropertiesCallback(() => {
+ return {
+ orientation: Application.orientation(),
+ systemAppearance: Application.systemAppearance(),
+ };
+});
diff --git a/packages/core/application/helpers.windows.ts b/packages/core/application/helpers.windows.ts
new file mode 100644
index 0000000000..db854a7c42
--- /dev/null
+++ b/packages/core/application/helpers.windows.ts
@@ -0,0 +1,14 @@
+// Stubs to avoid bundler warnings on Windows — mirrors helpers.ios.ts shape.
+export const updateContentDescription = (_view: any, _forceUpdate?: boolean): string | null => null;
+
+export function applyContentDescription(_view: any, _forceUpdate?: boolean) {
+ return null;
+}
+
+export function androidGetCurrentActivity() {}
+export function androidGetForegroundActivity() {}
+export function androidSetForegroundActivity(_activity: any): void {}
+export function androidGetStartActivity() {}
+export function androidSetStartActivity(_activity: any): void {}
+
+export function setupAccessibleView(_view: any): void {}
diff --git a/packages/core/application/index.windows.ts b/packages/core/application/index.windows.ts
new file mode 100644
index 0000000000..c912854dcd
--- /dev/null
+++ b/packages/core/application/index.windows.ts
@@ -0,0 +1,2 @@
+export * from './application.windows';
+export * from './application-shims';
diff --git a/packages/core/color/index.d.ts b/packages/core/color/index.d.ts
index 6e2b10a65c..f73a46e2ce 100644
--- a/packages/core/color/index.d.ts
+++ b/packages/core/color/index.d.ts
@@ -52,6 +52,11 @@ export declare class Color {
*/
ios: any /* UIColor */;
+ /**
+ * Gets the Windows-specific Color value representation. This is a read-only property.
+ */
+ windows: any /* Windows.UI.Color */;
+
/**
* Specifies whether this Color is equal to the Color parameter.
* @param value The Color to test.
diff --git a/packages/core/color/index.windows.ts b/packages/core/color/index.windows.ts
new file mode 100644
index 0000000000..3056a1f44a
--- /dev/null
+++ b/packages/core/color/index.windows.ts
@@ -0,0 +1,22 @@
+import { ColorBase } from './color-common';
+import type { IColor } from './color-types';
+
+export class Color extends ColorBase implements IColor {
+ private _windows: Windows.UI.Color | undefined;
+
+ get windows(): Windows.UI.Color {
+ if (!this._windows) {
+ this._windows = Windows.UI.ColorHelper.FromArgb(Math.round(this.a), Math.round(this.r), Math.round(this.g), Math.round(this.b));
+ }
+
+ return this._windows;
+ }
+
+ get windowsArgb(): number {
+ return ((((this.windows.A & 0xff) << 24) | ((this.windows.R & 0xff) << 16) | ((this.windows.G & 0xff) << 8) | (this.windows.B & 0xff)) >>> 0);
+ }
+
+ public static fromWindowsColor(value: Windows.UI.Color): Color {
+ return new Color(Math.round(value.A), Math.round(value.R), Math.round(value.G), Math.round(value.B));
+ }
+}
diff --git a/packages/core/connectivity/index.windows.ts b/packages/core/connectivity/index.windows.ts
new file mode 100644
index 0000000000..7076c332aa
--- /dev/null
+++ b/packages/core/connectivity/index.windows.ts
@@ -0,0 +1,58 @@
+export enum connectionType {
+ none = 0,
+ wifi = 1,
+ mobile = 2,
+ ethernet = 3,
+ bluetooth = 4,
+ vpn = 5,
+}
+
+function parseConnectionProfile(profile?: Windows.Networking.Connectivity.ConnectionProfile) {
+ if (!profile) return connectionType.none;
+
+ const adapter = profile.NetworkAdapter;
+ if (!adapter) return connectionType.none;
+
+ const type = adapter.IanaInterfaceType;
+
+ switch (type) {
+ case 71:
+ return connectionType.wifi;
+ case 6:
+ return connectionType.ethernet;
+ }
+
+ if (profile.IsWwanConnectionProfile) {
+ return connectionType.mobile;
+ }
+
+ if (profile.IsWlanConnectionProfile && profile.GetNetworkConnectivityLevel?.() === 3) {
+ return connectionType.vpn;
+ }
+
+ return connectionType.none;
+}
+
+export function getConnectionType(): number {
+ const profile = Windows.Networking.Connectivity.NetworkInformation.GetInternetConnectionProfile();
+ return parseConnectionProfile(profile);
+}
+
+let callback;
+let delegate;
+
+export function startMonitoring(connectionTypeChangedCallback: (newConnectionType: number) => void): void {
+ const zoneCallback = zonedCallback(connectionTypeChangedCallback);
+ delegate = NSWinRT.asDelegate(() => {
+ const newConnectionType = getConnectionType();
+ zoneCallback(newConnectionType);
+ });
+
+ Windows.Networking.Connectivity.NetworkInformation.NetworkStatusChanged = delegate;
+}
+
+export function stopMonitoring(): void {
+ Windows.Networking.Connectivity.NetworkInformation.NetworkStatusChanged = null as never;
+ delegate = null;
+ callback = null;
+}
diff --git a/packages/core/debugger/webinspector-network.windows.ts b/packages/core/debugger/webinspector-network.windows.ts
new file mode 100644
index 0000000000..897a4e2b2a
--- /dev/null
+++ b/packages/core/debugger/webinspector-network.windows.ts
@@ -0,0 +1,359 @@
+import * as inspectorCommands from './InspectorBackendCommands';
+import { File, knownFolders } from '../file-system';
+
+import * as debuggerDomains from '.';
+
+declare let __inspectorSendEvent;
+
+declare let __inspectorTimestamp;
+
+const frameId = 'NativeScriptMainFrameIdentifier';
+const loaderId = 'Loader Identifier';
+
+const resources_datas = [];
+
+const documentTypeByMimeType = {
+ 'text/xml': 'Document',
+ 'text/plain': 'Document',
+ 'text/html': 'Document',
+ 'application/xml': 'Document',
+ 'application/xhtml+xml': 'Document',
+ 'text/css': 'Stylesheet',
+ 'text/javascript': 'Script',
+ 'text/ecmascript': 'Script',
+ 'application/javascript': 'Script',
+ 'application/ecmascript': 'Script',
+ 'application/x-javascript': 'Script',
+ 'application/json': 'Script',
+ 'application/x-json': 'Script',
+ 'text/x-javascript': 'Script',
+ 'text/x-json': 'Script',
+ 'text/typescript': 'Script',
+};
+
+export class Request {
+ private _resourceType: string;
+ private _data: any;
+ private _mimeType: string;
+
+ constructor(
+ private _networkDomainDebugger: NetworkDomainDebugger,
+ private _requestID: string,
+ ) {}
+
+ get mimeType(): string {
+ return this._mimeType;
+ }
+
+ set mimeType(value: string) {
+ if (this._mimeType !== value) {
+ if (!value) {
+ this._mimeType = 'text/plain';
+ this._resourceType = 'Other';
+
+ return;
+ }
+
+ this._mimeType = value;
+
+ let resourceType = 'Other';
+
+ if (this._mimeType in documentTypeByMimeType) {
+ resourceType = documentTypeByMimeType[this._mimeType];
+ }
+
+ if (this._mimeType.indexOf('image/') !== -1) {
+ resourceType = 'Image';
+ }
+
+ if (this._mimeType.indexOf('font/') !== -1) {
+ resourceType = 'Font';
+ }
+
+ this._resourceType = resourceType;
+ }
+ }
+
+ get requestID(): string {
+ return this._requestID;
+ }
+
+ get hasTextContent(): boolean {
+ return ['Document', 'Stylesheet', 'Script', 'XHR'].indexOf(this._resourceType) !== -1;
+ }
+
+ get data(): any {
+ return this._data;
+ }
+
+ set data(value: any) {
+ if (this._data !== value) {
+ this._data = value;
+ }
+ }
+
+ get resourceType() {
+ return this._resourceType;
+ }
+
+ set resourceType(value: string) {
+ if (this._resourceType !== value) {
+ this._resourceType = value;
+ }
+ }
+
+ public responseReceived(response: inspectorCommands.NetworkDomain.Response): void {
+ if (this._networkDomainDebugger.enabled) {
+ this._networkDomainDebugger.events.responseReceived(this.requestID, frameId, loaderId, __inspectorTimestamp(), this.resourceType, response);
+ }
+ }
+
+ public loadingFinished(): void {
+ if (this._networkDomainDebugger.enabled) {
+ this._networkDomainDebugger.events.loadingFinished(this.requestID, __inspectorTimestamp());
+ }
+ }
+
+ public requestWillBeSent(request: inspectorCommands.NetworkDomain.Request): void {
+ if (this._networkDomainDebugger.enabled) {
+ this._networkDomainDebugger.events.requestWillBeSent(this.requestID, frameId, loaderId, request.url, request, __inspectorTimestamp(), { type: 'Script' });
+ }
+ }
+}
+
+function arrayBufferToString(buf: ArrayBuffer | Uint8Array): string {
+ try {
+ if (typeof Buffer !== 'undefined' && Buffer.from) {
+ if (buf instanceof ArrayBuffer) {
+ return Buffer.from(new Uint8Array(buf)).toString('utf8');
+ }
+ return Buffer.from(buf as Uint8Array).toString('utf8');
+ }
+
+ if (typeof TextDecoder !== 'undefined') {
+ return new TextDecoder('utf-8').decode(buf instanceof ArrayBuffer ? new Uint8Array(buf) : (buf as Uint8Array));
+ }
+
+ // Fallback manual decode
+ const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : (buf as Uint8Array);
+ let str = '';
+ for (let i = 0; i < bytes.length; i++) {
+ str += String.fromCharCode(bytes[i]);
+ }
+ return str;
+ } catch (e) {
+ return '';
+ }
+}
+
+function arrayBufferToBase64(buf: ArrayBuffer | Uint8Array): string {
+ try {
+ if (typeof Buffer !== 'undefined' && Buffer.from) {
+ if (buf instanceof ArrayBuffer) {
+ return Buffer.from(new Uint8Array(buf)).toString('base64');
+ }
+ return Buffer.from(buf as Uint8Array).toString('base64');
+ }
+
+ // Use Windows API if available
+ try {
+ if (typeof Windows !== 'undefined' && Windows.Security && Windows.Security.Cryptography) {
+ const writer = new Windows.Storage.Streams.DataWriter();
+ if (buf instanceof ArrayBuffer) {
+ writer.WriteBytes(new Uint8Array(buf) as never);
+ } else {
+ writer.WriteBytes(buf as never);
+ }
+ const iBuf = writer.DetachBuffer();
+ return Windows.Security.Cryptography.CryptographicBuffer.EncodeToBase64String(iBuf as any);
+ }
+ } catch (e) {}
+
+ // Fallback to btoa over chunks
+ const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : (buf as Uint8Array);
+ let binary = '';
+ const chunkSize = 0x8000;
+ for (let i = 0; i < bytes.length; i += chunkSize) {
+ const chunk = bytes.subarray(i, i + chunkSize);
+ binary += String.fromCharCode.apply(null, Array.prototype.slice.call(chunk));
+ }
+ if (typeof btoa !== 'undefined') {
+ return btoa(binary);
+ }
+ return '';
+ } catch (e) {
+ return '';
+ }
+}
+
+@inspectorCommands.DomainDispatcher('Network')
+export class NetworkDomainDebugger implements inspectorCommands.NetworkDomain.NetworkDomainDispatcher {
+ private _enabled: boolean;
+ public events: inspectorCommands.NetworkDomain.NetworkFrontend;
+
+ constructor() {
+ this.events = new inspectorCommands.NetworkDomain.NetworkFrontend();
+
+ // By default start enabled because we can miss the "enable" event when
+ // running with `--debug-brk` -- the frontend will send it before we've been created
+ this.enable();
+ }
+
+ get enabled(): boolean {
+ return this._enabled;
+ }
+
+ /**
+ * Enables network tracking, network events will now be delivered to the client.
+ */
+ enable(): void {
+ if (debuggerDomains.getNetwork()) {
+ throw new Error('One NetworkDomainDebugger may be enabled at a time.');
+ } else {
+ debuggerDomains.setNetwork(this);
+ }
+ this._enabled = true;
+ }
+
+ /**
+ * Disables network tracking, prevents network events from being sent to the client.
+ */
+ disable(): void {
+ if (debuggerDomains.getNetwork() === this) {
+ debuggerDomains.setNetwork(null);
+ }
+ this._enabled = false;
+ }
+
+ /**
+ * Specifies whether to always send extra HTTP headers with the requests from this page.
+ */
+ setExtraHTTPHeaders(params: inspectorCommands.NetworkDomain.SetExtraHTTPHeadersMethodArguments): void {
+ //
+ }
+
+ /**
+ * Returns content served for the given request.
+ */
+ getResponseBody(params: inspectorCommands.NetworkDomain.GetResponseBodyMethodArguments): { body: string; base64Encoded: boolean } {
+ const resource_data = resources_datas[params.requestId];
+ if (!resource_data) {
+ return { body: '', base64Encoded: false };
+ }
+
+ // Try to handle multiple data shapes across platforms/runtimes
+ // Prefer text decoding for text-like resources
+ try {
+ if (resource_data.hasTextContent) {
+ if (typeof resource_data.data === 'string') {
+ return { body: resource_data.data, base64Encoded: false };
+ }
+ if (resource_data.data instanceof ArrayBuffer || resource_data.data instanceof Uint8Array) {
+ return { body: arrayBufferToString(resource_data.data), base64Encoded: false };
+ }
+ // Fallback: attempt toString
+ return { body: resource_data.data?.toString() ?? '', base64Encoded: false };
+ } else {
+ if (resource_data.data instanceof ArrayBuffer || resource_data.data instanceof Uint8Array) {
+ return { body: arrayBufferToBase64(resource_data.data), base64Encoded: true };
+ }
+
+ // If underlying platform produced a Windows IBuffer or similar, try Windows API
+ try {
+ if (typeof Windows !== 'undefined' && Windows.Security && Windows.Security.Cryptography && resource_data.data && (resource_data.data as any).Length !== undefined) {
+ // resource_data.data is likely an IBuffer-like object
+ return { body: Windows.Security.Cryptography.CryptographicBuffer.EncodeToBase64String(resource_data.data as any), base64Encoded: true };
+ }
+ } catch (e) {}
+
+ // Last-ditch: attempt Buffer or btoa
+ if (typeof Buffer !== 'undefined' && Buffer.from) {
+ try {
+ return { body: Buffer.from(resource_data.data as any).toString('base64'), base64Encoded: true };
+ } catch (e) {}
+ }
+ // fallback empty
+ return { body: '', base64Encoded: true };
+ }
+ } catch (e) {
+ return { body: '', base64Encoded: !resource_data.hasTextContent };
+ }
+ }
+
+ /**
+ * Tells whether clearing browser cache is supported.
+ */
+ canClearBrowserCache(): { result: boolean } {
+ return {
+ result: false,
+ };
+ }
+
+ /**
+ * Clears browser cache.
+ */
+ clearBrowserCache(): void {
+ //
+ }
+
+ /**
+ * Tells whether clearing browser cookies is supported.
+ */
+ canClearBrowserCookies(): { result: boolean } {
+ return {
+ result: false,
+ };
+ }
+
+ /**
+ * Clears browser cookies.
+ */
+ clearBrowserCookies(): void {
+ //
+ }
+
+ /**
+ * Toggles ignoring cache for each request. If true, cache will not be used.
+ */
+ setCacheDisabled(params: inspectorCommands.NetworkDomain.SetCacheDisabledMethodArguments): void {
+ //
+ }
+
+ /**
+ * Loads a resource in the context of a frame on the inspected page without cross origin checks.
+ */
+ loadResource(params: inspectorCommands.NetworkDomain.LoadResourceMethodArguments): { content: string; mimeType: string; status: number } {
+ const appPath = knownFolders.currentApp().path;
+ // Normalize incoming url like: file:// or file:///app/...
+ let rel = params.url.replace(/^file:\/\/?/, '');
+ if (rel.startsWith('app/')) {
+ rel = rel.substr(4);
+ }
+ const pathUrl = `${appPath}/${rel}`;
+ const file = File.exists(pathUrl) ? File.fromPath(pathUrl) : undefined;
+ let content = '';
+ try {
+ if (file) {
+ // Prefer text read for resources; binary consumers will request via getResponseBody
+ content = file.readTextSync();
+ }
+ } catch (e) {
+ content = '';
+ }
+
+ return {
+ content: content.toString(),
+ mimeType: 'application/octet-stream',
+ status: 200,
+ };
+ }
+
+ public static idSequence = 0;
+ create(): Request {
+ const id = (++NetworkDomainDebugger.idSequence).toString();
+ const resourceData = new Request(this, id);
+ resources_datas[id] = resourceData;
+
+ return resourceData;
+ }
+}
diff --git a/packages/core/file-system/file-system-access.windows.ts b/packages/core/file-system/file-system-access.windows.ts
new file mode 100644
index 0000000000..52ea728552
--- /dev/null
+++ b/packages/core/file-system/file-system-access.windows.ts
@@ -0,0 +1,456 @@
+import { encoding as textEncoding } from '../text';
+
+function getPathSep(): string {
+ return '\\';
+}
+
+function getFileName(path: string): string {
+ const sep = path.lastIndexOf('\\') !== -1 ? '\\' : '/';
+ return path.substring(path.lastIndexOf(sep) + 1);
+}
+
+function getExtension(name: string): string {
+ const dot = name.lastIndexOf('.');
+ return dot >= 0 ? name.substring(dot) : '';
+}
+
+export class FileSystemAccess {
+ public getLastModified(path: string): Date {
+ try {
+ const file = Windows.Storage.StorageFile.GetFileFromPathAsync(path) as any;
+ return file?.DateCreated ? new Date(file.DateCreated) : new Date();
+ } catch {
+ return new Date();
+ }
+ }
+
+ public getFileSize(path: string): number {
+ try {
+ const props = (Windows.Storage.StorageFile.GetFileFromPathAsync(path) as any)?.GetBasicPropertiesAsync?.();
+ return props?.Size ?? 0;
+ } catch {
+ return 0;
+ }
+ }
+
+ public getParent(path: string, onError?: (error: any) => any): { path: string; name: string } {
+ try {
+ const sep = path.lastIndexOf('\\') !== -1 ? '\\' : '/';
+ const parentPath = path.substring(0, path.lastIndexOf(sep)) || path;
+ const name = getFileName(parentPath);
+ return { path: parentPath, name };
+ } catch (ex) {
+ if (onError) onError(ex);
+ return undefined;
+ }
+ }
+
+ public getFile(path: string, onError?: (error: any) => any): { path: string; name: string; extension: string } {
+ try {
+ if (!this.fileExists(path)) {
+ this._ensureParentDir(path);
+ Windows.Storage.PathIO.WriteTextAsync(path, '');
+ }
+ const name = getFileName(path);
+ return { path, name, extension: getExtension(name) };
+ } catch (ex) {
+ if (onError) onError(ex);
+ return undefined;
+ }
+ }
+
+ public getFolder(path: string, onError?: (error: any) => any): { path: string; name: string } {
+ try {
+ if (!this.folderExists(path)) {
+ Windows.Storage.StorageFolder.GetFolderFromPathAsync(path);
+ }
+ return { path, name: getFileName(path) };
+ } catch (ex) {
+ if (onError) onError(ex);
+ return undefined;
+ }
+ }
+
+ public getExistingFolder(path: string, onError?: (error: any) => any): { path: string; name: string } {
+ try {
+ if (this.folderExists(path)) {
+ return { path, name: getFileName(path) };
+ }
+ return undefined;
+ } catch (ex) {
+ if (onError) onError(ex);
+ return undefined;
+ }
+ }
+
+ public eachEntity(path: string, onEntity: (file: { path: string; name: string; extension: string }) => any, onError?: (error: any) => any) {
+ if (!onEntity) return;
+ this._enumEntities(path, onEntity, onError);
+ }
+
+ public getEntities(path: string, onError?: (error: any) => any): Array<{ path: string; name: string; extension: string }> {
+ const results: Array<{ path: string; name: string; extension: string }> = [];
+ let errored = false;
+ this._enumEntities(
+ path,
+ (entity) => {
+ results.push(entity);
+ return true;
+ },
+ (err) => {
+ if (onError) onError(err);
+ errored = true;
+ },
+ );
+ return errored ? null : results;
+ }
+
+ public fileExists(path: string): boolean {
+ try {
+ Windows.Storage.StorageFile.GetFileFromPathAsync(path);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ public folderExists(path: string): boolean {
+ try {
+ Windows.Storage.StorageFolder.GetFolderFromPathAsync(path);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ public deleteFile(path: string, onError?: (error: any) => any) {
+ try {
+ const file = Windows.Storage.StorageFile.GetFileFromPathAsync(path) as any;
+ file?.DeleteAsync?.();
+ } catch (ex) {
+ if (onError) onError(ex);
+ }
+ }
+
+ public deleteFolder(path: string, onError?: (error: any) => any) {
+ try {
+ const folder = Windows.Storage.StorageFolder.GetFolderFromPathAsync(path) as any;
+ folder?.DeleteAsync?.();
+ } catch (ex) {
+ if (onError) onError(ex);
+ }
+ }
+
+ public emptyFolder(path: string, onError?: (error: any) => any) {
+ const entities = this.getEntities(path, onError);
+ if (!entities) return;
+ for (const entity of entities) {
+ try {
+ this.deleteFile(entity.path, onError);
+ } catch (ex) {
+ if (onError) onError(ex);
+ return;
+ }
+ }
+ }
+
+ public rename(path: string, newPath: string, onError?: (error: any) => any) {
+ try {
+ const file = Windows.Storage.StorageFile.GetFileFromPathAsync(path) as any;
+ const newName = getFileName(newPath);
+ file?.RenameAsync?.(newName);
+ } catch (ex) {
+ if (onError) onError(new Error(`Failed to rename '${path}' to '${newPath}': ${ex}`));
+ }
+ }
+
+ public readText = this.readTextSync.bind(this);
+
+ public readTextAsync(path: string, encoding?: any): Promise {
+ const enc = encoding ?? textEncoding.UTF_8;
+ return new Promise((resolve, reject) => {
+ try {
+ (Windows.Storage.PathIO.ReadTextAsync(path) as any).then(resolve, reject);
+ } catch (ex) {
+ reject(new Error(`Failed to read file at path '${path}': ${ex}`));
+ }
+ });
+ }
+
+ public readTextSync(path: string, onError?: (error: any) => any, _encoding?: any): string {
+ try {
+ let result: string;
+ (Windows.Storage.PathIO.ReadTextAsync(path) as any).done((text: string) => {
+ result = text;
+ });
+ return result ?? '';
+ } catch (ex) {
+ if (onError) onError(new Error(`Failed to read file at path '${path}': ${ex}`));
+ return undefined;
+ }
+ }
+
+ public writeText = this.writeTextSync.bind(this);
+
+ public writeTextAsync(path: string, content: string, _encoding?: any): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ (Windows.Storage.PathIO.WriteTextAsync(path, content) as any).then(resolve, reject);
+ } catch (ex) {
+ reject(new Error(`Failed to write file at path '${path}': ${ex}`));
+ }
+ });
+ }
+
+ public writeTextSync(path: string, content: string, onError?: (error: any) => any, _encoding?: any) {
+ try {
+ (Windows.Storage.PathIO.WriteTextAsync(path, content) as any).done();
+ } catch (ex) {
+ if (onError) onError(new Error(`Failed to write file at path '${path}': ${ex}`));
+ }
+ }
+
+ public appendText = this.appendTextSync.bind(this);
+
+ public appendTextAsync(path: string, content: string, _encoding?: any): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ (Windows.Storage.PathIO.AppendTextAsync(path, content) as any).then(resolve, reject);
+ } catch (ex) {
+ reject(new Error(`Failed to append to file at path '${path}': ${ex}`));
+ }
+ });
+ }
+
+ public appendTextSync(path: string, content: string, onError?: (error: any) => any, _encoding?: any) {
+ try {
+ (Windows.Storage.PathIO.AppendTextAsync(path, content) as any).done();
+ } catch (ex) {
+ if (onError) onError(new Error(`Failed to append to file at path '${path}': ${ex}`));
+ }
+ }
+
+ public readBuffer = this.readBufferSync.bind(this);
+
+ public readBufferAsync(path: string): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ (Windows.Storage.PathIO.ReadBufferAsync(path) as any).then((buffer: any) => {
+ const reader = Windows.Storage.Streams.DataReader.FromBuffer(buffer);
+ const bytes = new Uint8Array(buffer.Length);
+ reader.ReadBytes(bytes);
+ resolve(bytes.buffer);
+ }, reject);
+ } catch (ex) {
+ reject(new Error(`Failed to read buffer at path '${path}': ${ex}`));
+ }
+ });
+ }
+
+ public readBufferSync(path: string, onError?: (error: any) => any): ArrayBuffer {
+ try {
+ let result: ArrayBuffer;
+ (Windows.Storage.PathIO.ReadBufferAsync(path) as any).done((buffer: any) => {
+ const reader = Windows.Storage.Streams.DataReader.FromBuffer(buffer);
+ const bytes = new Uint8Array(buffer.Length);
+ reader.ReadBytes(bytes);
+ result = bytes.buffer;
+ });
+ return result;
+ } catch (ex) {
+ if (onError) onError(new Error(`Failed to read buffer at path '${path}': ${ex}`));
+ return undefined;
+ }
+ }
+
+ public writeBuffer = this.writeBufferSync.bind(this);
+
+ public writeBufferAsync(path: string, content: ArrayBuffer): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ const writer = new Windows.Storage.Streams.DataWriter();
+ writer.WriteBytes(new Uint8Array(content));
+ const buf = writer.DetachBuffer();
+ (Windows.Storage.PathIO.WriteBufferAsync(path, buf) as any).then(resolve, reject);
+ } catch (ex) {
+ reject(new Error(`Failed to write buffer at path '${path}': ${ex}`));
+ }
+ });
+ }
+
+ public writeBufferSync(path: string, content: ArrayBuffer, onError?: (error: any) => any) {
+ try {
+ const writer = new Windows.Storage.Streams.DataWriter();
+ writer.WriteBytes(new Uint8Array(content));
+ const buf = writer.DetachBuffer();
+ (Windows.Storage.PathIO.WriteBufferAsync(path, buf) as any).done();
+ } catch (ex) {
+ if (onError) onError(new Error(`Failed to write buffer at path '${path}': ${ex}`));
+ }
+ }
+
+ public read = this.readSync.bind(this);
+
+ public readAsync(path: string): Promise {
+ return this.readBufferAsync(path);
+ }
+
+ public readSync(path: string, onError?: (error: any) => any): any {
+ return this.readBufferSync(path, onError);
+ }
+
+ public write = this.writeSync.bind(this);
+
+ public writeAsync(path: string, content: any): Promise {
+ return this.writeBufferAsync(path, content);
+ }
+
+ public writeSync(path: string, content: any, onError?: (error: any) => any) {
+ return this.writeBufferSync(path, content, onError);
+ }
+
+ public append = this.appendSync.bind(this);
+
+ public appendAsync(path: string, content: any): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ const writer = new Windows.Storage.Streams.DataWriter();
+ writer.WriteBytes(new Uint8Array(content));
+ const buf = writer.DetachBuffer();
+ (Windows.Storage.PathIO.WriteBufferAsync(path, buf) as any).then(resolve, reject);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ }
+
+ public appendSync(path: string, content: any, onError?: (error: any) => any) {
+ try {
+ const writer = new Windows.Storage.Streams.DataWriter();
+ writer.WriteBytes(new Uint8Array(content));
+ const buf = writer.DetachBuffer();
+ (Windows.Storage.PathIO.WriteBufferAsync(path, buf) as any).done();
+ } catch (ex) {
+ if (onError) onError(ex);
+ }
+ }
+
+ public copy = this.copySync.bind(this);
+
+ public copySync(src: string, dest: string, onError?: (error: any) => any): boolean {
+ try {
+ const srcFile = Windows.Storage.StorageFile.GetFileFromPathAsync(src) as any;
+ const destParent = this.getParent(dest)?.path;
+ const destFolder = Windows.Storage.StorageFolder.GetFolderFromPathAsync(destParent) as any;
+ const destName = getFileName(dest);
+ srcFile?.CopyAsync?.(destFolder, destName, Windows.Storage.NameCollisionOption.ReplaceExisting);
+ return true;
+ } catch (ex) {
+ if (onError) onError(ex);
+ return false;
+ }
+ }
+
+ public copyAsync(src: string, dest: string): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ (Windows.Storage.StorageFile.GetFileFromPathAsync(src) as any).then((srcFile: any) => {
+ const destParent = this.getParent(dest)?.path;
+ (Windows.Storage.StorageFolder.GetFolderFromPathAsync(destParent) as any).then((destFolder: any) => {
+ const destName = getFileName(dest);
+ srcFile.CopyAsync(destFolder, destName, Windows.Storage.NameCollisionOption.ReplaceExisting).then(() => resolve(true), reject);
+ }, reject);
+ }, reject);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ }
+
+ public getPathSeparator(): string {
+ return getPathSep();
+ }
+
+ public normalizePath(path: string): string {
+ return path.replace(/\//g, '\\');
+ }
+
+ public joinPath(left: string, right: string): string {
+ const l = left.endsWith('\\') || left.endsWith('/') ? left.slice(0, -1) : left;
+ const r = right.startsWith('\\') || right.startsWith('/') ? right.slice(1) : right;
+ return `${l}\\${r}`;
+ }
+
+ public joinPaths(...paths: string[]): string {
+ return paths.reduce((acc, p) => this.joinPath(acc, p));
+ }
+
+ public getDocumentsFolderPath(): string {
+ try {
+ return Windows.Storage.ApplicationData.Current.LocalFolder.Path;
+ } catch {
+ return '';
+ }
+ }
+
+ public getExternalDocumentsFolderPath(): string {
+ return this.getDocumentsFolderPath();
+ }
+
+ public getTempFolderPath(): string {
+ try {
+ return Windows.Storage.ApplicationData.Current.TemporaryFolder.Path;
+ } catch {
+ return '';
+ }
+ }
+
+ public getCurrentAppPath(): string {
+ try {
+ return Windows.ApplicationModel.Package.Current.InstalledLocation.Path;
+ } catch {
+ if (!global.__dirname) {
+ global.__dirname = typeof __dirname !== 'undefined' ? __dirname : '';
+ }
+ return global.__dirname;
+ }
+ }
+
+ public getLogicalRootPath(): string {
+ return this.getCurrentAppPath();
+ }
+
+ private _ensureParentDir(path: string): void {
+ const parent = this.getParent(path)?.path;
+ if (parent && !this.folderExists(parent)) {
+ try {
+ const parts = parent.split('\\');
+ let current = parts[0];
+ for (let i = 1; i < parts.length; i++) {
+ current += '\\' + parts[i];
+ if (!this.folderExists(current)) {
+ (Windows.Storage.StorageFolder.GetFolderFromPathAsync(current) as any);
+ }
+ }
+ } catch {}
+ }
+ }
+
+ private _enumEntities(path: string, onEntity: (file: { path: string; name: string; extension: string }) => any, onError?: (error: any) => any) {
+ try {
+ (Windows.Storage.StorageFolder.GetFolderFromPathAsync(path) as any).done((folder: any) => {
+ (folder.GetItemsAsync() as any).done((items: any) => {
+ for (let i = 0; i < items.Size; i++) {
+ const item = items.GetAt(i);
+ const name = item.Name;
+ const itemPath = item.Path;
+ const cont = onEntity({ path: itemPath, name, extension: getExtension(name) });
+ if (cont === false) break;
+ }
+ });
+ });
+ } catch (ex) {
+ if (onError) onError(ex);
+ }
+ }
+}
diff --git a/packages/core/fps-meter/fps-native.windows.ts b/packages/core/fps-meter/fps-native.windows.ts
new file mode 100644
index 0000000000..69b6dcf22a
--- /dev/null
+++ b/packages/core/fps-meter/fps-native.windows.ts
@@ -0,0 +1,44 @@
+export class FPSCallback {
+ public running: boolean = false;
+ private onFrame: (currentTimeMillis: number) => void;
+ private _timer: any = null;
+
+ constructor(onFrame: (currentTimeMillis: number) => void) {
+ this.onFrame = onFrame;
+ }
+
+ public start() {
+ if (this.running) {
+ return;
+ }
+ this.running = true;
+
+ try {
+ const timer = new Windows.UI.Xaml.DispatcherTimer();
+ const interval = { Duration: 160000 }; // 16ms in 100-nanosecond ticks
+ timer.Interval = interval as any;
+ timer.Tick = NSWinRT.asDelegate(() => {
+ if (this.running) {
+ this.onFrame(Date.now());
+ }
+ });
+ timer.Start();
+ this._timer = timer;
+ } catch {
+ // Fallback: no-op if WinRT not available
+ }
+ }
+
+ public stop() {
+ if (!this.running) {
+ return;
+ }
+ this.running = false;
+ if (this._timer) {
+ try {
+ this._timer.Stop();
+ } catch {}
+ this._timer = null;
+ }
+ }
+}
diff --git a/packages/core/global-types.d.ts b/packages/core/global-types.d.ts
index 78ca864489..d003c79cec 100644
--- a/packages/core/global-types.d.ts
+++ b/packages/core/global-types.d.ts
@@ -135,6 +135,7 @@ declare const __ANDROID__: boolean;
declare const __IOS__: boolean;
declare const __VISIONOS__: boolean;
declare const __APPLE__: boolean;
+declare const __WINDOWS__: boolean;
declare function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): number;
declare function clearTimeout(timeoutId: number): void;
diff --git a/packages/core/http/http-request-internal/index.windows.ts b/packages/core/http/http-request-internal/index.windows.ts
new file mode 100644
index 0000000000..53be0db452
--- /dev/null
+++ b/packages/core/http/http-request-internal/index.windows.ts
@@ -0,0 +1,145 @@
+import type { HttpRequestOptions, HttpResponse, Headers } from '../http-interfaces';
+import { HttpResponseEncoding } from '../http-interfaces';
+import { BaseHttpContent } from '.';
+import { addHeader } from './http-request-internal-common';
+import { isMainThread, dispatchToMainThread } from '../../utils/mainthread-helper';
+export { addHeader } from './http-request-internal-common';
+
+export function requestInternal(options: HttpRequestOptions, contentHandler?: T): Promise> {
+ return new Promise>((resolve, reject) => {
+ if (!options.url) {
+ reject(new Error('Request url was empty.'));
+ return;
+ }
+
+ try {
+ const httpClient = new Windows.Web.Http.HttpClient();
+ const uri = new Windows.Foundation.Uri(options.url.trim());
+ const method = new Windows.Web.Http.HttpMethod(options.method || 'GET');
+ const requestMessage = new Windows.Web.Http.HttpRequestMessage(method, uri);
+
+ if (options.headers) {
+ for (const key in options.headers) {
+ try {
+ requestMessage.Headers.TryAppendWithoutValidation(key, options.headers[key] + '');
+ } catch { }
+ }
+ }
+
+ if (options.content) {
+ if (typeof options.content === 'string') {
+ requestMessage.Content = new Windows.Web.Http.HttpStringContent(options.content);
+ } else if (options.content instanceof FormData) {
+ requestMessage.Content = new Windows.Web.Http.HttpStringContent(options.content.toString());
+ } else if (options.content instanceof ArrayBuffer) {
+ const writer = new Windows.Storage.Streams.DataWriter();
+ writer.WriteBytes(new Uint8Array(options.content as ArrayBuffer) as never);
+ requestMessage.Content = new Windows.Web.Http.HttpBufferContent(writer.DetachBuffer());
+ }
+ }
+
+
+ NSWinRT.toPromise(httpClient.SendRequestAsync(requestMessage)).then(
+ (response: Windows.Web.Http.HttpResponseMessage) => {
+ const statusCode: number = response.StatusCode as unknown as number;
+ const headers: Headers = {};
+
+ try {
+ const Headers = response.Headers;
+ const first = Headers.First();
+ while (first && first.HasCurrent) {
+ const header = first.Current;
+ addHeader(headers, header.Key, header.Value);
+ first.MoveNext();
+ }
+ } catch { }
+ try {
+ const Headers = response.Content.Headers;
+ const first = Headers.First();
+ while (first && first.HasCurrent) {
+ const header = first.Current;
+ addHeader(headers, header.Key, header.Value);
+ first.MoveNext();
+ }
+ } catch { }
+
+ NSWinRT.toPromise(response.Content.ReadAsBufferAsync()).then(
+ (buffer: Windows.Storage.Streams.IBuffer) => {
+ let bytes: Uint8Array;
+ try {
+ const reader = Windows.Storage.Streams.DataReader.FromBuffer(buffer);
+ bytes = new Uint8Array(buffer.Length);
+ reader.ReadBytes(bytes as never);
+ } catch {
+ bytes = new Uint8Array(0);
+ }
+
+ const rawBuffer = bytes.buffer as ArrayBuffer;
+
+ const content = {
+ raw: rawBuffer,
+ requestURL: options.url,
+ toNativeImage: (): Promise => {
+ return new Promise((resolve, reject) => {
+ try {
+ const writer = new Windows.Storage.Streams.DataWriter();
+ writer.WriteBytes(bytes as never);
+ const freshBuffer = writer.DetachBuffer();
+ const stream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
+ NSWinRT.toPromise((stream as any).WriteAsync(freshBuffer)).then(() => {
+ const createBitmap = () => {
+ try {
+ (stream as any).Seek(0);
+ const bmp = new Windows.UI.Xaml.Media.Imaging.BitmapImage();
+ NSWinRT.toPromise(bmp.SetSourceAsync(stream)).then(
+ () => { stream.Close(); resolve(bmp); },
+ reject
+ );
+ } catch (e) { reject(e); }
+ };
+ if (isMainThread()) {
+ createBitmap();
+ } else {
+ dispatchToMainThread(createBitmap);
+ }
+ }, reject);
+ } catch (e) { reject(e); }
+ });
+ },
+ toNativeString: (encoding?: HttpResponseEncoding) => {
+ return BufferToString(buffer, encoding);
+ },
+ };
+
+ if (contentHandler != null && typeof contentHandler === 'object' && !Array.isArray(contentHandler)) {
+ Object.assign(content, contentHandler);
+ }
+
+ resolve({
+ content: content as BaseHttpContent & T,
+ statusCode,
+ headers,
+ });
+ },
+ (err: any) => reject(new Error(err?.message ?? String(err)))
+ );
+ },
+ ).catch((err: any) => {
+ reject(new Error(err?.message ?? String(err)));
+ });
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+}
+
+
+function BufferToString(buffer: Windows.Storage.Streams.IBuffer, encoding?: HttpResponseEncoding): string {
+ let encodingType = Windows.Security.Cryptography.BinaryStringEncoding.Utf8;
+ if (encoding === HttpResponseEncoding.GBK) {
+ encodingType = Windows.Security.Cryptography.BinaryStringEncoding.Utf8; // Windows does not support GBK, fallback to UTF-8
+ }
+ return Windows.Security.Cryptography.CryptographicBuffer.ConvertBinaryToString(
+ encodingType,
+ buffer);
+}
\ No newline at end of file
diff --git a/packages/core/http/http-request/index.windows.ts b/packages/core/http/http-request/index.windows.ts
new file mode 100644
index 0000000000..334855d157
--- /dev/null
+++ b/packages/core/http/http-request/index.windows.ts
@@ -0,0 +1,44 @@
+import type { HttpResponse, HttpRequestOptions, HttpContentHandler } from '../../http';
+import { ImageSource } from '../../image-source';
+import { File } from '../../file-system';
+import type { HttpResponseEncoding } from '../http-interfaces';
+import { BaseHttpContent, requestInternal } from '../http-request-internal';
+import { getFilenameFromUrl, parseJSON } from './http-request-common';
+
+const contentHandler: HttpContentHandler = {
+ toArrayBuffer(this: BaseHttpContent) {
+ if (this.raw instanceof ArrayBuffer) {
+ return this.raw;
+ }
+ if (this.raw instanceof Uint8Array) {
+ return this.raw.buffer;
+ }
+ return new ArrayBuffer(0);
+ },
+ toString(this: BaseHttpContent, _encoding?: HttpResponseEncoding) {
+ const str = this.toNativeString(_encoding);
+ if (typeof str === 'string') {
+ return str;
+ }
+ throw new Error('Response content may not be converted to string');
+ },
+ toJSON(this: BaseHttpContent, encoding?: HttpResponseEncoding) {
+ return parseJSON(this.toNativeString(encoding));
+ },
+ toImage(this: BaseHttpContent) {
+ return this.toNativeImage().then((value) => new ImageSource(value));
+ },
+ toFile(this: BaseHttpContent, destinationFilePath?: string) {
+ if (!destinationFilePath) {
+ destinationFilePath = getFilenameFromUrl(this.requestURL);
+ }
+ const file = File.fromPath(destinationFilePath);
+ const data = this.raw instanceof ArrayBuffer ? this.raw : (this.raw instanceof Uint8Array ? this.raw.buffer : new ArrayBuffer(0));
+ file.writeSync(data);
+ return file;
+ },
+};
+
+export function request(options: HttpRequestOptions): Promise {
+ return requestInternal(options, contentHandler);
+}
diff --git a/packages/core/image-asset/index.windows.ts b/packages/core/image-asset/index.windows.ts
new file mode 100644
index 0000000000..fd44202d9c
--- /dev/null
+++ b/packages/core/image-asset/index.windows.ts
@@ -0,0 +1,46 @@
+import { ImageAssetBase, getRequestedImageSize } from './image-asset-common';
+import { path as fsPath, knownFolders } from '../file-system';
+
+export * from './image-asset-common';
+
+export class ImageAsset extends ImageAssetBase {
+ private _windows: any;
+
+ constructor(asset: string | any) {
+ super();
+ if (typeof asset === 'string') {
+ if (asset.indexOf('~/') === 0) {
+ asset = fsPath.join(knownFolders.currentApp().path, asset.replace('~/', ''));
+ }
+ this._windows = asset;
+ } else {
+ this._windows = asset;
+ }
+ }
+
+ get windows(): any {
+ return this._windows;
+ }
+
+ set windows(value: any) {
+ this._windows = value;
+ }
+
+ public getImageAsync(callback: (image: any, error: any) => void) {
+ try {
+ if (typeof this._windows === 'string') {
+ const path = this._windows;
+ (Windows.Storage.StorageFile.GetFileFromPathAsync(path) as any).then(
+ (file: any) => {
+ callback(file, null);
+ },
+ (err: any) => callback(null, err),
+ );
+ } else {
+ callback(this._windows, null);
+ }
+ } catch (ex) {
+ callback(null, ex);
+ }
+ }
+}
diff --git a/packages/core/image-source/index.d.ts b/packages/core/image-source/index.d.ts
index 95d086d474..86157737e2 100644
--- a/packages/core/image-source/index.d.ts
+++ b/packages/core/image-source/index.d.ts
@@ -31,6 +31,11 @@ export class ImageSource {
*/
android: any /* android.graphics.Bitmap */;
+ /**
+ * The Windows-specific [image](https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.imagesource) instance. Will be undefined when running on iOS or Android.
+ */
+ windows: any /* Windows.UI.Xaml.Media.ImageSource */;
+
/**
* Loads this instance from the specified asset asynchronously.
* @param asset The ImageAsset instance used to create ImageSource.
diff --git a/packages/core/image-source/index.windows.ts b/packages/core/image-source/index.windows.ts
new file mode 100644
index 0000000000..28f845d5b4
--- /dev/null
+++ b/packages/core/image-source/index.windows.ts
@@ -0,0 +1,400 @@
+import type { ImageSource as ImageSourceDefinition } from '.';
+import { ImageAsset } from '../image-asset';
+import { path as fsPath, knownFolders } from '../file-system';
+import { isFileOrResourcePath } from '../utils';
+import { Trace } from '../trace';
+import { isMainThread, dispatchToMainThread } from '../utils/mainthread-helper';
+import { requestInternal as httpRequest } from '../http/http-request-internal';
+export { isFileOrResourcePath };
+
+// Cached constants for resource name handling
+const RES_PREFIX = 'res://';
+const LEADING_SLASHES_RE = /^[\\/]+/;
+const BACKSLASH_RE = /\\/g;
+
+function makeBitmapImage() {
+ return new Windows.UI.Xaml.Media.Imaging.BitmapImage();
+}
+
+function bitmapFromUriAsync(uriStr: string): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ const bmp = makeBitmapImage();
+ const onOpened = () => {
+ resolve(bmp);
+ };
+ const onFailed = (s: any, e: any) => {
+ reject(e || new Error('Image load failed'));
+ };
+ bmp.ImageOpened = onOpened;
+ bmp.ImageFailed = onFailed;
+ bmp.UriSource = new Windows.Foundation.Uri(uriStr);
+ } catch (err) { reject(err); }
+ });
+}
+
+function fileUri(filePath: string): string {
+ if (/^(ms-|http|file:)/i.test(filePath)) return filePath;
+ return 'file:///' + filePath.replace(/\\/g, '/');
+}
+
+function bytesToStream(bytes: Uint8Array): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ const writer = new Windows.Storage.Streams.DataWriter();
+ writer.WriteBytes(bytes as never);
+ const buffer = writer.DetachBuffer();
+ const stream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
+ NSWinRT.toPromise((stream as any).WriteAsync(buffer)).then(
+ () => { try { (stream as any).Seek(0); resolve(stream); } catch (e) { reject(e); } },
+ reject
+ );
+ } catch (e) { reject(e); }
+ });
+}
+
+function bitmapFromStream(stream: any): Promise {
+ return new Promise((resolve, reject) => {
+ // BitmapImage has UI thread affinity — create and load on the UI thread.
+ const create = () => {
+ try {
+ (stream as any).Seek(0);
+ const bmp = makeBitmapImage();
+ NSWinRT.toPromise(bmp.SetSourceAsync(stream)).then(
+ () => resolve(bmp),
+ reject
+ );
+ } catch (e) { reject(e); }
+ };
+ if (isMainThread()) {
+ create();
+ } else {
+ dispatchToMainThread(create);
+ }
+ });
+}
+
+function bitmapFromBytesAsync(bytes: Uint8Array): Promise {
+ return bytesToStream(bytes).then((stream) => {
+ return bitmapFromStream(stream);
+ });
+}
+
+function fetchBytesAsync(url: string): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ const httpClient = new Windows.Web.Http.HttpClient();
+ const uri = new Windows.Foundation.Uri(url);
+ NSWinRT.toPromise(httpClient.GetBufferAsync(uri)).then(
+ (buffer: any) => {
+ try {
+ const reader = Windows.Storage.Streams.DataReader.FromBuffer(buffer);
+ const bytes = new Uint8Array(buffer.Length);
+ reader.ReadBytes(bytes as never);
+ resolve(bytes);
+ } catch (e) { reject(e); }
+ },
+ (err: any) => { reject(err); }
+ );
+ } catch (e) { reject(e); }
+ });
+}
+
+function bytesToBase64(bytes: Uint8Array): string {
+ let binary = '';
+ const len = bytes.byteLength;
+ for (let i = 0; i < len; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return btoa(binary);
+}
+
+function readFileBytes(filePath: string): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ NSWinRT.toPromise((Windows.Storage.PathIO as any).ReadBufferAsync(filePath)).then(
+ (buffer: any) => {
+ try {
+ const reader = Windows.Storage.Streams.DataReader.FromBuffer(buffer);
+ const bytes = new Uint8Array(buffer.Length);
+ reader.ReadBytes(bytes as never);
+ resolve(bytes);
+ } catch (e) { reject(e); }
+ },
+ reject
+ );
+ } catch (e) { reject(e); }
+ });
+}
+
+function writeBytesToFile(filePath: string, bytes: Uint8Array): Promise {
+ return new Promise((resolve, reject) => {
+ NSWinRT.toPromise(
+ NativeScript.Widgets.ImageHelper.SaveToFileAsync(bytes as never, filePath)
+ ).then(
+ (result) => resolve(result),
+ (err) => reject(err)
+ );
+ });
+}
+
+function resizeBitmapAsync(bytes: Uint8Array, maxSize: number): Promise<{ bmp: any; bytes: Uint8Array; width: number; height: number }> {
+ return new Promise((resolve, reject) => {
+ NSWinRT.toPromise(
+ NativeScript.Widgets.ImageHelper.ResizeAsync(bytes as never, maxSize)
+ ).then(
+ (result: NativeScript.Widgets.ImageResult) => {
+ resolve({
+ bmp: result.Bitmap,
+ bytes: bytes,
+ width: result.Width,
+ height: result.Height,
+ });
+ })
+ .catch((err) => reject(err));
+
+ });
+}
+
+export class ImageSource implements ImageSourceDefinition {
+ public android: any;
+ public ios: any;
+ public windows: any; // BitmapImage or raw Uint8Array (lazy)
+
+ private _rawBytes: Uint8Array | null = null;
+ private _width: number = 0;
+ private _height: number = 0;
+ private _rotationAngle: number = 0;
+
+ public get height(): number { return this._height; }
+ public get width(): number { return this._width; }
+ public get rotationAngle(): number { return this._rotationAngle; }
+ public set rotationAngle(value: number) { this._rotationAngle = value; }
+
+ constructor(nativeSource?: any) {
+ if (nativeSource) {
+ this.setNativeSource(nativeSource);
+ }
+ }
+
+ static fromAsset(asset: ImageAsset): Promise {
+ return new Promise((resolve, reject) => {
+ asset.getImageAsync((fileOrNative: any, err: any) => {
+ if (err) { reject(err); return; }
+ if (!fileOrNative) { reject(new Error('No image from asset')); return; }
+ try {
+ if (typeof fileOrNative === 'string') {
+ ImageSource.fromFile(fileOrNative).then(resolve, reject);
+ return;
+ }
+ NSWinRT.toPromise(fileOrNative.OpenAsync(0 /* FileAccessMode.Read */)).then(
+ (stream: any) => bitmapFromStream(stream).then(
+ (bmp) => resolve(new ImageSource(bmp)),
+ reject
+ ),
+ reject
+ );
+ } catch (e) { reject(e); }
+ });
+ });
+ }
+
+ static fromUrl(url: string): Promise {
+ return httpRequest({ url, method: 'GET' }).then((response) => response.content.toNativeImage().then((value) => new ImageSource(value)));
+ }
+
+ static fromResourceSync(name: string): ImageSource {
+ if (!name) return null as any;
+ try {
+ const resourceName = name.startsWith(RES_PREFIX) ? name.slice(RES_PREFIX.length) : name;
+ const normalized = resourceName.replace(LEADING_SLASHES_RE, '').replace(BACKSLASH_RE, '/');
+ const exts = ['', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp'];
+ const resolve = (globalThis as any).__nsMsAppxResolve;
+ if (typeof resolve === 'function') {
+ for (const ext of exts) {
+ const uri = `ms-appx:///Assets/${normalized}${ext}`;
+ if (resolve(uri) != null) {
+ return ImageSource.fromFileSync(uri);
+ }
+ }
+ return null as any;
+ }
+ // Fallback when runtime hasn't registered the resolver yet
+ return ImageSource.fromFileSync(`ms-appx:///Assets/${normalized}`);
+ } catch {
+ return null as any;
+ }
+ }
+
+ static async fromResource(name: string): Promise {
+ if (!name) return null as any;
+ try {
+ const resourceName = name.startsWith(RES_PREFIX) ? name.slice(RES_PREFIX.length) : name;
+ const normalized = resourceName.replace(LEADING_SLASHES_RE, '').replace(BACKSLASH_RE, '/');
+ const exts = ['', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp'];
+
+ for (const ext of exts) {
+ try {
+ const bmp = await bitmapFromUriAsync(`ms-appx:///Assets/${normalized}${ext}`);
+ if (bmp) {
+ const src = new ImageSource(bmp);
+ src._width = bmp.PixelWidth ?? 0;
+ src._height = bmp.PixelHeight ?? 0;
+ return src;
+ }
+ } catch { /* try next extension */ }
+ }
+
+ return null as any;
+ } catch {
+ return null as any;
+ }
+ }
+
+ static fromFileSync(filePath: string): ImageSource {
+ try {
+ const bmp = makeBitmapImage();
+ const src = new ImageSource();
+ bmp.ImageOpened = () => {
+ src._width = bmp.PixelWidth ?? 0;
+ src._height = bmp.PixelHeight ?? 0;
+ //@ts-ignore
+ bmp.ImageOpened = null;
+ };
+ bmp.ImageFailed = () => {
+ //@ts-ignore
+ bmp.ImageOpened = null;
+ //@ts-ignore
+ bmp.ImageFailed = null;
+ }
+ bmp.UriSource = new Windows.Foundation.Uri(fileUri(filePath));
+ src.windows = bmp;
+ return src;
+ } catch {
+ return null as any;
+ }
+ }
+
+ static fromFile(filePath: string): Promise {
+ return readFileBytes(filePath).then((bytes) => {
+ return bitmapFromBytesAsync(bytes).then((bmp) => {
+ const src = new ImageSource(bmp);
+ src._rawBytes = bytes;
+ src._width = bmp.PixelWidth ?? 0;
+ src._height = bmp.PixelHeight ?? 0;
+ return src;
+ });
+ }).catch(() => {
+ return bitmapFromUriAsync(filePath).then((bmp) => {
+ const src = new ImageSource(bmp);
+ src._width = bmp.PixelWidth ?? 0;
+ src._height = bmp.PixelHeight ?? 0;
+ return src;
+ });
+ });
+ }
+
+ static fromDataSync(data: ArrayBuffer | Uint8Array): ImageSource {
+ const src = new ImageSource();
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data as ArrayBuffer);
+ src.windows = bytes;
+ src._rawBytes = bytes;
+ return src;
+ }
+
+ static fromData(data: ArrayBuffer | Uint8Array): Promise {
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data as ArrayBuffer);
+ return bitmapFromBytesAsync(bytes).then((bmp) => {
+ const src = new ImageSource(bmp);
+ src._rawBytes = bytes;
+ src._width = bmp.PixelWidth ?? 0;
+ src._height = bmp.PixelHeight ?? 0;
+ return src;
+ });
+ }
+
+ static fromBase64Sync(source: string): ImageSource {
+ try {
+ const bytes = Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
+ return ImageSource.fromDataSync(bytes);
+ } catch {
+ return null as any;
+ }
+ }
+
+ static fromBase64(source: string): Promise {
+ try {
+ const bytes = Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
+ return ImageSource.fromData(bytes);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ }
+
+ static fromSystemImageSync(name: string): ImageSource {
+ return ImageSource.fromResourceSync(name);
+ }
+
+ static fromSystemImage(name: string): Promise {
+ return ImageSource.fromResource(name);
+ }
+
+ static fromFontIconCodeSync(_source: string, _font: any, _color: any): ImageSource {
+ return null as any;
+ }
+
+ public getNativeSource(): any {
+ return this.windows ?? this._rawBytes;
+ }
+
+ public setNativeSource(source: any): void {
+ this.windows = source;
+ try {
+ if (source && typeof source.PixelWidth === 'number') {
+ this._width = source.PixelWidth;
+ this._height = source.PixelHeight;
+ }
+ } catch { }
+ }
+
+ public saveToFile(_path: string, _format: string, _quality?: number): boolean {
+ return false;
+ }
+
+ public saveToFileAsync(path: string, _format: string, _quality?: number): Promise {
+ if (!this._rawBytes) {
+ return Promise.resolve(false);
+ }
+ return writeBytesToFile(path, this._rawBytes).catch(() => false);
+ }
+
+ public toBase64String(_format: string, _quality?: number): string {
+ if (!this._rawBytes) return '';
+ try {
+ return bytesToBase64(this._rawBytes);
+ } catch {
+ return '';
+ }
+ }
+
+ public toBase64StringAsync(format: string, quality?: number): Promise {
+ return Promise.resolve(this.toBase64String(format, quality));
+ }
+
+ public resize(_maxSize: number, _options?: any): ImageSource {
+ return this;
+ }
+
+ public resizeAsync(maxSize: number, _options?: any): Promise {
+ if (!this._rawBytes) {
+ return Promise.resolve(this);
+ }
+ return resizeBitmapAsync(this._rawBytes, maxSize).then(({ bmp, bytes, width, height }) => {
+ const src = new ImageSource(bmp);
+ src._rawBytes = bytes;
+ src._width = width;
+ src._height = height;
+ return src;
+ }).catch(() => this);
+ }
+}
diff --git a/packages/core/package.json b/packages/core/package.json
index 937baae32a..673afd699f 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -73,7 +73,8 @@
"nativescript": {
"platforms": {
"ios": "6.0.0",
- "android": "6.0.0"
+ "android": "6.0.0",
+ "windows": "9.1.0"
},
"hooks": [
{
diff --git a/packages/core/platform/device/index.windows.ts b/packages/core/platform/device/index.windows.ts
new file mode 100644
index 0000000000..1b42bd9fea
--- /dev/null
+++ b/packages/core/platform/device/index.windows.ts
@@ -0,0 +1,103 @@
+import { Screen } from '../screen';
+
+const MIN_TABLET_PIXELS = 600;
+
+class DeviceRef {
+ private _manufacturer: string;
+ private _model: string;
+ private _osVersion: string;
+ private _uuid: string;
+
+ get manufacturer(): string {
+ if (!this._manufacturer) {
+ try {
+ this._manufacturer = Windows.System.Profile.SystemManufacturerInfo.SmbiosInfo.SystemManufacturer || 'Microsoft';
+ } catch {
+ this._manufacturer = 'Microsoft';
+ }
+ }
+ return this._manufacturer;
+ }
+
+ get os(): string {
+ return 'Windows';
+ }
+
+ get osVersion(): string {
+ if (!this._osVersion) {
+ try {
+ const versionStr = Windows.System.Profile.AnalyticsInfo.VersionInfo.DeviceFamilyVersion;
+ const version = parseInt(versionStr, 10);
+ if (!isNaN(version)) {
+ const major = (version / 0x1000000000000) & 0xffff;
+ const minor = (version / 0x100000000) & 0xffff;
+ const build = (version / 0x10000) & 0xffff;
+ this._osVersion = `${major}.${minor}.${build}`;
+ } else {
+ this._osVersion = versionStr || 'unknown';
+ }
+ } catch {
+ this._osVersion = 'unknown';
+ }
+ }
+ return this._osVersion;
+ }
+
+ get model(): string {
+ if (!this._model) {
+ try {
+ this._model = Windows.System.Profile.SystemManufacturerInfo.SmbiosInfo.SystemProductName || 'Windows PC';
+ } catch {
+ this._model = 'Windows PC';
+ }
+ }
+ return this._model;
+ }
+
+ get sdkVersion(): string {
+ return this.osVersion;
+ }
+
+ get deviceType(): 'Phone' | 'Tablet' {
+ const dips = Math.min(Screen.mainScreen.widthPixels, Screen.mainScreen.heightPixels) / Screen.mainScreen.scale;
+ return dips >= MIN_TABLET_PIXELS ? 'Tablet' : 'Phone';
+ }
+
+ get uuid(): string {
+ if (!this._uuid) {
+ try {
+ const systemId = Windows.System.Profile.SystemIdentification.GetSystemIdForPublisher();
+ const buffer = systemId.Id;
+ const reader = Windows.Storage.Streams.DataReader.FromBuffer(buffer);
+ const bytes = new Uint8Array(buffer.Length);
+ reader.ReadBytes(bytes);
+ this._uuid = Array.from(bytes)
+ .map((b) => b.toString(16).padStart(2, '0'))
+ .join('');
+ } catch {
+ this._uuid = 'windows-' + Math.random().toString(36).substr(2, 9);
+ }
+ }
+ return this._uuid;
+ }
+
+ get language(): string {
+ try {
+ return Windows.Globalization.Language.CurrentInputMethodLanguageTag || 'en';
+ } catch {
+ return 'en';
+ }
+ }
+
+ get region(): string {
+ try {
+ return Windows.System.UserProfile.GlobalizationPreferences.HomeGeographicRegion || 'US';
+ } catch {
+ return 'US';
+ }
+ }
+}
+
+export const Device = new DeviceRef();
+
+export const device = Device;
diff --git a/packages/core/platform/screen/index.windows.ts b/packages/core/platform/screen/index.windows.ts
new file mode 100644
index 0000000000..f637d27794
--- /dev/null
+++ b/packages/core/platform/screen/index.windows.ts
@@ -0,0 +1,41 @@
+class MainScreen {
+ get widthPixels(): number {
+ try {
+ return Windows.Graphics.Display.DisplayInformation.GetForCurrentView().ScreenWidthInRawPixels;
+ } catch {
+ return 1920;
+ }
+ }
+
+ get heightPixels(): number {
+ try {
+ return Windows.Graphics.Display.DisplayInformation.GetForCurrentView().ScreenHeightInRawPixels;
+ } catch {
+ return 1080;
+ }
+ }
+
+ get scale(): number {
+ try {
+ return Windows.Graphics.Display.DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel || 1;
+ } catch {
+ return 1;
+ }
+ }
+
+ get widthDIPs(): number {
+ return this.widthPixels / this.scale;
+ }
+
+ get heightDIPs(): number {
+ return this.heightPixels / this.scale;
+ }
+
+ public _updateMetrics(): void {}
+}
+
+export class Screen {
+ static mainScreen = new MainScreen();
+}
+
+export const screen = Screen;
diff --git a/packages/core/platforms/windows/NativeScript.Widgets.dll b/packages/core/platforms/windows/NativeScript.Widgets.dll
new file mode 100644
index 0000000000..a43b2f1773
Binary files /dev/null and b/packages/core/platforms/windows/NativeScript.Widgets.dll differ
diff --git a/packages/core/references.d.ts b/packages/core/references.d.ts
index 4300b64aee..9306e7dddf 100644
--- a/packages/core/references.d.ts
+++ b/packages/core/references.d.ts
@@ -8,3 +8,6 @@
///
///
///
+///
+///
+///
diff --git a/packages/core/text/index.windows.ts b/packages/core/text/index.windows.ts
new file mode 100644
index 0000000000..e1377eb270
--- /dev/null
+++ b/packages/core/text/index.windows.ts
@@ -0,0 +1,13 @@
+export * from './text-common';
+
+export namespace encoding {
+ export const UTF_16LE = 1200; // UTF-16LE
+ export const UTF_16BE = 1201; // UTF-16BE
+ export const UTF_8 = 65001; // CP_UTF8
+
+ export const US_ASCII = 20127; // US-ASCII
+ export const ISO_8859_1 = 28591; // ISO-8859-1
+
+ // UTF-16 on Windows is effectively UTF-16LE.
+ export const UTF_16 = UTF_16LE;
+}
\ No newline at end of file
diff --git a/packages/core/timer/index.windows.ts b/packages/core/timer/index.windows.ts
new file mode 100644
index 0000000000..f54a9c3c19
--- /dev/null
+++ b/packages/core/timer/index.windows.ts
@@ -0,0 +1,25 @@
+// __ns__setTimeout / __ns__setInterval are Rust-backed timers registered by
+// the windows-runtime. They use a background scheduler thread and post
+// callbacks back to the V8 thread via a per-thread channel that is drained
+// on every pump() tick — no XAML dispatcher required.
+declare function __ns__setTimeout(callback: Function, ms: number): number;
+declare function __ns__setInterval(callback: Function, ms: number): number;
+declare function __ns__clearTimeout(id: number): void;
+declare function __ns__clearInterval(id: number): void;
+
+export function setTimeout(callback: Function, milliseconds = 0, ...args: any[]): number {
+ milliseconds += 0;
+ const invoke = args.length ? () => callback(...args) : callback;
+ return __ns__setTimeout(zonedCallback(invoke), milliseconds);
+}
+
+export function clearTimeout(id: number): void {
+ __ns__clearTimeout(id);
+}
+
+export function setInterval(callback: Function, milliseconds = 0, ...args: []): number {
+ const invoke = args.length ? () => callback(...args) : callback;
+ return __ns__setInterval(zonedCallback(invoke), milliseconds);
+}
+
+export const clearInterval = clearTimeout;
diff --git a/packages/core/ui/action-bar/index.windows.ts b/packages/core/ui/action-bar/index.windows.ts
new file mode 100644
index 0000000000..f342fa45e0
--- /dev/null
+++ b/packages/core/ui/action-bar/index.windows.ts
@@ -0,0 +1,25 @@
+export * from './action-bar-common';
+
+import { ActionBarBase, ActionItemBase } from './action-bar-common';
+
+export class ActionItem extends ActionItemBase {}
+
+export class NavigationButton extends ActionItem {}
+
+export class ActionBar extends ActionBarBase {
+ get windows(): undefined {
+ return undefined;
+ }
+
+ public update(): void {
+ const page = this.parent as any;
+ if (!page) return;
+ const frame = page.frame as any;
+ if (!frame?._updateActionBar) return;
+ frame._updateActionBar(page);
+ }
+
+ public _onTitlePropertyChanged(): void {
+ this.update();
+ }
+}
diff --git a/packages/core/ui/activity-indicator/index.windows.ts b/packages/core/ui/activity-indicator/index.windows.ts
new file mode 100644
index 0000000000..4a7b6a604f
--- /dev/null
+++ b/packages/core/ui/activity-indicator/index.windows.ts
@@ -0,0 +1,68 @@
+import { colorProperty, visibilityProperty } from '../styling/style-properties';
+import { CoreTypes } from '../../core-types';
+import { ActivityIndicatorBase, busyProperty } from './activity-indicator-common';
+import { Color } from '../../color';
+export * from './activity-indicator-common';
+
+export class ActivityIndicator extends ActivityIndicatorBase {
+ nativeViewProtected: Windows.UI.Xaml.Controls.ProgressBar;
+
+ public createNativeView(): Object {
+ const indicator = new Windows.UI.Xaml.Controls.ProgressBar();
+ indicator.IsIndeterminate = true;
+ indicator.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
+ return indicator;
+ }
+
+
+ [busyProperty.getDefault](): boolean {
+ return false;
+ }
+ [busyProperty.setNative](value: boolean) {
+ if (this.visibility === CoreTypes.Visibility.visible) {
+ this.nativeViewProtected.Visibility = value ? Windows.UI.Xaml.Visibility.Visible : Windows.UI.Xaml.Visibility.Collapsed;
+ }
+ }
+
+
+ [visibilityProperty.getDefault](): CoreTypes.VisibilityType {
+ return CoreTypes.Visibility.hidden;
+ }
+
+ [visibilityProperty.setNative](value: CoreTypes.VisibilityType) {
+ switch (value) {
+ case CoreTypes.Visibility.visible:
+ this.nativeViewProtected.Visibility = this.busy ? Windows.UI.Xaml.Visibility.Visible : Windows.UI.Xaml.Visibility.Collapsed;
+ break;
+ case CoreTypes.Visibility.hidden:
+ this.nativeViewProtected.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
+ break;
+ case CoreTypes.Visibility.collapse:
+ this.nativeViewProtected.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
+ break;
+ default:
+ throw new Error(`Invalid visibility value: ${value}. Valid values are: "${CoreTypes.Visibility.visible}", "${CoreTypes.Visibility.hidden}", "${CoreTypes.Visibility.collapse}".`);
+ }
+ }
+
+
+ [colorProperty.getDefault](): number {
+ return -1;
+ }
+ [colorProperty.setNative](value: number | Color) {
+ const color = value instanceof Color ? value.windows : value
+ if (color) {
+ if (typeof color === 'number') {
+ this.nativeViewProtected.Foreground = new Windows.UI.Xaml.Media.SolidColorBrush(
+ new Color(color).windows
+ );
+ } else {
+ this.nativeViewProtected.Foreground = new Windows.UI.Xaml.Media.SolidColorBrush(
+ color
+ );
+ }
+ } else {
+ this.nativeViewProtected.Foreground = new Windows.UI.Xaml.Media.SolidColorBrush();
+ }
+ }
+}
diff --git a/packages/core/ui/animation/index.windows.ts b/packages/core/ui/animation/index.windows.ts
new file mode 100644
index 0000000000..16c59b5f51
--- /dev/null
+++ b/packages/core/ui/animation/index.windows.ts
@@ -0,0 +1,388 @@
+export * from './animation-common';
+export * from './animation-shared';
+export * from './animation-types';
+
+export function _resolveAnimationCurve(curve: any): any {
+ return curve;
+}
+
+import { AnimationBase, Properties } from './animation-common';
+import type { View } from '../core/view';
+import { _ensureNativeTransforms } from '../core/view';
+
+function _applyFinalValue(target: any, property: string, to: any): void {
+ try {
+ switch (property) {
+ case Properties.opacity: target.opacity = to; break;
+ case Properties.translate: target.translateX = to?.x ?? 0; target.translateY = to?.y ?? 0; break;
+ case Properties.scale: target.scaleX = to?.x ?? 1; target.scaleY = to?.y ?? 1; break;
+ case Properties.rotate: target.rotate = to?.z ?? to; break;
+ case Properties.width: target.width = typeof to === 'object' ? (to?.value ?? to) : to; break;
+ case Properties.height: target.height = typeof to === 'object' ? (to?.value ?? to) : to; break;
+ default: break;
+ }
+ } catch (_e) {}
+}
+
+// Per-native-view, per-property storyboard conflict tracking.
+// When a new animation starts on the same native element + property, the previous
+// storyboard is stopped immediately and its promise resolved so the chain continues.
+const _activeStoryboards = new WeakMap