completed the basic ui and anilist module

This commit is contained in:
Benedict Xavier Wanyonyi
2024-05-15 20:33:20 +03:00
commit ffe5db9e33
44 changed files with 3775 additions and 0 deletions

View File

View File

@@ -0,0 +1,96 @@
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import StringProperty kivy.properties.StringProperty
# custom sets for existing
<MDBoxLayout>
size_hint: 1,1
# custom components
<CommonNavigationRailItem@MDNavigationRailItem>
icon:""
text:""
<CommonNavigationRailItem>
MDNavigationRailItemIcon:
icon:root.icon
MDNavigationRailItemLabel:
text: root.text
<MainScreenView>
md_bg_color: self.theme_cls.backgroundColor
main_container:main_container
MDBoxLayout:
MDNavigationRail:
anchor:"top"
type: "selected"
md_bg_color: self.theme_cls.secondaryContainerColor
MDNavigationRailFabButton:
icon: "home-outline"
CommonNavigationRailItem:
icon: "magnify"
text: "Search"
on_release:
# print("r")
root.manager_screens.current = "search screen"
CommonNavigationRailItem:
icon: "bookmark-outline"
text: "Bookmark"
CommonNavigationRailItem:
icon: "library-outline"
text: "Library"
CommonNavigationRailItem:
icon: "cog"
text: "settings"
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
MDBoxLayout:
orientation: 'vertical'
id:p
MDBoxLayout:
pos_hint: {'center_x': 0.5,'top': 1}
padding: "10dp"
size_hint_y:None
height: self.minimum_height
size_hint_x:.75
spacing: '20dp'
MDTextField:
size_hint_x:1
MDTextFieldLeadingIcon:
icon: "magnify"
MDTextFieldHintText:
text: "Search for anime"
# MDTextFieldTrailingIcon:
# icon: "filter"
MDIconButton:
pos_hint: {'center_y': 0.5}
icon: "account-circle"
# size: 32,32
MDScrollView:
# do_scroll_y:True
# do_scroll_x:False
size_hint:1,1
# height:main_container.minimum_height
MDBoxLayout:
id:main_container
padding:"50dp","5dp","50dp","150dp"
spacing:"10dp"
orientation: 'vertical'
size_hint_y:None
height:max(self.minimum_height,p.height,1800)
adaptive_height:True
# MDBoxLayout:
# size_hint_y:None
# height:self.minimum_height
# MDLabel:
# text: "By BeneX"

View File

@@ -0,0 +1,16 @@
from kivy.properties import ObjectProperty
from View.base_screen import BaseScreenView
class MainScreenView(BaseScreenView):
main_container = ObjectProperty()
def write_data(self):
self.controller.write_data()
def model_is_changed(self) -> None:
"""
Called whenever any change has occurred in the data model.
The view in this method tracks these changes and updates the UI
according to these changes.
"""

View File

@@ -0,0 +1,76 @@
<MyListScreenView>
md_bg_color: self.theme_cls.backgroundColor
my_list_container:my_list_container
MDBoxLayout:
size_hint:1,1
MDNavigationRail:
anchor:"top"
type: "selected"
md_bg_color: self.theme_cls.secondaryContainerColor
MDNavigationRailFabButton:
icon: "home"
on_release:
root.manager_screens.current = "main screen"
CommonNavigationRailItem:
icon: "magnify"
text: "Search"
# on_release:
# root.manager_screens.current_screen = "search screen"
CommonNavigationRailItem:
icon: "bookmark-outline"
text: "Bookmark"
CommonNavigationRailItem:
icon: "library-outline"
text: "Library"
CommonNavigationRailItem:
icon: "cog"
text: "settings"
# ScreenManager:
# MDScreen:
# name:"main"
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
size_hint:1,1
MDBoxLayout:
orientation: 'vertical'
id:p
size_hint:1,1
MDBoxLayout:
pos_hint: {'center_x': 0.5,'top': 1}
padding: "10dp"
size_hint_y:None
height: self.minimum_height
size_hint_x:.75
spacing: '20dp'
MDTextField:
size_hint_x:1
required:True
on_text_validate:
root.handle_search_for_anime(args[0])
MDTextFieldLeadingIcon:
icon: "magnify"
MDTextFieldHintText:
text: "Search for anime"
# MDTextFieldTrailingIcon:
# icon: "filter"
MDIconButton:
pos_hint: {'center_y': 0.5}
icon: "account-circle"
# size: 32,32
MDScrollView:
size_hint:1,1
MDGridLayout:
spacing: '10dp'
padding: "75dp","50dp","10dp","100dp"
id:my_list_container
cols:5
size_hint_y:None
height:self.minimum_height

View File

@@ -0,0 +1,28 @@
from kivy.properties import ObjectProperty,StringProperty,DictProperty
from View.base_screen import BaseScreenView
class MyListScreenView(BaseScreenView):
my_list_container = ObjectProperty()
def model_is_changed(self) -> None:
"""
Called whenever any change has occurred in the data model.
The view in this method tracks these changes and updates the UI
according to these changes.
"""
def handle_search_for_anime(self,search_widget):
search_term = search_widget.text
if search_term and not(self.is_searching):
self.search_term = search_term
self.search_results_container.clear_widgets()
if self.filters:
self.controller.requested_search_for_anime(search_term,**self.filters)
else:
self.controller.requested_search_for_anime(search_term)
def update_layout(self,widget):
self.search_results_container.add_widget(widget)
def add_pagination(self,pagination_info):
pass

View File

View File

@@ -0,0 +1,80 @@
<SearchScreenView>
md_bg_color: self.theme_cls.backgroundColor
search_results_container:search_results_container
MDBoxLayout:
size_hint:1,1
MDNavigationRail:
anchor:"top"
type: "selected"
md_bg_color: self.theme_cls.secondaryContainerColor
MDNavigationRailFabButton:
icon: "home"
on_release:
root.manager_screens.current = "main screen"
CommonNavigationRailItem:
icon: "magnify"
text: "Search"
# on_release:
# root.manager_screens.current_screen = "search screen"
CommonNavigationRailItem:
icon: "bookmark-outline"
text: "My Anime List"
on_release:
root.manager_screens.current = "my list screen"
CommonNavigationRailItem:
icon: "library-outline"
text: "Library"
CommonNavigationRailItem:
icon: "cog"
text: "settings"
# ScreenManager:
# MDScreen:
# name:"main"
MDAnchorLayout:
anchor_y: 'top'
padding:"10dp"
size_hint:1,1
MDBoxLayout:
orientation: 'vertical'
id:p
size_hint:1,1
MDBoxLayout:
pos_hint: {'center_x': 0.5,'top': 1}
padding: "10dp"
size_hint_y:None
height: self.minimum_height
size_hint_x:.75
spacing: '20dp'
MDTextField:
size_hint_x:1
required:True
on_text_validate:
root.handle_search_for_anime(args[0])
MDTextFieldLeadingIcon:
icon: "magnify"
MDTextFieldHintText:
text: "Search for anime"
# MDTextFieldTrailingIcon:
# icon: "filter"
MDIconButton:
pos_hint: {'center_y': 0.5}
icon: "account-circle"
# size: 32,32
MDScrollView:
size_hint:1,1
MDGridLayout:
id:search_results_container
spacing: '10dp'
padding: "75dp","75dp","10dp","200dp"
cols:5
size_hint_y:None
height:self.minimum_height

View File

@@ -0,0 +1,31 @@
from kivy.properties import ObjectProperty,StringProperty,DictProperty
from View.base_screen import BaseScreenView
class SearchScreenView(BaseScreenView):
search_results_container = ObjectProperty()
search_term = StringProperty()
filters = DictProperty()
is_searching = False
def model_is_changed(self) -> None:
"""
Called whenever any change has occurred in the data model.
The view in this method tracks these changes and updates the UI
according to these changes.
"""
def handle_search_for_anime(self,search_widget):
search_term = search_widget.text
if search_term and not(self.is_searching):
self.search_term = search_term
self.search_results_container.clear_widgets()
if self.filters:
self.controller.requested_search_for_anime(search_term,**self.filters)
else:
self.controller.requested_search_for_anime(search_term)
def update_layout(self,widget):
self.search_results_container.add_widget(widget)
def add_pagination(self,pagination_info):
pass

3
app/View/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .MainScreen.main_screen import MainScreenView
from .SearchScreen.search_screen import SearchScreenView
from .MylistScreen.my_list_screen import MyListScreenView

45
app/View/base_screen.py Normal file
View File

@@ -0,0 +1,45 @@
from kivy.properties import ObjectProperty
from kivymd.app import MDApp
from kivymd.uix.screen import MDScreen
from Utility.observer import Observer
class BaseScreenView(MDScreen, Observer):
"""
A base class that implements a visual representation of the model data.
The view class must be inherited from this class.
"""
controller = ObjectProperty()
"""
Controller object - :class:`~Controller.controller_screen.ClassScreenControler`.
:attr:`controller` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
model = ObjectProperty()
"""
Model object - :class:`~Model.model_screen.ClassScreenModel`.
:attr:`model` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
manager_screens = ObjectProperty()
"""
Screen manager object - :class:`~kivymd.uix.screenmanager.MDScreenManager`.
:attr:`manager_screens` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
def __init__(self, **kw):
super().__init__(**kw)
# Often you need to get access to the application object from the view
# class. You can do this using this attribute.
self.app = MDApp.get_running_app()
# Adding a view class as observer.
self.model.add_observer(self)

View File

@@ -0,0 +1 @@
from .media_card import MediaCard,MediaCardsContainer

View File

@@ -0,0 +1 @@
from .media_card import MediaCard,MediaCardsContainer

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,249 @@
#:import get_hex_from_color kivy.utils.get_hex_from_color
#:set yellow [.9,.9,0,.9]
# overides and customization
<MDLabel>:
adaptive_height:True
max_lines:1
valign:"top"
shorten_from:"right"
shorten:True
<MDLabelShortened@MDLabel>
bold:True
<MDIcon>
valign:"center"
<MDBoxLayout>:
# adaptive_height:True
size_hint_y:None
height:self.minimum_height
# custom components
<Tooltip>
MDTooltipPlain:
text:root.tooltip_text
# <TooltipMDIconButton>
<MediaPopup>
size_hint: None, None
height: dp(500)
width: dp(400)
radius:[5,5,5,5]
md_bg_color:self.theme_cls.backgroundColor
anchor_y: 'top'
player:player
MDBoxLayout:
orientation: 'vertical'
adaptive_height:False
anchor_y: 'top'
MDRelativeLayout:
size_hint_y: None
height: dp(250)
line_color:root.caller.has_trailer_color
line_width:2
MediaPopupVideoPlayer:
id:player
source:root.caller.trailer_url
preview:root.caller.preview_image
state:"play" if root.caller.trailer_url else "stop"
fit_mode:"fill"
size_hint_y: None
height: dp(250)
MDBoxLayout:
padding: "10dp","5dp"
spacing:"5dp"
pos_hint: {'left': 1,'top': 1}
adaptive_height:True
MDIcon:
icon: "star"
color:yellow
disabled: not(root.caller.stars[0])
MDIcon:
color:yellow
disabled: not(root.caller.stars[1])
icon: "star"
MDIcon:
color:yellow
disabled: not(root.caller.stars[2])
icon: "star"
MDIcon:
color:yellow
disabled: not(root.caller.stars[3])
icon: "star"
MDIcon:
color:yellow
icon: "star"
disabled: not(root.caller.stars[4])
MDIcon:
color: yellow
icon: "star"
disabled: not(root.caller.stars[5])
MDLabel:
text: f"{root.caller.episodes} Episodes"
halign:"right"
font_style:"Label"
role:"medium"
bold:True
pos_hint: {'center_y': 0.5}
adaptive_height:True
color: 0,0,0,.7
MDBoxLayout:
padding:"5dp"
pos_hint: {'bottom': 1}
adaptive_height:True
MDLabel:
text:root.caller.media_status
opacity:.8
halign:"left"
font_style:"Label"
role:"medium"
bold:True
pos_hint: {'center_y': .5}
adaptive_height:True
MDLabel:
text:root.caller.first_aired_on
opacity:.8
halign:"right"
font_style:"Label"
role:"medium"
bold:True
pos_hint: {'center_y': .5}
adaptive_height:True
MDBoxLayout:
orientation: 'vertical'
padding:"10dp"
spacing:"10dp"
adaptive_height:False
MDBoxLayout:
adaptive_height:True
MDBoxLayout:
adaptive_height:True
pos_hint: {'center_y': 0.5}
TooltipMDIconButton:
tooltip_text:root.caller.title
icon: "play-circle"
on_press: root.caller.is_play
MDIconButton:
icon: "plus-circle" if not(root.caller.is_in_my_list) else "check-circle"
on_release:
root.caller.is_in_my_list = not(root.caller.is_in_my_list)
self.icon = "plus-circle" if not(root.caller.is_in_my_list) else "check-circle"
MDIconButton:
icon: "bell-circle" if not(root.caller.is_in_my_notify) else "bell-check"
MDBoxLayout:
orientation: 'vertical'
pos_hint: {'center_y': 0.5}
adaptive_height:True
MDLabelShortened:
font_style:"Label"
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Genres: "+"[/color]"+root.caller.genres
markup:True
MDBoxLayout:
adaptive_height:True
MDLabelShortened:
font_style:"Label"
role:"small"
markup:True
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Popularity: "+"[/color]"+root.caller.popularity
MDLabelShortened:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Favourites: "+"[/color]"+root.caller.favourites
MDScrollView:
size_hint:1,1
do_scroll_y:True
MDLabel:
font_style:"Body"
role:"small"
text:root.caller.description
shorten:False
adaptive_height:True
# shorten_from:"right"
max_lines:0
MDBoxLayout:
orientation: 'vertical'
adaptive_height:True
MDLabelShortened:
font_style:"Label"
role:"small"
markup:True
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Author: "+"[/color]"+root.caller.author
MDLabelShortened:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Studios: "+"[/color]"+root.caller.studios
MDLabelShortened:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Characters: "+"[/color]"+root.caller.characters
MDLabelShortened:
font_style:"Label"
markup:True
role:"small"
text: f"[color={get_hex_from_color(self.theme_cls.primaryColor)}]"+"Tags: "+"[/color]"+root.caller.tags
<MediaCard>
spacing:"5dp"
image:"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx163270-oxwgbe43Cpog.jpg"
on_release:
self.open()
size_hint_x: None
width:dp(100)
# height:self.minimum_height
FitImage:
source:root.cover_image_url
fit_mode:"fill"
size_hint: None, None
width: dp(100)
height: dp(150)
MDDivider:
color:root.has_trailer_color
MDLabel:
font_style:"Label"
role:"medium"
text:root.title
max_lines:2
halign:"center"
color:self.theme_cls.secondaryColor
<MediaCardsContainer>
# adaptive_height:True
# size_hint_y:None
size_hint_x:1
# height:dp(300)
size_hint_y:None
height:max(self.minimum_height,dp(350),container.minimum_height)
# adaptive_height:True
# width:self.minimum_width
container:container
orientation: 'vertical'
padding:"10dp"
spacing:"5dp"
MDLabel:
text:root.list_name
MDScrollView:
# do_scroll_x:True
# do_scroll_y:False
size_hint:1,None
height:container.minimum_height
MDBoxLayout:
id:container
padding:"0dp","10dp","100dp","10dp"
size_hint:None,None
height:self.minimum_height
width:self.minimum_width
spacing:"10dp"

View File

@@ -0,0 +1,185 @@
import os
from kivy.clock import Clock
from kivy.uix.behaviors import ButtonBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.tooltip import MDTooltip
from kivymd.uix.behaviors import BackgroundColorBehavior,StencilBehavior,CommonElevationBehavior,HoverBehavior
from kivymd.uix.button import MDIconButton
from kivymd.theming import ThemableBehavior
from kivy.uix.modalview import ModalView
from kivy.properties import ObjectProperty,StringProperty,BooleanProperty,ListProperty,NumericProperty
from kivy.uix.video import Video
class Tooltip(MDTooltip):
pass
class TooltipMDIconButton(Tooltip,MDIconButton):
tooltip_text = StringProperty()
class MediaPopupVideoPlayer(Video):
# self.prev
pass
class MediaPopup(ThemableBehavior,HoverBehavior,StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior,ModalView):
caller = ObjectProperty()
player = ObjectProperty()
def __init__(self, caller,*args,**kwarg):
self.caller = caller
super(MediaPopup,self).__init__(*args,**kwarg)
def on_leave(self,*args):
def _leave(dt):
if not self.hovering:
self.dismiss()
Clock.schedule_once(_leave,2)
class MediaCard(ButtonBehavior,HoverBehavior,MDBoxLayout):
title = StringProperty()
is_play = ObjectProperty()
trailer_url = StringProperty()
episodes = StringProperty()
favourites = StringProperty()
popularity = StringProperty()
media_status = StringProperty("Releasing")
is_in_my_list = BooleanProperty(False)
is_in_my_notify = BooleanProperty(False)
genres = StringProperty()
first_aired_on = StringProperty()
description = StringProperty()
author = StringProperty()
studios = StringProperty()
characters = StringProperty()
tags = StringProperty()
stars = ListProperty([0,0,0,0,0,0])
cover_image_url = StringProperty()
preview_image = StringProperty()
# screen_name = StringProperty()
screen = ObjectProperty()
anime_id = NumericProperty()
has_trailer_color = ListProperty([1,1,1,0])
def __init__(self,trailer_url=None,**kwargs):
super().__init__(**kwargs)
self.orientation = "vertical"
if trailer_url:
self.trailer_url = trailer_url
self.adaptive_size = True
# self.app = MDApp.get_running_app()
# def on_screen_name(self,instance,value):
# if self.app:
# self.screen = self.app.manager_screens.get_screen(value)
def on_enter(self):
def _open_popup(dt):
if self.hovering:
window = self.get_parent_window()
for widget in window.children: # type: ignore
if isinstance(widget,MediaPopup):
return
self.open()
Clock.schedule_once(_open_popup,5)
def on_popup_open(self,popup:MediaPopup):
popup.center = self.center
def on_dismiss(self,popup:MediaPopup):
popup.player.unload()
def set_preview_image(self,image):
self.preview_image = image
def set_trailer_url(self,trailer_url):
self.trailer_url = trailer_url
self.has_trailer_color = self.theme_cls.primaryColor
def open(self,*_):
popup = MediaPopup(self)
popup.title = self.title
popup.bind(on_dismiss=self.on_dismiss,on_open=self.on_popup_open)
popup.open(self)
# ---------------respond to user actions and call appropriate model-------------------------
def on_is_in_my_list(self,instance,value):
if self.screen:
self.screen.controller.update_my_list(self.anime_id,value)
def on_trailer_url(self,*args):
pass
class MediaCardsContainer(MDBoxLayout):
container = ObjectProperty()
list_name = StringProperty()
# if __name__ == "__main__":
# from kivymd.app import MDApp
# from kivy.lang import Builder
# import json
# import os
# import tracemalloc
# tracemalloc.start()
# data = {}
# with open(os.path.join(os.curdir,"View","components","media_card","data.json"),"r") as file:
# data = json.loads(file.read())
# cache = {}
# def fetch_data(key):
# yt = YouTube(key)
# preview_image = yt.thumbnail_url
# video_stream_url = yt.streams.filter(progressive=True,file_extension="mp4")[-1].url
# return preview_image,video_stream_url
# def cached_fetch_data(key):
# if key not in cache:
# cache[key] = fetch_data(key)
# return cache[key]
# class MediaCardApp(MDApp):
# def build(self):
# self.theme_cls.primary_palette = "Magenta"
# self.theme_cls.theme_style = "Dark"
# ui = Builder.load_file("./media_card.kv")
# for item in data["data"]["Page"]["media"]:
# media_card = MediaCard()
# if item["title"]["english"]:
# media_card.title = item["title"]["english"]
# else:
# media_card.title = item["title"]["romaji"]
# media_card.cover_image_url = item["coverImage"]["medium"]
# media_card.popularity = str(item["popularity"])
# media_card.favourites = str(item["favourites"])
# media_card.episodes = str(item["episodes"])
# media_card.description = item["description"]
# media_card.first_aired_on = str(item["startDate"])
# media_card.studios = str(item["studios"]["nodes"])
# media_card.tags = str(item["tags"])
# media_card.media_status = item["status"]
# if item["trailer"]:
# try:
# url = cached_fetch_data("https://youtube.com/watch?v="+item["trailer"]["id"])[1]
# media_card.trailer_url =url
# except:
# pass
# media_card.genres = ",".join(item["genres"])
# stars = int(item["averageScore"]/100*6)
# if stars:
# for i in range(stars):
# media_card.stars[i] = 1
# ui.ids.cards.add_widget(media_card) # type: ignore
# return ui
# MediaCardApp().run()
# snapshot = tracemalloc.take_snapshot()
# print("-----------------------------------------------")
# for stat in snapshot.statistics("lineno")[:10]:
# print(stat)

21
app/View/screens.py Normal file
View File

@@ -0,0 +1,21 @@
# The screens dictionary contains the objects of the models and controllers
# of the screens of the application.
from Controller import SearchScreenController,MainScreenController,MyListScreenController
from Model import MainScreenModel,SearchScreenModel,MyListScreenModel
screens = {
"main screen": {
"model": MainScreenModel,
"controller": MainScreenController,
},
"search screen": {
"model": SearchScreenModel,
"controller": SearchScreenController,
},
"my list screen": {
"model": MyListScreenModel,
"controller": MyListScreenController,
},
}