diff --git a/app_src/hot_reload.py b/app_src/hot_reload.py index e3cda5f..08ca43d 100644 --- a/app_src/hot_reload.py +++ b/app_src/hot_reload.py @@ -65,6 +65,7 @@ class MDLive(App,MDApp): "MyBtmSheet":"ui.screens.gallery_screen", # "NotificationScreen": "important", "WelcomeScreen": "ui.screens.welcome_screen", + "StatsScreen": "ui.screens.stats_screen", # "MyRoundButton": "ui.widgets.buttons", "BottomNavigationBar": "ui.widgets.buttons", "DialogScreen": "ui.widgets.modals", @@ -77,7 +78,7 @@ class MDLive(App,MDApp): def build_app(self, *args): # return Factory.MyPopUp() # return Factory.DialogScreen(ok_callback=None) - return Factory.FullscreenScreen() + return Factory.StatsScreen() def on_start(self): # self.theme_cls.theme_style = "Light" @@ -97,7 +98,6 @@ def monitor_dark_and_light_device_change(self): MDLive().run() - # from jnius import PythonJavaClass, java_method diff --git a/app_src/ui/screens/gallery_screen.kv b/app_src/ui/screens/gallery_screen.kv index 7e734a8..06fe2cf 100644 --- a/app_src/ui/screens/gallery_screen.kv +++ b/app_src/ui/screens/gallery_screen.kv @@ -119,6 +119,12 @@ adaptive_size: True padding: [0,0,0,dp(20)] # md_bg_color:[1,0,1,1] + MDIconButton: + icon: "delete-sweep" + icon: "folder-cog" + theme_icon_color:"Custom" + icon_color: app.theme_colors.ICON + on_release: setattr(app.sm,"current","stats") MDIconButton: icon: "view-dashboard-outline" theme_icon_color:"Custom" diff --git a/app_src/ui/screens/manager.py b/app_src/ui/screens/manager.py index 709458f..d72fd68 100644 --- a/app_src/ui/screens/manager.py +++ b/app_src/ui/screens/manager.py @@ -8,6 +8,7 @@ from kivymd.uix.screenmanager import MDScreenManager from android_notify import NotificationHandler +from ui.screens.stats_screen import StatsScreen from ui.widgets.layouts import MyMDScreen from utils.android import DisplayListener from utils.logger import app_logger @@ -32,6 +33,7 @@ def __init__(self, **kwargs): self.settings_screen = SettingsScreen() self.log_screen = LogsScreen() self.download_apk_screen = DownloadApkScreen() + self.stats_screen = StatsScreen() self.add_widget(self.gallery_screen) self.add_widget(self.full_screen) @@ -39,6 +41,7 @@ def __init__(self, **kwargs): self.add_widget(self.welcome_screen) self.add_widget(self.log_screen) self.add_widget(self.download_apk_screen) + self.add_widget(self.stats_screen) self.__register_rotate_listener() # self.__run_rotate_method_for_each_screen("BOTTOM") @@ -61,13 +64,12 @@ def __register_rotate_listener(self): app_logger.exception(error_registering_rotate_listener) traceback.print_exc() - def on_current(self,*args): - screen_name = args[1] - self.btm_nav_patch(screen_name) - super().on_current(instance=args[0],value=args[1]) + def on_current(self,instance, value): + self.btm_nav_patch(value) + super().on_current(instance=instance,value=value) def btm_nav_patch(self, screen_name): - is_fullscreen = screen_name in ["welcome", "fullscreen", "logs", "update_screen"] + is_fullscreen = screen_name in ["welcome", "fullscreen", "logs", "update_screen","stats"] if is_fullscreen and self.app.bottom_bar: self.app.bottom_bar.hide(animation=False, hidden_by=self) elif self.app.bottom_bar: diff --git a/app_src/ui/screens/stats_screen.py b/app_src/ui/screens/stats_screen.py new file mode 100644 index 0000000..6a47ced --- /dev/null +++ b/app_src/ui/screens/stats_screen.py @@ -0,0 +1,269 @@ +import random + +from kivy.properties import StringProperty +from kivy.clock import Clock +from kivy.utils import get_color_from_hex +from kivy.uix.scrollview import ScrollView +from kivy.metrics import dp +from kivy.uix.widget import Widget +from kivy.graphics import Color, Line + + +from kivymd.uix.button import MDIconButton, MDButton +from kivymd.uix.label import MDLabel + + +from ui.widgets.layouts import MyMDScreen, GenericStatusBarSpacer, Row, Column + + + +# 1. Define a reusable, self-updating horizontal line widget +class LineDivider(Widget): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.size_hint_y = None + self.height = 2 # Set line thickness space area + + with self.canvas: + Color(*get_color_from_hex("5E5E5E")) + self.line = Line(points=[], width=0.5) + + # Bind it to its own size/position changes + self.bind(pos=self.update_line, size=self.update_line) + + def update_line(self, *args): + # Keeps the line perfectly straight along its own layout position + self.line.points = [self.x, self.y, self.x + self.width, self.y] + + +from kivy.uix.anchorlayout import AnchorLayout +from kivymd.uix.button import MDButtonText + +class MyTextButton(MDButton): + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self.fix_width,2) + def adjust_width(self,*gg): + pass + def fix_width(self, *_): + pass + +class StatsListItem(Row): # Assuming Row inherits from horizontal MDBoxLayout + title = StringProperty() + size_txt = StringProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + grey_color=get_color_from_hex("999898") + + self.title_label = MDLabel( + text=self.title, + theme_text_color="Custom", text_color=(1, 1, 1, 1), + theme_font_name="Custom", font_name = "RobotoMono", + theme_font_size="Custom", font_size="15sp", + adaptive_height=True, pos_hint={"center_y": 0.5}, + ) + self.size_label = MDLabel( + text=self.size_txt,theme_text_color="Custom", text_color=grey_color, + theme_font_name="Custom", font_name = "RobotoMono", + theme_font_size="Custom", font_size="13sp", + adaptive_height=True, pos_hint={"center_y": 0.5}, + halign="right" + ) + + # 1. Create a wrapper to isolate and center the button horizontally + button_container = AnchorLayout(anchor_x="center", anchor_y="center") + + self.button = MyTextButton( + style="outlined", + theme_line_color="Custom", + line_color=grey_color, + radius=[4, ], + # 'theme_elevation_level', + # 'theme_icon_color', 'theme_line_color', 'theme_shadow_color', + # 'theme_shadow_offset', 'theme_shadow_softness', + # padding=[4, ] + ) + self.theme_height="Custom" + self.theme_width="Custom" + # self.button.width=dp(10) + self.button.height=dp(35) + self.button_text = MDButtonText( + text="Remove", + theme_text_color="Custom", text_color=(1, 1, 1, 1), + theme_font_name="Custom", font_name = "RobotoMono", + theme_font_size="Custom", font_size="12sp" + ) + self.button.add_widget(self.button_text) + + # 2. Add button to container, then container to your row layout + button_container.add_widget(self.button) + + self.add_widget(self.title_label) + self.add_widget(self.size_label) + self.add_widget(button_container) # This acts as the third centered column + + # self.md_bg_color = (random.random(), random.random(), random.random(), 1) + # self.adaptive_height=10 + self.padding=[10,0] + self.size_hint_y=None + self.height = dp(40) + +class StatsScreen(MyMDScreen): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.name = "stats" + self.status_bar_spacer = GenericStatusBarSpacer( + status_bar_height=self.status_bar_height, + md_bg_color=[.1, .1, .1, 1] + ) + self.md_bg_color = get_color_from_hex("252424") + self.header_section = Row( + pos_hint = {'center_y': 1}, + adaptive_height = True, + # md_bg_color = [.1, .1, 1, 1], + spacing = dp(10), + padding=10, + ) + self.header_btn = MDIconButton( + icon="chevron-left", + style="tonal", + size=(dp(70), dp(70)), + pos_hint={'center_y': 0.5}, + theme_text_color='Custom', + text_color=[1, 1, 1, 1], + on_release=self.handle_going_back, + theme_bg_color='Custom' + ) + self.header_label = MDLabel( + text="Manage Storage", + theme_text_color="Custom", text_color=(1, 1, 1, 1), + theme_font_name="Custom", font_name = "RobotoMono" + ) + self.header_btn_2 = MDIconButton( + icon="refresh", + style="tonal", + size=(dp(70), dp(70)), + pos_hint={'center_y': 0.45}, + theme_text_color='Custom', + text_color=[1, 1, 1, 1], + on_release=self.handle_going_back, + theme_bg_color='Custom' + ) + + # Scroll area + scroll = ScrollView(size_hint=(1, 1)) + sections_container = Column( + # md_bg_color=[1, .4, .4, 1], + # size_hint_y=None, + spacing=dp(6), + # padding=(0, dp(4)) + ) + sections_container.bind(minimum_height=sections_container.setter("height")) + + self.section_layout = Column(padding=10, + adaptive_height=True, + spacing=dp(10), + # md_bg_color=[1, 1, .1, 1] + ) + self.section_title_1= MDLabel( + text="Pictures",bold=1, + theme_text_color="Custom", text_color=(1, 1, 1, 1), + theme_font_name="Custom", font_name = "RobotoMono", + adaptive_height=1,padding=[10] + + ) + self.sub_text_1 = StatsListItem(title="Both", size_txt="70KB") + self.sub_text_2 = StatsListItem(title="Day", size_txt="5MB") + self.sub_text_3 = StatsListItem(title="Noon", size_txt="200KB",) + self.sub_text_3.padding=[10,0,10,10] + self.sub_text_3.height=dp(50) + + + self.section_layout_2 = Column( + padding=[10,10,10,20], + adaptive_height=True, + spacing=dp(10), + # md_bg_color=[1, .1, .1, 1] + ) + self.section_title_2 = MDLabel( + text="Images", + theme_text_color="Custom", text_color=(1, 1, 1, 1), + theme_font_name="Custom", font_name = "RobotoMono", + adaptive_height=1,padding=[10] + + ) + self.sub_text_4 = StatsListItem(title="Cache", size_txt="100KB") + self.sub_text_5 = StatsListItem(title="Config", size_txt="50KB") + self.sub_text_5.padding = [10, 0, 10, 10] + self.sub_text_5.height = dp(50) + + + + self.add_widget(self.status_bar_spacer) + self.header_section.add_widget(self.header_btn) + self.header_section.add_widget(self.header_label) + self.header_section.add_widget(self.header_btn_2) + + + from ui.widgets.charts import HorizontalBarChart + + self.graph_container = Column( + adaptive_height=True, + padding=[16, 12, 16, 16], + spacing=dp(12), + md_bg_color=get_color_from_hex("2E2E2E"), + radius=[12, 12, 12, 12], + ) + self.graph_title = MDLabel( + text="Storage Usage", + bold=1, + theme_text_color="Custom", text_color=(1, 1, 1, 1), + theme_font_name="Custom", font_name="RobotoMono", + theme_font_size="Custom", font_size="15sp", + adaptive_height=True, + ) + self.storage_chart = HorizontalBarChart( + data=[ + ("Both", 70 * 1024), + ("Day", 5 * 1024 * 1024), + ("Noon", 200 * 1024), + ("Cache", 100 * 1024), + ("Config", 50 * 1024), + ], + size_hint_y=None, + height=dp(210), + ) + self.graph_container.add_widget(self.graph_title) + self.graph_container.add_widget(self.storage_chart) + sections_container.add_widget(self.graph_container) + sections_container.add_widget(LineDivider()) + sections_container.add_widget(self.section_title_1) + sections_container.add_widget(self.sub_text_1) + sections_container.add_widget(self.sub_text_2) + sections_container.add_widget(self.sub_text_3) + # sections_container.add_widget(self.section_layout) + + # Section 2 + sections_container.add_widget(LineDivider()) + self.section_layout_2.add_widget(self.section_title_2) + self.section_layout_2.add_widget(self.sub_text_4) + self.section_layout_2.add_widget(self.sub_text_5) + sections_container.add_widget(self.section_layout_2) + + scroll.add_widget(sections_container) + self.add_widget(self.header_section) + self.add_widget(scroll) + + self.build_ui() + + @staticmethod + def update_line(parent_widget, widget): + # Keeps the Y coordinate identical (self.center_y) to ensure it stays perfectly flat + widget.points = [parent_widget.x, parent_widget.center_y, parent_widget.x + parent_widget.width, parent_widget.center_y] + + def build_ui(self): + pass + + def handle_going_back(self,*_): + self.manager.go_to_thumbs() diff --git a/app_src/ui/widgets/charts.py b/app_src/ui/widgets/charts.py new file mode 100644 index 0000000..288a3e7 --- /dev/null +++ b/app_src/ui/widgets/charts.py @@ -0,0 +1,107 @@ +from kivy.core.text import Label as CoreLabel +from kivy.graphics import Color, Line, Rectangle, RoundedRectangle +from kivy.metrics import dp, sp +from kivy.properties import ListProperty +from kivy.utils import get_color_from_hex +from kivy.uix.widget import Widget + + +def format_bytes(size): + if size >= 1024 * 1024: + return f"{size / (1024 * 1024):.1f} MB" + if size >= 1024: + return f"{int(round(size / 1024))} KB" + return f"{size} B" + + +class HorizontalBarChart(Widget): + """Canvas-drawn horizontal bar chart. + + data: list of (label, value) tuples. Values are byte counts. + """ + + data = ListProperty([]) + bar_color = ListProperty(get_color_from_hex("98F1DD")) + track_color = ListProperty(get_color_from_hex("2E2E2E")) + grid_color = ListProperty(get_color_from_hex("4A4A4A")) + label_color = ListProperty(get_color_from_hex("FFFFFF")) + value_color = ListProperty(get_color_from_hex("999898")) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._label_textures = {} + self._value_textures = {} + self.bind(pos=self._redraw, size=self._redraw, data=self._redraw, + bar_color=self._redraw, track_color=self._redraw, + grid_color=self._redraw, label_color=self._redraw, + value_color=self._redraw) + + def _texture(self, text, color, font_size, cache): + tex = cache.get(text) + if tex is None: + label = CoreLabel(text=text, font_size=font_size, color=color, + font_name="RobotoMono", bold=True) + label.refresh() + tex = label.texture + cache[text] = tex + return tex + + def _redraw(self, *_): + self.canvas.clear() + data = self.data or [] + if not data: + return + width, height = self.width, self.height + if width <= 0 or height <= 0: + return + + label_w = dp(64) + value_w = dp(58) + plot_x = self.x + label_w + plot_w = width - label_w - value_w + if plot_w <= 0: + return + + max_value = max(value for _, value in data) + row_h = height / len(data) + bar_h = row_h * 0.5 + + for fraction in (0.0, 0.25, 0.5, 0.75, 1.0): + grid_x = plot_x + plot_w * fraction + with self.canvas: + Color(*self.grid_color) + Line(points=[grid_x, self.y, grid_x, self.y + height], width=1) + + for index, (label, value) in enumerate(data): + row_center = self.y + height - row_h * (index + 0.5) + bar_y = row_center - bar_h / 2 + fraction = value / max_value if max_value else 0.0 + bar_w = plot_w * fraction + + with self.canvas: + Color(*self.track_color) + RoundedRectangle(pos=(plot_x, bar_y), size=(plot_w, bar_h), + radius=[4]) + Color(*self.bar_color) + RoundedRectangle(pos=(plot_x, bar_y), size=(bar_w, bar_h), + radius=[4]) + + label_tex = self._texture(label, self.label_color, sp(13), + self._label_textures) + with self.canvas: + Color(1, 1, 1, 1) + Rectangle(texture=label_tex, + pos=(plot_x - label_tex.width - dp(6), + row_center - label_tex.height / 2), + size=label_tex.size) + + value_text = format_bytes(value) + value_tex = self._texture(value_text, self.value_color, sp(12), + self._value_textures) + value_x = min(plot_x + bar_w + dp(8), + plot_x + plot_w - value_tex.width) + with self.canvas: + Color(1, 1, 1, 1) + Rectangle(texture=value_tex, + pos=(value_x, row_center - value_tex.height / 2), + size=value_tex.size)