diff --git a/bin/breezy_gnome_setup b/bin/breezy_gnome_setup index c5d9ce6..798b3a7 100755 --- a/bin/breezy_gnome_setup +++ b/bin/breezy_gnome_setup @@ -43,8 +43,16 @@ else cp $abs_path $tmp_dir fi +# if abs_path is present, grab the filename from it +if [ -n "$abs_path" ] +then + filename=$(basename $abs_path) +else + filename="breezyGNOME.tar.gz" +fi + echo "Extracting to: ${tmp_dir}/breezy_gnome" -tar -xf breezyGNOME.tar.gz +tar -xf $filename pushd breezy_gnome > /dev/null diff --git a/modules/xrealAirLinuxDriver b/modules/xrealAirLinuxDriver index f379c97..8e5513c 160000 --- a/modules/xrealAirLinuxDriver +++ b/modules/xrealAirLinuxDriver @@ -1 +1 @@ -Subproject commit f379c976e003823ed6a61fe2d054c802b3bca80d +Subproject commit 8e5513c89df30ca2cfdcd03947dcb8d9562db6b3 diff --git a/ui/modules/PyXRLinuxDriverIPC b/ui/modules/PyXRLinuxDriverIPC index b2cdc8c..831a31d 160000 --- a/ui/modules/PyXRLinuxDriverIPC +++ b/ui/modules/PyXRLinuxDriverIPC @@ -1 +1 @@ -Subproject commit b2cdc8cc5fc593ab04015cec08a8e97b103ffa63 +Subproject commit 831a31d3b8ac74964be5506b4cd9b81f6755d634 diff --git a/ui/src/breezydesktop.gresource.xml b/ui/src/breezydesktop.gresource.xml index 4df66f8..016c262 100644 --- a/ui/src/breezydesktop.gresource.xml +++ b/ui/src/breezydesktop.gresource.xml @@ -2,6 +2,7 @@ gtk/connected-device.ui + gtk/license-dialog.ui gtk/no-device.ui gtk/no-extension.ui gtk/shortcut-dialog.ui diff --git a/ui/src/gtk/connected-device.ui b/ui/src/gtk/connected-device.ui index 4314de3..3a8c237 100644 --- a/ui/src/gtk/connected-device.ui +++ b/ui/src/gtk/connected-device.ui @@ -13,7 +13,7 @@ 4 - VITURE One + diff --git a/ui/src/gtk/license-dialog.ui b/ui/src/gtk/license-dialog.ui new file mode 100644 index 0000000..896b618 --- /dev/null +++ b/ui/src/gtk/license-dialog.ui @@ -0,0 +1,70 @@ + + + + + diff --git a/ui/src/gtk/shortcut-dialog.ui b/ui/src/gtk/shortcut-dialog.ui index 8435a9b..31990cb 100644 --- a/ui/src/gtk/shortcut-dialog.ui +++ b/ui/src/gtk/shortcut-dialog.ui @@ -2,6 +2,7 @@ - \ No newline at end of file + diff --git a/ui/src/gtk/window.ui b/ui/src/gtk/window.ui index 59c560f..e1a5fbd 100644 --- a/ui/src/gtk/window.ui +++ b/ui/src/gtk/window.ui @@ -26,9 +26,28 @@ + + + vertical + + + 0 + Some features expire soon + View details + + + + + + +
+ + License Details + app.license + _About BreezyDesktop app.about diff --git a/ui/src/licensedialog.py b/ui/src/licensedialog.py new file mode 100644 index 0000000..3e6e21e --- /dev/null +++ b/ui/src/licensedialog.py @@ -0,0 +1,76 @@ +from gi.repository import Adw, Gtk, GLib +from .statemanager import StateManager +from .licensetierrow import LicenseTierRow +from .licensefeaturerow import LicenseFeatureRow +from .xrdriveripc import XRDriverIPC + +@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/license-dialog.ui') +class LicenseDialog(Gtk.Dialog): + __gtype_name__ = 'LicenseDialog' + + tiers = Gtk.Template.Child() + features = Gtk.Template.Child() + request_token = Gtk.Template.Child() + verify_token = Gtk.Template.Child() + refresh_license_button = Gtk.Template.Child() + + def __init__(self): + super(Gtk.Dialog, self).__init__() + self.init_template() + + self.ipc = XRDriverIPC.get_instance() + StateManager.get_instance().connect('license-action-needed', self._handle_license) + self._handle_license(StateManager.get_instance()) + + self.request_token.connect('apply', self._on_request_token) + self.verify_token.connect('apply', self._on_verify_token) + self.refresh_license_button.connect('clicked', self._refresh_license) + + def _refresh_license(self, widget): + self.refresh_license_button.set_sensitive(False) + self.ipc.write_control_flags({'refresh_device_license': True}) + GLib.timeout_add_seconds(3, self._handle_license) + + def _handle_license(self, state_manager = None, val = None): + GLib.idle_add(self._handle_license_idle, state_manager or StateManager.get_instance()) + + def _handle_license_idle(self, state_manager): + self.refresh_license_button.set_sensitive(False) + + license_view = state_manager.state['ui_view']['license'] + self.request_token.set_visible(not state_manager.confirmed_token) + self.verify_token.set_visible(not state_manager.confirmed_token) + + for child in self.tiers: + self.tiers.remove(child) + tiers_group = Adw.PreferencesGroup(title="Paid Tier Status", margin_top=20) + self.tiers.append(tiers_group) + + for tier_name, tier_details in license_view['tiers'].items(): + tiers_group.add(LicenseTierRow(tier_name, tier_details)) + + for child in self.features: + self.features.remove(child) + features_group = Adw.PreferencesGroup(title="Feature Availability", margin_top=20) + self.features.append(features_group) + + for feature_name, feature_details in license_view['features'].items(): + features_group.add(LicenseFeatureRow(feature_name, feature_details)) + + self.refresh_license_button.set_sensitive(True) + + def _on_request_token(self, widget): + email_address = self.request_token.get_text() + self.request_token.set_editable(False) + if not self.ipc.request_token(email_address): + self.request_token.set_editable(True) + + def _on_verify_token(self, widget): + token = self.verify_token.get_text() + self.request_token.set_editable(False) + self.verify_token.set_editable(False) + if self.ipc.verify_token(token): + self.ipc.write_control_flags({'refresh_device_license': True}) + else: + self.request_token.set_editable(True) + self.verify_token.set_editable(True) \ No newline at end of file diff --git a/ui/src/licensefeaturerow.py b/ui/src/licensefeaturerow.py new file mode 100644 index 0000000..0e733ca --- /dev/null +++ b/ui/src/licensefeaturerow.py @@ -0,0 +1,30 @@ +from gi.repository import Adw, Gtk + +from .time import time_remaining_text + +FEATURE_NAMES = { + 'sbs': 'Side-by-side mode (for gaming)', + 'smooth_follow': 'Smooth Follow', + 'productivity_basic': 'Breezy Desktop', + 'productivity_pro': 'Breezy Desktop w/ multiple monitors', +} + +class LicenseFeatureRow(Adw.ActionRow): + + def __init__(self, feature, feature_details): + super().__init__() + + self.set_title(FEATURE_NAMES[feature]) + + status = 'Disabled' + is_trial = feature_details.get('is_trial') == True + if feature_details.get('is_enabled') == True: + status = 'In trial' if is_trial else 'Enabled' + + details = '' + funds_needed_in_seconds = feature_details.get('funds_needed_in_seconds') + if funds_needed_in_seconds is not None and funds_needed_in_seconds > 0: + time_remaining = time_remaining_text(funds_needed_in_seconds, is_trial) + if time_remaining: details = f" ({time_remaining} remaining)" + + self.set_subtitle(f"{status}{details}") diff --git a/ui/src/licensetierrow.py b/ui/src/licensetierrow.py new file mode 100644 index 0000000..9ee6cc7 --- /dev/null +++ b/ui/src/licensetierrow.py @@ -0,0 +1,63 @@ +from gi.repository import Adw, Gtk + +from .time import time_remaining_text + +TIER_NAMES = { + 'supporter': 'Gaming', + 'subscriber': 'Productivity', + 'subscriber_pro': 'Productivity Pro', +} + +PERIOD_DESCRIPTIONS = { + 'monthly': ' - renewing monthly', + 'yearly': ' - renewing yearly', + 'lifetime': 'with lifetime access', +} + +PERIOD_RANKS = { + 'monthly': 1, + 'yearly': 2, + 'lifetime': 3, +} + +class LicenseTierRow(Adw.ExpanderRow): + + def __init__(self, tier, tier_details): + super().__init__() + + self.set_title(TIER_NAMES[tier]) + + active_period = tier_details.get('active_period') + funds_needed_in_seconds = tier_details.get('funds_needed_in_seconds') + + status = 'Active' if active_period else 'Inactive' + details = '' + if active_period: + details += f" {PERIOD_DESCRIPTIONS[active_period]}" + if funds_needed_in_seconds is not None and funds_needed_in_seconds > 0: + time_remaining = time_remaining_text(funds_needed_in_seconds) + if time_remaining: details += f" ({time_remaining} remaining)" + if active_period == 'lifetime': + self.set_enable_expansion(False) + self.set_icon_name(None) + + self.set_expanded(False) + self.set_subtitle(f"{status}{details}") + + for period, amount in tier_details['funds_needed_by_period'].items(): + amount_text = None + if amount > 0: + amount_text = f"${amount} USD" + if active_period == period: + amount_text += " to renew" + elif active_period is not None: + amount_text += " to upgrade" + elif active_period is not None and PERIOD_RANKS[period] >= PERIOD_RANKS[active_period]: + amount_text = "Ready to auto-renew" + + if amount_text is not None: + row_widget = Adw.ActionRow(title=period.capitalize()) + row_widget.add_suffix(Gtk.Label(label=amount_text, use_markup=True)) + self.add_row(row_widget) + + diff --git a/ui/src/main.py b/ui/src/main.py index 78789b4..9901e88 100644 --- a/ui/src/main.py +++ b/ui/src/main.py @@ -25,6 +25,7 @@ gi.require_version('Adw', '1') gi.require_version('Gio', '2.0') from gi.repository import Adw, Gtk, Gio +from .licensedialog import LicenseDialog from .statemanager import StateManager from .window import BreezydesktopWindow @@ -36,6 +37,7 @@ class BreezydesktopApplication(Adw.Application): flags=Gio.ApplicationFlags.DEFAULT_FLAGS) self.create_action('quit', self.on_quit_action, ['q']) self.create_action('about', self.on_about_action) + self.create_action('license', self.on_license_action) def do_activate(self): """Called when the application is activated. @@ -61,6 +63,11 @@ class BreezydesktopApplication(Adw.Application): copyright='© 2024 Wayne Heaney') about.present() + def on_license_action(self, widget, _): + dialog = LicenseDialog() + dialog.set_transient_for(self.props.active_window) + dialog.present() + def create_action(self, name, callback, shortcuts=None): """Add an application action. diff --git a/ui/src/meson.build b/ui/src/meson.build index 674d1f1..710cec1 100644 --- a/ui/src/meson.build +++ b/ui/src/meson.build @@ -31,12 +31,16 @@ breezydesktop_sources = [ '__init__.py', 'connecteddevice.py', 'extensionsmanager.py', + 'licensedialog.py', + 'licensefeaturerow.py', + 'licensetierrow.py', 'main.py', 'nodevice.py', 'noextension.py', 'settingsmanager.py', 'shortcutdialog.py', 'statemanager.py', + 'time.py', 'window.py' ] diff --git a/ui/src/statemanager.py b/ui/src/statemanager.py index 78c54e1..98fd97b 100644 --- a/ui/src/statemanager.py +++ b/ui/src/statemanager.py @@ -1,7 +1,12 @@ +import sys import threading from gi.repository import GObject +from .time import LICENSE_WARN_SECONDS from .xrdriveripc import XRDriverIPC +# shouldn't need a number larger than a year +LICENSE_ACTION_NEEDED_MAX = 60 * 60 * 24 * 366 + class Logger: def info(self, message): print(message) @@ -11,11 +16,13 @@ class Logger: class StateManager(GObject.GObject): __gsignals__ = { - 'device-update': (GObject.SIGNAL_RUN_FIRST, None, (str,)) + 'device-update': (GObject.SIGNAL_RUN_FIRST, None, (str,)), + 'license-action-needed': (GObject.SIGNAL_RUN_FIRST, None, (bool,)), } __gproperties__ = { - 'follow-mode': (bool, 'Follow Mode', 'Whether the follow mode is enabled', False, GObject.ParamFlags.READWRITE) + 'follow-mode': (bool, 'Follow Mode', 'Whether the follow mode is enabled', False, GObject.ParamFlags.READWRITE), + 'license-action-needed-seconds': (int, 'License Action Needed Seconds', 'The remaining time until the license action is needed', 0, LICENSE_ACTION_NEEDED_MAX, 0, GObject.ParamFlags.READWRITE), } _instance = None @@ -44,6 +51,9 @@ class StateManager(GObject.GObject): GObject.GObject.__init__(self) self.ipc = XRDriverIPC.get_instance() self.connected_device_name = None + self.license_action_needed = False + self.license_action_needed_seconds = 0 + self.confirmed_token = False self.start() @@ -61,14 +71,32 @@ class StateManager(GObject.GObject): self.connected_device_name = new_device_name self.emit('device-update', self.connected_device_name) + license_view = self.state['ui_view'].get('license') + if license_view: + confirmed_token = license_view.get('confirmed_token') == True + action_needed_details = license_view.get('action_needed') + action_needed_seconds = action_needed_details.get('seconds') if action_needed_details else None + + action_needed = action_needed_seconds is not None and action_needed_seconds < LICENSE_WARN_SECONDS + if (action_needed != self.license_action_needed or self.confirmed_token != confirmed_token): + self.license_action_needed = action_needed + self.license_action_needed_seconds = action_needed_seconds + self.confirmed_token = confirmed_token + self.emit('license-action-needed', action_needed or not confirmed_token) + self.set_property('follow-mode', self.state.get('breezy_desktop_smooth_follow_enabled')) + self.set_property('license-action-needed-seconds', self.license_action_needed_seconds) if self.running: threading.Timer(1.0, self._refresh_state).start() def do_set_property(self, prop, value): if prop.name == 'follow-mode': self.follow_mode = value + if prop.name == 'license-action-needed-seconds': + self.license_action_needed_seconds = value def do_get_property(self, prop): if prop.name == 'follow-mode': - return self.follow_mode \ No newline at end of file + return self.follow_mode + if prop.name == 'license-action-needed-seconds': + return self.license_action_needed_seconds \ No newline at end of file diff --git a/ui/src/time.py b/ui/src/time.py new file mode 100644 index 0000000..10989cf --- /dev/null +++ b/ui/src/time.py @@ -0,0 +1,19 @@ +from math import floor + +# we'll begin to alert the user when there's less than a week left +LICENSE_WARN_SECONDS = 60 * 60 * 24 * 7 + +def time_remaining_text(seconds, no_cap=False): + if not seconds: + return + + if seconds / 60 < 60: + return 'less than an hour' + elif seconds / (60 * 60) < 24: + time_remaining = floor(seconds / (60 * 60)) + return '1 hour' if time_remaining == 1 else f'{time_remaining} hours' + elif seconds / (24 * 60 * 60) < 30 or no_cap: + time_remaining = floor(seconds / (24 * 60 * 60)) + return '1 day' if time_remaining == 1 else f'{time_remaining} days' + else: + return \ No newline at end of file diff --git a/ui/src/window.py b/ui/src/window.py index 3f719d9..a96057c 100644 --- a/ui/src/window.py +++ b/ui/src/window.py @@ -17,39 +17,62 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from gi.repository import Gtk +from gi.repository import Gtk, GLib from .extensionsmanager import ExtensionsManager +from .licensedialog import LicenseDialog from .statemanager import StateManager from .connecteddevice import ConnectedDevice from .nodevice import NoDevice from .noextension import NoExtension +from .time import LICENSE_WARN_SECONDS @Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/window.ui') class BreezydesktopWindow(Gtk.ApplicationWindow): __gtype_name__ = 'BreezydesktopWindow' + main_content = Gtk.Template.Child() + license_action_needed_banner = Gtk.Template.Child() + def __init__(self, **kwargs): super().__init__(**kwargs) self.state_manager = StateManager.get_instance() self.state_manager.connect('device-update', self._handle_device_update) + self.state_manager.connect('license-action-needed', self._handle_device_update) self.connected_device = ConnectedDevice() self.no_device = NoDevice() self.no_extension = NoExtension() - self._handle_device_update(self.state_manager, StateManager.device_name(self.state_manager.state)) + self.license_action_needed_banner.connect('button-clicked', self._on_license_action_needed_button_clicked) + + self._handle_device_update(self.state_manager, None) self.connect("destroy", self._on_window_destroy) - def _handle_device_update(self, state_manager, connected_device_name): + def _handle_device_update(self, state_manager, val): + GLib.idle_add(self._handle_device_update_gui, state_manager) + + def _handle_device_update_gui(self, state_manager): + license_action_needed_seconds = state_manager.get_property('license-action-needed-seconds') + show_banner = license_action_needed_seconds is not None and license_action_needed_seconds < LICENSE_WARN_SECONDS + self.license_action_needed_banner.set_revealed(show_banner) + + for child in self.main_content: + self.main_content.remove(child) + if not ExtensionsManager.get_instance().is_installed(): - self.set_child(self.no_extension) - elif connected_device_name: - self.set_child(self.connected_device) - self.connected_device.set_device_name(connected_device_name) + self.main_content.append(self.no_extension) + elif state_manager.connected_device_name: + self.main_content.append(self.connected_device) + self.connected_device.set_device_name(state_manager.connected_device_name) else: - self.set_child(self.no_device) + self.main_content.append(self.no_device) + + def _on_license_action_needed_button_clicked(self, widget): + dialog = LicenseDialog() + dialog.set_transient_for(widget.get_ancestor(Gtk.Window)) + dialog.present() def _on_window_destroy(self, widget): self.state_manager.disconnect_by_func(self._handle_device_update) \ No newline at end of file