Add license UI (#23)

This commit is contained in:
Wayne Heaney 2024-05-22 15:12:23 -07:00 committed by GitHub
parent b8e2310461
commit a7dd9885e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 365 additions and 31 deletions

View File

@ -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

@ -1 +1 @@
Subproject commit f379c976e003823ed6a61fe2d054c802b3bca80d
Subproject commit 8e5513c89df30ca2cfdcd03947dcb8d9562db6b3

@ -1 +1 @@
Subproject commit b2cdc8cc5fc593ab04015cec08a8e97b103ffa63
Subproject commit 831a31d3b8ac74964be5506b4cd9b81f6755d634

View File

@ -2,6 +2,7 @@
<gresources>
<gresource prefix="/com/xronlinux/BreezyDesktop">
<file preprocess="xml-stripblanks">gtk/connected-device.ui</file>
<file preprocess="xml-stripblanks">gtk/license-dialog.ui</file>
<file preprocess="xml-stripblanks">gtk/no-device.ui</file>
<file preprocess="xml-stripblanks">gtk/no-extension.ui</file>
<file preprocess="xml-stripblanks">gtk/shortcut-dialog.ui</file>

View File

@ -13,7 +13,7 @@
<property name="column-spacing">4</property>
<child>
<object class="GtkLabel" id="device_label">
<property name="label">VITURE One</property>
<property name="label"></property>
</object>
</child>
<child>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="LicenseDialog" parent="GtkDialog">
<property name="title">License Details</property>
<property name="modal">1</property>
<property name="default_width">440</property>
<property name="default_height">200</property>
<property name="use-header-bar">1</property>
<child type="action">
<object class="GtkButton" id="refresh_license_button">
<property name="icon-name">view-refresh-symbolic</property>
</object>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="margin-start">20</property>
<property name="margin-end">20</property>
<child>
<object class="AdwPreferencesGroup">
<property name="margin-top">10</property>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Donate</property>
<property name="subtitle" translatable="true">ko-fi.com/wheaney</property>
<property name="subtitle-selectable">True</property>
<child type="suffix">
<object class="GtkLinkButton">
<property name="icon-name">go-next-symbolic</property>
<property name="uri">https://ko-fi.com/wheaney</property>
</object>
</child>
<style>
<class name="property"/>
</style>
</object>
</child>
<child>
<object class="AdwEntryRow" id="request_token">
<property name="visible">0</property>
<property name="title" translatable="true">Request a token</property>
<property name="input-purpose">6</property>
<property name="show-apply-button">1</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="verify_token">
<property name="visible">0</property>
<property name="title" translatable="true">Verify token</property>
<property name="input-hints">16</property>
<property name="show-apply-button">1</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="tiers">
</object>
</child>
<child>
<object class="GtkBox" id="features">
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -2,6 +2,7 @@
<interface>
<requires lib="gtk" version="4.0"/>
<template class="ShortcutDialog" parent="GtkDialog">
<property name="title">Assign Keyboard Shortcut</property>
<property name="modal">1</property>
<property name="default_width">440</property>
<property name="default_height">200</property>
@ -21,23 +22,8 @@
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="title-widget">
<object class="GtkLabel">
<property name="label" translatable="yes">Keyboard Shortcut</property>
<property name="single-line-mode">1</property>
<property name="ellipsize">end</property>
<property name="width-chars">5</property>
<style>
<class name="title"/>
</style>
</object>
</property>
</object>
</child>
<child>
<object class="GtkEventControllerKey" id="event_controller" />
</child>
</template>
</interface>
</interface>

View File

@ -26,9 +26,28 @@
</child>
</object>
</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwBanner" id="license_action_needed_banner">
<property name="revealed">0</property>
<property name="title" translatable="yes">Some features expire soon</property>
<property name="button-label" translatable="yes">View details</property>
</object>
</child>
<child>
<object class="GtkBox" id="main_content" />
</child>
</object>
</property>
</template>
<menu id="primary_menu">
<section>
<item>
<attribute name="label" translatable="yes">License Details</attribute>
<attribute name="action">app.license</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_About BreezyDesktop</attribute>
<attribute name="action">app.about</attribute>

76
ui/src/licensedialog.py Normal file
View File

@ -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)

View File

@ -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}")

63
ui/src/licensetierrow.py Normal file
View File

@ -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"<b>${amount}</b> 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)

View File

@ -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, ['<primary>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.

View File

@ -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'
]

View File

@ -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
return self.follow_mode
if prop.name == 'license-action-needed-seconds':
return self.license_action_needed_seconds

19
ui/src/time.py Normal file
View File

@ -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

View File

@ -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)