diff --git a/lib/logic/applications.dart b/lib/logic/applications.dart new file mode 100644 index 00000000..8ef9885e --- /dev/null +++ b/lib/logic/applications.dart @@ -0,0 +1,63 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +class ApplicationsManager { + static const channel = MethodChannel('com.expidusos.genesis.shell/applications'); + + ApplicationsManager() { + channel.setMethodCallHandler((call) async { + switch (call.method) { + case 'sync': + _sync(); + break; + default: + throw MissingPluginException(); + } + }); + + _sync(); + } + + List _applications = []; + UnmodifiableListView get applications => UnmodifiableListView(_applications); + + void _sync() { + channel.invokeListMethod('list').then((list) { + _applications.clear(); + _applications.addAll(list!.map( + (app) => + Application( + id: app['id'], + name: app['name'], + displayName: app['displayName'], + description: app['description'], + isHidden: app['isHidden'], + icon: app['icon'], + ) + )); + }).catchError((err) { + print(err); + }); + } +} + +class Application { + const Application({ + this.id, + this.name, + this.displayName, + this.description, + this.isHidden = false, + this.icon, + }); + + final String? id; + final String? name; + final String? displayName; + final String? description; + final bool isHidden; + final String? icon; +} diff --git a/lib/logic/wm.dart b/lib/logic/wm.dart index a391f3cb..8804352d 100644 --- a/lib/logic/wm.dart +++ b/lib/logic/wm.dart @@ -13,7 +13,7 @@ class WindowManager extends ChangeNotifier { this.mode = WindowManagerMode.stacking, }); - final WindowManagerMode mode; + WindowManagerMode mode; List _wins = []; void dispose() {} diff --git a/lib/main.dart b/lib/main.dart index d05f21d9..dec0391d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:libtokyo/libtokyo.dart' hide TokyoApp; import 'package:provider/provider.dart'; import 'logic/account.dart'; +import 'logic/applications.dart'; import 'logic/display.dart'; import 'logic/outputs.dart'; import 'logic/power.dart'; @@ -64,6 +65,7 @@ class GenesisShellApp extends StatefulWidget { class _GenesisShellAppState extends State { late AccountManager _accountManager; + late ApplicationsManager _applicationsManager; late DisplayManager _displayManager; late OutputManager _outputManager; late PowerManager _powerManager; @@ -73,6 +75,7 @@ class _GenesisShellAppState extends State { super.initState(); _accountManager = AccountManager(); + _applicationsManager = ApplicationsManager(); _displayManager = DisplayManager(); _outputManager = OutputManager(); _powerManager = PowerManager.auto(); @@ -90,6 +93,7 @@ class _GenesisShellAppState extends State { MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => _accountManager), + Provider(create: (_) => _applicationsManager), ChangeNotifierProvider(create: (_) => _displayManager), ChangeNotifierProvider(create: (_) => _outputManager), Provider(create: (_) => _powerManager), diff --git a/lib/views/desktop.dart b/lib/views/desktop.dart index 57359e4f..19ccc05e 100644 --- a/lib/views/desktop.dart +++ b/lib/views/desktop.dart @@ -41,6 +41,7 @@ class _DesktopViewState extends State { String? sessionName = null; DisplayServer? _displayServer = null; GlobalKey _key = GlobalKey(); + GlobalKey _wmKey = GlobalKey(); void _syncOutputs() { final outputs = Provider.of(_key.currentContext!, listen: false); @@ -117,6 +118,7 @@ class _DesktopViewState extends State { constraints: BoxConstraints.expand(), child: _displayServer != null ? WindowManagerView( + key: _wmKey, displayServer: _displayServer!, mode: Breakpoints.small.isActive(context) ? WindowManagerMode.stacking : WindowManagerMode.floating, ) : null, @@ -128,7 +130,10 @@ class _DesktopViewState extends State { if (_displayServer != null) { value = ChangeNotifierProvider.value( value: _displayServer!, - child: value, + child: ChangeNotifierProvider( + create: (context) => _wmKey!.currentState!.instance, + child: value, + ), ); } return value; diff --git a/lib/widgets/system_layout.dart b/lib/widgets/system_layout.dart index d8ba331c..3a0af637 100644 --- a/lib/widgets/system_layout.dart +++ b/lib/widgets/system_layout.dart @@ -1,4 +1,5 @@ import 'package:backdrop/backdrop.dart'; +import 'package:flutter/material.dart' as material; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:libtokyo_flutter/libtokyo.dart' hide ColorScheme; import 'package:libtokyo/libtokyo.dart' hide TokyoApp, Scaffold; @@ -100,6 +101,7 @@ class SystemLayout extends StatelessWidget { ? Padding( padding: const EdgeInsets.all(8.0), child: Drawer( + width: double.infinity, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0), ), @@ -111,7 +113,14 @@ class SystemLayout extends StatelessWidget { backgroundColor: Theme.of(context).colorScheme.surface, ), ), - child: UserDrawer(), + child: Builder( + builder: (context) => + UserDrawer( + onClose: () { + material.Scaffold.of(context).closeDrawer(); + }, + ), + ), ), ), ), diff --git a/lib/widgets/system_navbar.dart b/lib/widgets/system_navbar.dart index d81b14ba..6527858e 100644 --- a/lib/widgets/system_navbar.dart +++ b/lib/widgets/system_navbar.dart @@ -1,9 +1,19 @@ +import 'package:flutter/material.dart' as material; import 'package:libtokyo_flutter/libtokyo.dart' hide ColorScheme; import 'package:libtokyo/libtokyo.dart' hide TokyoApp, Scaffold; -class SystemNavbar extends StatelessWidget { +import 'user_drawer.dart'; + +class SystemNavbar extends StatefulWidget { const SystemNavbar({ super.key }); + @override + State createState() => _SystemNavbarState(); +} + +class _SystemNavbarState extends State { + PersistentBottomSheetController? _controller; + @override Widget build(BuildContext context) => BottomAppBar( @@ -17,7 +27,19 @@ class SystemNavbar extends StatelessWidget { icon: Icon(Icons.chevronLeft), ), IconButton( - onPressed: () {}, + onPressed: () { + if (_controller != null) { + _controller!.close(); + _controller = null; + } else { + _controller = material.Scaffold.of(context).showBottomSheet((context) => + UserDrawer( + onClose: () { + _controller!.close(); + }, + )); + } + }, icon: Icon(Icons.circleDot), ), IconButton( diff --git a/lib/widgets/toplevel.dart b/lib/widgets/toplevel.dart index 16d58053..3cfe4609 100644 --- a/lib/widgets/toplevel.dart +++ b/lib/widgets/toplevel.dart @@ -20,8 +20,25 @@ class ToplevelDecor extends StatelessWidget { final VoidCallback? onClose; @override - Widget build(BuildContext context) => - AppBar( + Widget build(BuildContext context) { + final actions = [ + onMinimize != null + ? IconButton( + onPressed: onMinimize!, + icon: Icon(Icons.windowMinimize), + ) : null, + onMaximize != null + ? IconButton( + onPressed: onMaximize!, + icon: Icon(Icons.windowMaximize), + ) : null, + onClose != null + ? IconButton( + onPressed: onClose!, + icon: Icon(Icons.circleXmark), + ) : null, + ].where((e) => e != null).toList().cast(); + return AppBar( automaticallyImplyLeading: false, primary: false, title: Text(toplevel.title ?? 'Untitled Window'), @@ -32,24 +49,12 @@ class ToplevelDecor extends StatelessWidget { topRight: Radius.circular(12), ), ), - actions: [ - onMinimize != null - ? IconButton( - onPressed: onMinimize!, - icon: Icon(Icons.windowMinimize), - ) : null, - onMaximize != null - ? IconButton( - onPressed: onMaximize!, - icon: Icon(Icons.windowMaximize), - ) : null, - onClose != null - ? IconButton( - onPressed: onClose!, - icon: Icon(Icons.circleXmark), - ) : null, - ].where((e) => e != null).toList().cast(), + actions: actions.isEmpty + ? [ + const SizedBox() + ] : actions, ); + } } class ToplevelView extends StatefulWidget { @@ -91,13 +96,10 @@ class _ToplevelViewState extends State { @override Widget _buildContent(BuildContext context, DisplayServerToplevel toplevel) { - Widget content = ConstrainedBox( - constraints: toplevel.buildBoxConstraints(), - child: toplevel.texture == null - ? SizedBox() : Texture( - textureId: toplevel.texture!, - ), - ); + Widget content = toplevel.texture == null + ? SizedBox() : Texture( + textureId: toplevel.texture!, + ); if (widget.isFocusable) { content = Focus( @@ -116,7 +118,10 @@ class _ToplevelViewState extends State { return true; }, child: SizeChangedLayoutNotifier( - child: content, + child: ConstrainedBox( + constraints: toplevel.buildBoxConstraints(), + child: content, + ), ), ); } diff --git a/lib/widgets/user_drawer.dart b/lib/widgets/user_drawer.dart index 72f3606f..2d0bf2c1 100644 --- a/lib/widgets/user_drawer.dart +++ b/lib/widgets/user_drawer.dart @@ -1,19 +1,113 @@ +import 'dart:io'; + import 'package:libtokyo_flutter/libtokyo.dart' hide ColorScheme; import 'package:libtokyo/libtokyo.dart' hide TokyoApp, Scaffold; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:path/path.dart' as path; import 'package:provider/provider.dart'; +import '../logic/applications.dart'; import '../logic/display.dart'; +import '../logic/wm.dart'; + +import 'toplevel.dart'; class UserDrawer extends StatelessWidget { const UserDrawer({ super.key, + required this.onClose, }); + final VoidCallback onClose; + @override - Widget build(BuildContext context) { - print(context.watch()); - return ListView( - children: [], + Widget build(BuildContext context) => + ListView( + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: context.watch().toplevels.map( + (toplevel) => + Padding( + padding: const EdgeInsets.all(8), + child: InkWell( + onTap: () { + final wm = Provider.of(context, listen: false); + final win = wm.fromToplevel(toplevel); + + win.toplevel.setActive(true); + win.minimized = false; + win.layer++; + + onClose(); + }, + child: FittedBox( + fit: BoxFit.scaleDown, + child: ToplevelView( + toplevel: toplevel, + isFocusable: false, + isSizable: false, + buildDecor: (context, toplevel, content) => + !Breakpoints.small.isActive(context) + ? Container( + width: toplevel.size != null ? (toplevel.size!.width ?? 0).toDouble() : null, + child: Column( + children: [ + ToplevelDecor( + toplevel: toplevel, + ), + ClipRRect( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + child: content, + ), + ], + ), + ) : null, + ), + ), + ), + ) + ).toList(), + ), + ), + GridView.count( + crossAxisCount: 5, + shrinkWrap: true, + children: (Provider.of(context).applications.toList()..where((app) => !app.isHidden)) + .map( + (app) => + InkWell( + // TODO: launch app + child: Column( + children: [ + app.icon != null + ? (path.extension(app.icon!) == '.svg' + ? SvgPicture.file( + File(app.icon!), + width: 40, + height: 40, + ) : Image.file( + File(app.icon!), + width: 40, + height: 40, + )) : Icon(Icons.tablet, size: 80), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + app.displayName ?? app.name ?? '', + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ) + ).toList(), + ), + ], ); - } } diff --git a/lib/widgets/wm.dart b/lib/widgets/wm.dart index 6db2c64f..2288fbed 100644 --- a/lib/widgets/wm.dart +++ b/lib/widgets/wm.dart @@ -84,22 +84,25 @@ class WindowManagerView extends StatefulWidget { final WindowManagerMode mode; @override - State createState() => _WindowManagerViewState(); + State createState() => WindowManagerViewState(); + + static WindowManagerViewState? maybeOf(BuildContext context) => + context.findAncestorStateOfType(); + + static WindowManagerViewState of(BuildContext context) => maybeOf(context)!; } -class _WindowManagerViewState extends State { - late WindowManager _instance; +class WindowManagerViewState extends State { + WindowManager _instance = WindowManager(); late StreamSubscription _toplevelAdded; late StreamSubscription _toplevelRemoved; + WindowManager get instance => _instance; + @override void initState() { super.initState(); - _instance = WindowManager( - mode: widget.mode, - ); - _toplevelAdded = widget.displayServer.toplevelAdded.listen((toplevel) { _instance.fromToplevel(toplevel); }); diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 1f730b61..83436e77 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -117,6 +117,7 @@ add_executable(${BINARY_NAME} "application.cc" "messaging.c" "channels/account.cc" + "channels/applications.cc" "channels/auth.cc" "channels/display/backend/dummy.c" "channels/display/backend/wayland.c" diff --git a/linux/application-priv.h b/linux/application-priv.h index 235e73cc..cd31e60a 100644 --- a/linux/application-priv.h +++ b/linux/application-priv.h @@ -1,6 +1,7 @@ #pragma once #include "channels/account.h" +#include "channels/applications.h" #include "channels/auth.h" #include "channels/display.h" #include "channels/outputs.h" @@ -14,6 +15,7 @@ struct _GenesisShellApplication { FlView* view; AccountChannel account; + ApplicationsChannel applications; AuthChannel auth; DisplayChannel display; OutputsChannel outputs; diff --git a/linux/application.cc b/linux/application.cc index 2d7ea9ad..34af3a1e 100644 --- a/linux/application.cc +++ b/linux/application.cc @@ -32,6 +32,7 @@ static void genesis_shell_application_activate(GApplication* application) { fl_register_plugins(FL_PLUGIN_REGISTRY(self->view)); account_channel_init(&self->account, self->view); + applications_channel_init(&self->applications, self->view); auth_channel_init(&self->auth, self->view); display_channel_init(&self->display, self->view); outputs_channel_init(&self->outputs, self->view); @@ -83,6 +84,7 @@ static void genesis_shell_application_dispose(GObject* object) { g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); account_channel_deinit(&self->account); + applications_channel_deinit(&self->applications); auth_channel_deinit(&self->auth); display_channel_deinit(&self->display); outputs_channel_deinit(&self->outputs); diff --git a/linux/channels/applications.cc b/linux/channels/applications.cc new file mode 100644 index 00000000..b274d627 --- /dev/null +++ b/linux/channels/applications.cc @@ -0,0 +1,84 @@ +#include "applications.h" + +static FlValue* new_string(const gchar* str) { + if (str == nullptr || g_utf8_strlen(str, -1) == 0) return fl_value_new_null(); + return fl_value_new_string(str); +} + +static FlValue* from_app_info(GAppInfo* appinfo) { + FlValue* value = fl_value_new_map(); + + fl_value_set(value, fl_value_new_string("id"), new_string(g_app_info_get_id(appinfo))); + fl_value_set(value, fl_value_new_string("name"), new_string(g_app_info_get_name(appinfo))); + fl_value_set(value, fl_value_new_string("displayName"), new_string(g_app_info_get_display_name(appinfo))); + fl_value_set(value, fl_value_new_string("description"), new_string(g_app_info_get_description(appinfo))); + fl_value_set(value, fl_value_new_string("isHidden"), fl_value_new_bool(!g_app_info_should_show(appinfo))); + + GIcon* icon = g_app_info_get_icon(appinfo); + if (icon == nullptr) { + fl_value_set(value, fl_value_new_string("icon"), fl_value_new_null()); + } else { + if (G_IS_FILE_ICON(icon)) { + GFile* file = g_file_icon_get_file(G_FILE_ICON(icon)); + fl_value_set(value, fl_value_new_string("icon"), new_string(g_file_get_path(file))); + } else if (G_IS_THEMED_ICON(icon)) { + GtkIconTheme* theme = gtk_icon_theme_get_default(); + GtkIconInfo* icon_info = gtk_icon_theme_lookup_by_gicon(theme, icon, 48, (GtkIconLookupFlags)0); + if (icon_info != nullptr) { + fl_value_set(value, fl_value_new_string("icon"), new_string(gtk_icon_info_get_filename(icon_info))); + } else { + fl_value_set(value, fl_value_new_string("icon"), fl_value_new_null()); + } + } else { + fl_value_set(value, fl_value_new_string("icon"), fl_value_new_null()); + } + } + return value; +} + +static void method_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data) { + ApplicationsChannel* self = (ApplicationsChannel*)user_data; + (void)self; + + g_autoptr(FlMethodResponse) response = nullptr; + + if (strcmp(fl_method_call_get_name(method_call), "list") == 0) { + g_autoptr(FlValue) value = fl_value_new_list(); + + GList* list = g_app_info_get_all(); + for (GList* entry = list; entry != NULL; entry = entry->next) { + fl_value_append(value, from_app_info(G_APP_INFO(entry->data))); + } + + g_clear_list(&list, g_object_unref); + + response = FL_METHOD_RESPONSE(fl_method_success_response_new(value)); + } else { + response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + } + + g_autoptr(GError) error = nullptr; + if (!fl_method_call_respond(method_call, response, &error)) { + g_warning("Failed to send response: %s", error->message); + } +} + +static void changed(GAppInfoMonitor* monitor, ApplicationsChannel* self) { + (void)monitor; + + fl_method_channel_invoke_method(self->channel, "sync", nullptr, nullptr, nullptr, nullptr); +} + +void applications_channel_init(ApplicationsChannel* self, FlView* view) { + self->monitor = G_APP_INFO_MONITOR(g_object_ref(G_OBJECT(g_app_info_monitor_get()))); + self->changed = g_signal_connect(self->monitor, "changed", G_CALLBACK(changed), self); + + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + self->channel = fl_method_channel_new(fl_engine_get_binary_messenger(fl_view_get_engine(view)), "com.expidusos.genesis.shell/applications", FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler(self->channel, method_call_handler, self, nullptr); +} + +void applications_channel_deinit(ApplicationsChannel* self) { + g_clear_object(&self->channel); + g_clear_object(&self->monitor); +} diff --git a/linux/channels/applications.h b/linux/channels/applications.h new file mode 100644 index 00000000..2ad32523 --- /dev/null +++ b/linux/channels/applications.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "../application.h" + +typedef struct _ApplicationsChannel { + guint changed; + + GAppInfoMonitor* monitor; + FlMethodChannel* channel; +} ApplicationsChannel; + +void applications_channel_init(ApplicationsChannel* self, FlView* view); +void applications_channel_deinit(ApplicationsChannel* self); diff --git a/pubspec.lock b/pubspec.lock index 26e35ab8..3305dfa2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -171,6 +171,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + url: "https://pub.dev" + source: hosted + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -312,13 +320,21 @@ packages: source: hosted version: "1.0.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_provider: dependency: transitive description: @@ -620,6 +636,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" vector_math: dependency: transitive description: diff --git a/pubspec.lock.json b/pubspec.lock.json index d3a1c4ad..ad6a74a7 100644 --- a/pubspec.lock.json +++ b/pubspec.lock.json @@ -212,6 +212,16 @@ "source": "sdk", "version": "0.0.0" }, + "flutter_svg": { + "dependency": "direct main", + "description": { + "name": "flutter_svg", + "sha256": "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2", + "url": "https://pub.dev" + }, + "source": "hosted", + "version": "2.0.10+1" + }, "flutter_test": { "dependency": "direct dev", "description": "flutter", @@ -387,7 +397,7 @@ "version": "1.0.0" }, "path": { - "dependency": "transitive", + "dependency": "direct main", "description": { "name": "path", "sha256": "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af", @@ -396,6 +406,16 @@ "source": "hosted", "version": "1.9.0" }, + "path_parsing": { + "dependency": "transitive", + "description": { + "name": "path_parsing", + "sha256": "e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf", + "url": "https://pub.dev" + }, + "source": "hosted", + "version": "1.0.1" + }, "path_provider": { "dependency": "transitive", "description": { @@ -772,6 +792,36 @@ "source": "hosted", "version": "3.1.1" }, + "vector_graphics": { + "dependency": "transitive", + "description": { + "name": "vector_graphics", + "sha256": "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3", + "url": "https://pub.dev" + }, + "source": "hosted", + "version": "1.1.11+1" + }, + "vector_graphics_codec": { + "dependency": "transitive", + "description": { + "name": "vector_graphics_codec", + "sha256": "c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da", + "url": "https://pub.dev" + }, + "source": "hosted", + "version": "1.1.11+1" + }, + "vector_graphics_compiler": { + "dependency": "transitive", + "description": { + "name": "vector_graphics_compiler", + "sha256": "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81", + "url": "https://pub.dev" + }, + "source": "hosted", + "version": "1.1.11+1" + }, "vector_math": { "dependency": "transitive", "description": { diff --git a/pubspec.yaml b/pubspec.yaml index ff70d03e..83f58f24 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,8 @@ dependencies: args: ^2.5.0 provider: ^6.1.2 dbus: ^0.7.10 + flutter_svg: ^2.0.10+1 + path: ^1.9.0 dependency_overrides: meta: ^1.14.0