class BookInfoScreen(MDScreen): """Contains layout with detailed information about the choosen book. Also containstoolbar with buttons for getting back to the books list and deleting the choosen book from the list. """ def __init__(self, **kwargs): super().__init__(**kwargs) scroll_view = ScrollView() self.layout = MDBoxLayout(orientation="vertical", size_hint=(1, 1.2)) scroll_view.add_widget(self.layout) self.add_widget(scroll_view) def load_screen(self, book): """Loads all elements for the detailed info. Created as separate method because on every book we need to reload the information. """ self.book = book toolbar = MDToolbar(type="top") toolbar.left_action_items = [["arrow-left", self.go_back]] toolbar.right_action_items = [["delete", self.delete_item]] content = BookInfoContent(book) self.layout.add_widget(toolbar) self.layout.add_widget(content) def go_back(self, touch): self.layout.clear_widgets() self.manager.transition.direction = "right" self.manager.switch_to(BooksTab.screens["books_list"]) def delete_item(self, touch): BooksTab.screens["books_list"].books.remove(self.book) books_list = BooksTab.screens["books_list"].books BooksTab.screens["books_list"].load_books_list(books_list) self.go_back(touch)
class Main(MDApp): def __init__(self): super(Main, self).__init__() self.theme_cls.primary_palette = "Amber" self.title = "Four in a row" self.root = Builder.load_file("style.kv") self.current_widget = MDBoxLayout() self.start_widget = StartWidget(self.start_game, self.change_theme) self.theme_widget = ThemeWidget(self, self.return_back) self.current_widget.add_widget(self.start_widget) def change_theme(self): self.current_widget.clear_widgets() self.current_widget.add_widget(self.theme_widget) def start_game(self): self.current_widget.clear_widgets() self.current_widget.add_widget(GameWidget(self.return_back)) def return_back(self): self.current_widget.clear_widgets() self.current_widget.add_widget(self.start_widget) def build(self): return self.current_widget
class BookAdderScreen(MDScreen): """Contains layout with functionality for adding new books to the list. Also contains toolbar with buttons for getting back to the books list and saving new book. """ def __init__(self, **kwargs): super().__init__(**kwargs) self.layout = MDBoxLayout(orientation="vertical") self.load_content() self.add_widget(self.layout) def load_content(self): self.layout.clear_widgets() toolbar = MDToolbar(type="top") toolbar.left_action_items = [["arrow-left", self.go_back]] toolbar.right_action_items = [["plus", self.add_book]] title_label = MDLabel( text="Title: ", halign="left", valign="top", ) subtitle_label = MDLabel( text="Subtitle: ", halign="left", valign="top", ) price_label = MDLabel( text="Price: ", halign="left", valign="top", ) self.title_input = MDTextField() self.subtitle_input = MDTextField() self.price_input = MDTextField() self.layout.add_widget(toolbar) self.layout.add_widget(title_label) self.layout.add_widget(self.title_input) self.layout.add_widget(subtitle_label) self.layout.add_widget(self.subtitle_input) self.layout.add_widget(price_label) self.layout.add_widget(self.price_input) def go_back(self, touch): self.layout.clear_widgets() self.manager.transition.direction = "right" self.manager.switch_to(BooksTab.screens["books_list"]) def add_book(self, touch): book = Book(title=self.title_input.text, subtitle=self.subtitle_input.text, price=self.price_input.text) BooksTab.screens["books_list"].books.append(book) books_list = BooksTab.screens["books_list"].books BooksTab.screens["books_list"].load_books_list(books_list) self.go_back(touch)
class DiscoveryMixin(): def makeDiscoveryPage(self): # Discovery Page screen = Screen(name='Discovery') self.discoveryScreen = screen layout = BoxLayout(orientation='vertical', spacing=10) screen.add_widget(layout) label = Label( size_hint=(1, None), halign="center", text= 'Browsing your local network.\nWarning: anyone on your network\ncan advertise a site with any title they want.' ) layout.add_widget(self.makeBackButton()) layout.add_widget(label) self.discoveryScroll = ScrollView(size_hint=(1, 1)) self.discoveryListbox = BoxLayout(orientation='vertical', size_hint=(1, None)) self.discoveryListbox.bind( minimum_height=self.discoveryListbox.setter('height')) self.discoveryScroll.add_widget(self.discoveryListbox) layout.add_widget(self.discoveryScroll) return screen def goToDiscovery(self, *a): "Go to the local network discovery page" self.discoveryListbox.clear_widgets() try: hardline.discoveryPeer.search('', n=1) time.sleep(2) for i in hardline.getAllDiscoveries(): info = i self.discoveryListbox.add_widget( MDToolbar(title=str(info.get('title', 'no title')))) l = StackLayout(adaptive_size=True, spacing=8, size_hint=(1, None)) #Need to capture that info in the closures def scope(info): btn = Button(text="Open in Browser") def f(*a): self.openInBrowser("http://" + info['hash'] + ".localhost:7009") btn.bind(on_press=f) l.add_widget(btn) btn = Button(text="Copy URL") def f(*a): try: from kivy.core.clipboard import Clipboard Clipboard.copy("http://" + info['hash'] + ".localhost:7009") except: logging.exception("Could not copy to clipboard") btn.bind(on_press=f) self.localServicesListBox.add_widget( MDToolbar(title=str(info.get('title', 'no title')))) l.add_widget(btn) scope(info) self.discoveryListbox.add_widget(l) lb = self.saneLabel("Hosted By: " + info.get("from_ip", ""), self.discoveryListbox) self.discoveryListbox.add_widget(lb) lb = self.saneLabel("ID: " + info['hash'], self.discoveryListbox) self.discoveryListbox.add_widget(lb) except Exception: logging.info(traceback.format_exc()) self.screenManager.current = "Discovery"
class ToolsAndSettingsMixin(): def goToSettings(self, *a): self.screenManager.current = "Settings" def goToGlobalSettings(self, *a): if os.path.exists(hardline.globalSettingsPath): with open(hardline.globalSettingsPath) as f: globalConfig = toml.load(f) else: globalConfig = {} self.localSettingsBox.clear_widgets() self.localSettingsBox.add_widget( Label(size_hint=(1, 6), halign="center", text='OpenDHT Proxies')) self.localSettingsBox.add_widget( Label(size_hint=(1, None), text='Proxies are tried in order from 1-3')) self.localSettingsBox.add_widget( self.settingButton(globalConfig, "DHTProxy", 'server1')) self.localSettingsBox.add_widget( self.settingButton(globalConfig, "DHTProxy", 'server2')) self.localSettingsBox.add_widget( self.settingButton(globalConfig, "DHTProxy", 'server3')) self.localSettingsBox.add_widget( Label(size_hint=(1, 6), halign="center", text='Stream Server')) self.localSettingsBox.add_widget( Label( size_hint=(1, None), text= 'To allow others to sync to this node as a DrayerDB Stream server, set a server title to expose a service' )) self.localSettingsBox.add_widget( self.settingButton(globalConfig, "DrayerDB", 'serverName')) btn1 = Button(text='Save') def save(*a): with open(hardline.globalSettingsPath, 'w') as f: toml.dump(globalConfig, f) if platform == 'android': self.stop_service() self.start_service() else: daemonconfig.loadDrayerServerConfig() self.screenManager.current = "Main" btn1.bind(on_press=save) self.localSettingsBox.add_widget(btn1) self.screenManager.current = "GlobalSettings" def makeGlobalSettingsPage(self): screen = Screen(name='GlobalSettings') layout = BoxLayout(orientation='vertical', spacing=10) screen.add_widget(layout) layout.add_widget(self.makeBackButton()) self.localSettingsScroll = ScrollView(size_hint=(1, 1)) self.localSettingsBox = BoxLayout(orientation='vertical', size_hint=(1, None), spacing=10) self.localSettingsBox.bind( minimum_height=self.localSettingsBox.setter('height')) self.localSettingsScroll.add_widget(self.localSettingsBox) layout.add_widget(self.localSettingsScroll) return screen def makeSettingsPage(self): page = Screen(name='Settings') layout = BoxLayout(orientation='vertical') page.add_widget(layout) label = MDToolbar(title="Settings and Tools") layout.add_widget(label) layout.add_widget(self.makeBackButton()) log = Button(text='System Logs') btn1 = Button(text='Local Services') label1 = Label(halign="center", text='Share a local webservice with the world') log.bind(on_release=self.gotoLogs) btn1.bind(on_press=self.goToLocalServices) layout.add_widget(log) layout.add_widget(btn1) layout.add_widget(label1) btn = Button(text='Global Settings') btn.bind(on_press=self.goToGlobalSettings) layout.add_widget(btn) # Start/Stop btn3 = Button(text='Stop') btn3.bind(on_press=self.stop_service) label3 = Label( size_hint=(1, None), halign="center", text= 'Stop the background process. It must be running to acess hardline sites. Starting may take a few seconds.' ) layout.add_widget(btn3) layout.add_widget(label3) btn4 = Button(text='Start or Restart.') btn4.bind(on_press=self.start_service) label4 = Label( size_hint=(1, None), halign="center", text='Restart the process. It will show in your notifications.') layout.add_widget(btn4) layout.add_widget(label4) layout.add_widget(Widget()) return page def makeLogsPage(self): screen = Screen(name='Logs') self.servicesScreen = screen layout = BoxLayout(orientation='vertical', spacing=10) screen.add_widget(layout) layout.add_widget(MDToolbar(title="System Logs")) layout.add_widget(self.makeBackButton()) self.logsListBoxScroll = ScrollView(size_hint=(1, 1)) self.logsListBox = BoxLayout(orientation='vertical', size_hint=(1, None), spacing=10) self.logsListBox.bind(minimum_height=self.logsListBox.setter('height')) self.logsListBoxScroll.add_widget(self.logsListBox) layout.add_widget(self.logsListBoxScroll) return screen def gotoLogs(self, *a): self.logsListBox.clear_widgets() try: from kivy.logger import LoggerHistory for i in LoggerHistory.history: self.logsListBox.add_widget( MDTextField(text=str(i.getMessage()), multiline=True, size_hint=(1, None), mode="rectangle")) self.screenManager.current = "Logs" except Exception as e: logging.info(traceback.format_exc())
class StreamsMixin(): #Reuse the same panel for editStream, the main hub for accessing the stream, #and it's core settings def editStream(self, name): if not name in daemonconfig.userDatabases: self.goToStreams() db = daemonconfig.userDatabases[name] c = db.config try: c.add_section("Service") except: pass try: c.add_section("Info") except: pass self.streamEditPanel.clear_widgets() topbar = BoxLayout(size_hint=(1, None), adaptive_height=True, spacing=5) stack = StackLayout(size_hint=(1, None), adaptive_height=True, spacing=5) def upOne(*a): self.goToStreams() btn1 = Button(text='Up') btn1.bind(on_press=upOne) topbar.add_widget(btn1) topbar.add_widget(self.makeBackButton()) self.streamEditPanel.add_widget(topbar) self.streamEditPanel.add_widget(MDToolbar(title=name)) def goHere(): self.editStream(name) self.backStack.append(goHere) self.backStack = self.backStack[-50:] btn2 = Button(text='Notebook View') def goPosts(*a): self.gotoStreamPosts(name) btn2.bind(on_press=goPosts) stack.add_widget(btn2) btn2 = Button(text='Feed View') def goPosts(*a): self.gotoStreamPosts(name, parent=None) btn2.bind(on_press=goPosts) stack.add_widget(btn2) btn2 = Button(text='Stream Settings') def goSettings(*a): self.editStreamSettings(name) btn2.bind(on_press=goSettings) stack.add_widget(btn2) if name.startswith('file:'): btn2 = Button(text='Close Stream') def close(*a): daemonconfig.closeUserDatabase(name) self.goToStreams() btn2.bind(on_press=close) stack.add_widget(btn2) importData = Button(text="Import Data File") def promptSet(*a): try: #Needed for android self.getPermission('files') except: logging.exception("cant ask permission") def f(selection): if selection: def f2(x): if x: with daemonconfig.userDatabases[name]: with open(selection) as f: daemonconfig.userDatabases[ name].importFromToml(f.read()) daemonconfig.userDatabases[name].commit() self.askQuestion("Really import?", "yes", cb=f2) self.openFM.close() from .kivymdfmfork import MDFileManager from . import directories self.openFM = MDFileManager(select_path=f) if os.path.exists("/storage/emulated/0/Documents") and os.access( "/storage/emulated/0/Documents", os.W_OK): self.openFM.show("/storage/emulated/0/Documents") elif os.path.exists( os.path.expanduser("~/Documents")) and os.access( os.path.expanduser("~/Documents"), os.W_OK): self.openFM.show(os.path.expanduser("~/Documents")) else: self.openFM.show(directories.externalStorageDir or directories.settings_path) importData.bind(on_release=promptSet) stack.add_widget(importData) export = Button(text="Export All Posts") def promptSet(*a): from .kivymdfmfork import MDFileManager from .. import directories try: #Needed for android self.getPermission('files') except: logging.exception("cant ask permission") def f(selection): if selection: if not selection.endswith(".toml"): selection = selection + ".toml" def g(a): if a == 'yes': r = daemonconfig.userDatabases[ name].getDocumentsByType('post', parent='') data = daemonconfig.userDatabases[ name].exportRecordSetToTOML( [i['id'] for i in r]) logging.info("Exporting data to:" + selection) with open(selection, 'w') as f: f.write(data) self.openFM.close() if os.path.exists(selection): self.askQuestion("Overwrite?", 'yes', g) else: g('yes') #Autocorrect had some fun with the kivymd devs self.openFM = MDFileManager(select_path=f, save_mode=(name + '.toml')) if os.path.exists("/storage/emulated/0/Documents") and os.access( "/storage/emulated/0/Documents", os.W_OK): self.openFM.show("/storage/emulated/0/Documents") elif os.path.exists( os.path.expanduser("~/Documents")) and os.access( os.path.expanduser("~/Documents"), os.W_OK): self.openFM.show(os.path.expanduser("~/Documents")) else: self.openFM.show(directories.externalStorageDir or directories.settings_path) export.bind(on_release=promptSet) stack.add_widget(export) self.streamEditPanel.add_widget(stack) #Show recent changes no matter where they are in the tree. #TODO needs to be hideable for anti-spoiler purposes in fiction. self.streamEditPanel.add_widget(MDToolbar(title="Recent Changes:")) for i in daemonconfig.userDatabases[name].getDocumentsByType( 'post', orderBy='arrival DESC', limit=5): x = self.makePostWidget(name, i, includeParent=True) self.streamEditPanel.add_widget(x) self.screenManager.current = "EditStream" def showSharingCode(self, name, c, wp=True): if daemonconfig.ddbservice[0]: try: localServer = daemonconfig.ddbservice[0].getSharableURL() except: logging.exception("wtf") else: localServer = '' d = { 'sv': c['Sync'].get('server', '') or localServer, 'vk': c['Sync'].get("syncKey", ''), 'n': name[:24] } if wp: d['sk'] = c['Sync'].get('writePassword', '') else: d['sk'] = '' import json d = json.dumps(d, indent=0, separators=(',', ':')) if wp: self.showQR(d, "Stream Code(full access)") else: self.showQR(d, "Stream Code(readonly)") def editStreamSettings(self, name): db = daemonconfig.userDatabases[name] c = db.config self.streamEditPanel.clear_widgets() self.streamEditPanel.add_widget( Label(size_hint=(1, None), halign="center", text=name)) self.streamEditPanel.add_widget( Label(size_hint=(1, None), halign="center", text="file:" + db.filename)) self.streamEditPanel.add_widget(self.makeBackButton()) def save(*a): logging.info("SAVE BUTTON WAS PRESSED") # On android this is the bg service's job db.saveConfig() if platform == 'android': self.stop_service() self.start_service() else: db.close() daemonconfig.loadUserDatabases( None, only=name, callbackFunction=self.onDrayerRecordChange) def delete(*a): def f(n): if n and n == name: daemonconfig.delDatabase(None, n) if platform == 'android': self.stop_service() self.start_service() self.goToStreams() self.askQuestion("Really delete?", name, f) self.streamEditPanel.add_widget( Label(size_hint=(1, None), halign="center", font_size="24sp", text='Sync')) self.streamEditPanel.add_widget( keyBox := self.settingButton(c, "Sync", "syncKey")) self.streamEditPanel.add_widget( pBox := self.settingButton(c, "Sync", "writePassword")) self.streamEditPanel.add_widget( Label( size_hint=(1, None), halign="center", font_size="12sp", text= 'Keys have a special format, you must use the generator to change them.' )) def promptNewKeys(*a, **k): def makeKeys(a): if a == 'yes': import base64 vk, sk = libnacl.crypto_sign_keypair() vk = base64.b64encode(vk).decode() sk = base64.b64encode(sk).decode() keyBox.text = vk pBox.text = sk self.askQuestion("Overwrite with random keys?", 'yes', makeKeys) keyButton = Button(text='Generate New Keys') keyButton.bind(on_press=promptNewKeys) self.streamEditPanel.add_widget(keyButton) self.streamEditPanel.add_widget( serverBox := self.settingButton(c, "Sync", "server")) self.streamEditPanel.add_widget( Label(size_hint=(1, None), halign="center", text='Do not include the http:// ')) self.streamEditPanel.add_widget( self.settingButton(c, "Sync", "serve", 'yes')) self.streamEditPanel.add_widget( Label(size_hint=(1, None), halign="center", text='Set serve=no to forbid clients to sync')) self.streamEditPanel.add_widget( Label(size_hint=(1, None), halign="center", font_size="24sp", text='Application')) self.streamEditPanel.add_widget( self.settingButton(c, "Application", "notifications", 'no')) def f(*a): def g(a): try: import json a = json.loads(a) serverBox.text = c['Sync'][ 'server'] = a['sv'] or c['Sync']['server'] keyBox.text = c['Sync']['syncKey'] = a['vk'] pBox.text = c['Sync']['writePassword'] = a['sk'] except: pass self.askQuestion("Enter Sharing Code", cb=g, multiline=True) keyButton = Button(text='Load from Code') keyButton.bind(on_press=f) self.streamEditPanel.add_widget(keyButton) def f(*a): self.showSharingCode(name, c) keyButton = Button(text='Show Sharing Code') keyButton.bind(on_press=f) self.streamEditPanel.add_widget(keyButton) def f(*a): self.showSharingCode(name, c, wp=False) keyButton = Button(text='Readonly Sharing Code') keyButton.bind(on_press=f) self.streamEditPanel.add_widget(keyButton) btn1 = Button(text='Save Changes') btn1.bind(on_press=save) self.streamEditPanel.add_widget(btn1) btn2 = Button(text='Delete this stream') btn2.bind(on_press=delete) self.streamEditPanel.add_widget(btn2) def gotoOrphans(*a, **k): self.gotoStreamPosts(name, orphansMode=True) oButton = Button(text='Show Unreachable Garbage') oButton.bind(on_press=gotoOrphans) self.streamEditPanel.add_widget(oButton) noSpreadsheet = Button(text="Spreadsheet on/off") def promptSet(*a): from .kivymdfmfork import MDFileManager from .. import directories try: #Needed for android self.getPermission('files') except: logging.exception("cant ask permission") def f(selection): if selection == 'on': daemonconfig.userDatabases[ name].enableSpreadsheetEval = True else: daemonconfig.userDatabases[ name].enableSpreadsheetEval = False if hasattr(daemonconfig.userDatabases[name], 'enableSpreadsheetEval'): esf = daemonconfig.userDatabases[name].enableSpreadsheetEval else: esf = True self.askQuestion("Allow Spreadsheet Functions?", 'on' if esf else 'off', f) noSpreadsheet.bind(on_release=promptSet) self.streamEditPanel.add_widget(noSpreadsheet) self.streamEditPanel.add_widget( self.saneLabel( "Disabling only takes effect for this session. Use this feature if a stream is loading too slowly, to allow you to fix the offending expression.", self.streamEditPanel)) self.screenManager.current = "EditStream" def makeStreamsPage(self): "Prettu much just an empty page filled in by the specific goto functions" screen = Screen(name='Streams') self.servicesScreen = screen self.streamsEditPanelScroll = ScrollView(size_hint=(1, 1)) self.streamsEditPanel = BoxLayout(orientation='vertical', adaptive_height=True, spacing=5, size_hint=(1, None)) self.streamsEditPanel.bind( minimum_height=self.streamsEditPanel.setter('height')) self.streamsEditPanelScroll.add_widget(self.streamsEditPanel) screen.add_widget(self.streamsEditPanelScroll) return screen def goToStreams(self, *a): "Go to a page wherein we can list user-modifiable services." self.streamsEditPanel.clear_widgets() layout = self.streamsEditPanel bar = BoxLayout(spacing=10, adaptive_height=True, size_hint=(1, None)) stack = StackLayout(spacing=10, adaptive_height=True, size_hint=(1, None)) layout.add_widget(bar) layout.add_widget(MDToolbar(title="My Streams")) layout.add_widget(stack) def upOne(*a): self.gotoMainScreen() btn1 = Button(text='Up') btn1.bind(on_press=upOne) bar.add_widget(btn1) bar.add_widget(self.makeBackButton()) btn2 = Button(text='Create a Stream') btn2.bind(on_press=self.promptAddStream) stack.add_widget(btn2) def f(selection): if selection: dn = 'file:' + os.path.basename(selection) while dn in daemonconfig.userDatabases: dn = dn + '2' try: daemonconfig.loadUserDatabase(selection, dn) self.editStream(dn) except: logging.exception(dn) self.openFM.close() #This lets us view notebook files that aren't installed. def promptOpen(*a): try: #Needed for android self.getPermission('files') except: logging.exception("cant ask permission") from .kivymdfmfork import MDFileManager from . import directories self.openFM = MDFileManager(select_path=f) if os.path.exists("/storage/emulated/0/Documents") and os.access( "/storage/emulated/0/Documents", os.W_OK): self.openFM.show("/storage/emulated/0/Documents") elif os.path.exists( os.path.expanduser("~/Documents")) and os.access( os.path.expanduser("~/Documents"), os.W_OK): self.openFM.show(os.path.expanduser("~/Documents")) else: self.openFM.show(directories.externalStorageDir or directories.settings_path) btn1 = Button(text='Open Book File') btn1.bind(on_press=promptOpen) stack.add_widget(btn1) def goHere(): self.screenManager.current = "Streams" self.backStack.append(goHere) self.backStack = self.backStack[-50:] layout.add_widget(MDToolbar(title="Open Streams:")) try: s = daemonconfig.userDatabases time.sleep(0.5) for i in s: layout.add_widget(self.makeButtonForStream(i)) try: for j in daemonconfig.userDatabases[i].connectedServers: if daemonconfig.userDatabases[i].connectedServers[ j] > (time.time() - (10 * 60)): w = 'online' else: w = 'idle/offline' layout.add_widget( self.saneLabel(j[:28] + ": " + w, layout)) except: logging.exception("Error showing node status") except Exception: logging.info(traceback.format_exc()) self.screenManager.current = "Streams" def makeButtonForStream(self, name): "Make a button that, when pressed, edits the stream in the title" btn = Button(text=name) def f(*a): self.editStream(name) btn.bind(on_press=f) return btn def promptAddStream(self, *a, **k): def f(v): if v: daemonconfig.makeUserDatabase(None, v) self.editStream(v) self.askQuestion("New Stream Name?", cb=f) def makeStreamEditPage(self): "Prettu much just an empty page filled in by the specific goto functions" screen = Screen(name='EditStream') self.servicesScreen = screen self.streamEditPanelScroll = ScrollView(size_hint=(1, 1)) self.streamEditPanel = BoxLayout(orientation='vertical', adaptive_height=True, spacing=5, size_hint=(1, None)) self.streamEditPanel.bind( minimum_height=self.streamEditPanel.setter('height')) self.streamEditPanelScroll.add_widget(self.streamEditPanel) screen.add_widget(self.streamEditPanelScroll) return screen
class ServiceApp(MDApp, uihelpers.AppHelpersMixin, tools.ToolsAndSettingsMixin, servicesUI.ServicesMixin, discovery.DiscoveryMixin, tables.TablesMixin, posts.PostsMixin, streams.StreamsMixin): def stop_service(self, foo=None): if self.service: self.service.stop() self.service = None else: hardline.stop() def on_location(self, **kwargs): Logger.info("Called on_location") Logger.info(kwargs) self.location = kwargs def onDrayerRecordChange(self,db,record,sig): if self.currentPageNewRecordHandler: self.currentPageNewRecordHandler(db,record,sig) #Only deleting or changing as data row can affect this if record['type'] in ('null','row'): self.clearSpreadsheetCache() def start_service(self, foo=None): try: self.service.stop() self.service = None except: logging.exception("Likely no need to stop nonexistent service") try: self.getPermission('location') from plyer import gps gps.configure( on_location=self.on_location ) gps.start() except: logging.exception("Could not start location service") if platform == 'android': from android import AndroidService logging.info("About to start Android service") service = AndroidService('HardlineP2P Service', 'running') service.start('service started') self.service = service # On android the service that will actually be handling these databases is in the background in a totally separate # process. So we open an SECOND drayer database object for each, with the same physical storage, using the first as the server. # just for use in the foreground app. # Because of this, two connections to the same DB file is a completetely supported use case that drayerDB has optimizations for. daemonconfig.loadUserDatabases(None, forceProxy='localhost:7004',callbackFunction=self.onDrayerRecordChange) else: def f(): # Ensure stopped hardline.stop() loadedServices = daemonconfig.loadUserServices( None) daemonconfig.loadDrayerServerConfig() self.currentPageNewRecordHandler=None db = daemonconfig.loadUserDatabases( None,callbackFunction=self.onDrayerRecordChange) hardline.start(7009) # Unload them at exit because we will be loading them again on restart for i in loadedServices: loadedServices[i].close() t = threading.Thread(target=f, daemon=True) t.start() def build(self): self.service = None self.start_service() # Create the manager sm = ScreenManager() self.currentPageNewRecordHandler=None sm.add_widget(self.makeMainScreen()) sm.add_widget(self.makeDiscoveryPage()) sm.add_widget(self.makeSettingsPage()) sm.add_widget(self.makeLocalServiceEditPage()) sm.add_widget(self.makeLocalServicesPage()) sm.add_widget(self.makeGlobalSettingsPage()) sm.add_widget(self.makeStreamsPage()) sm.add_widget(self.makeStreamEditPage()) sm.add_widget(self.makeLogsPage()) sm.add_widget(self.makePostMetaDataPage()) sm.add_widget(self.makeQuestionPage()) from kivy.base import EventLoop EventLoop.window.bind(on_keyboard=self.hook_keyboard) import kivymd self.theme_cls.colors=kivymd.color_definitions.colors import kivy.clock kivy.clock.Clock.max_iteration = 10 #Horid hacks for material design self.theme_cls.colors['Brown']['900']='050200' self.theme_cls.colors['Green']['600']='43694e' self.theme_cls.colors['Light']['Background']='E3DFDA' self.theme_cls.primary_palette = "Green" self.theme_cls.theme_style = "Light" self.theme_cls.primary_hue='600' self.theme_cls.accent_hue='900' self.theme_cls.accent_palette='Brown' self.backStack = [] # Call this to save whatever unsaved data. Also acts as a flag. self.unsavedDataCallback = None self.screenManager = sm Clock.schedule_interval(self.flushUnsaved, 60*5) self.gotoMainScreen() return sm # Here is our autosave def on_pause(self): self.flushUnsaved() return True def on_stop(self): self.flushUnsaved() def on_destroy(self): self.flushUnsaved() def flushUnsaved(self, *a): if self.unsavedDataCallback: self.unsavedDataCallback() self.unsavedDataCallback = None def makeMainScreen(self): mainScreen = Screen(name='Main') mainscroll = ScrollView(size_hint=(1, 1)) self.mainScreenlayout = BoxLayout(orientation='vertical', spacing=10, size_hint=(1, 1),adaptive_height=True) mainscroll.add_widget(self.mainScreenlayout) mainScreen.add_widget(mainscroll) return mainScreen def gotoMainScreen(self): self.mainScreenlayout.clear_widgets() layout=self.mainScreenlayout label = MDToolbar(title="Drayer Journal") label.icon=os.path.join(os.path.dirname(os.path.abspath("__file__")),'assets','icons',"Craftpix.net",'medival','cart.jpg') layout.add_widget(label) stack = StackLayout(size_hint=(1,None),adaptive_height=True,spacing=5) l = self.saneLabel("Notice: streams may be stored on the SD card. Some other apps may be able to read them",layout) layout.add_widget(l) btn1 = Button(text='My Streams') stack.add_widget(btn1) btn1.bind(on_press=self.goToStreams) btn1 = Button(text='Discover Services') btn1.bind(on_press=self.goToDiscovery) stack.add_widget(btn1) btn5 = Button(text='Settings+Tools') btn5.bind(on_press=self.goToSettings) stack.add_widget(btn5) btn6 = Button(text='Help') btn6.bind(on_press=self.goToHelp) stack.add_widget(btn6) layout.add_widget(stack) label = MDToolbar(title="Bookmarks") layout.add_widget(label) for i in sorted(list(daemonconfig.getBookmarks().keys())): bw =BoxLayout(orientation='horizontal', spacing=10, size_hint=(1, None),adaptive_height=True) b = Button(text=i[:32]) bd = Button(text="Del") def dlbm(*a,i=i): def f(a): if a: daemonconfig.setBookmark(a,None,None) self.gotoMainScreen() self.askQuestion("Delete Bookmark?",i,f) bd.bind(on_press=dlbm) def bm(*a,i=i): self.gotoBookmark(i) b.bind(on_press=bm) bw.add_widget(b) bw.add_widget(bd) layout.add_widget(bw) self.screenManager.current = "Main" def goBack(self,*a): def f(d): try: self.openFM.close() except: pass if d: self.currentPageNewRecordHandler=None self.unsavedDataCallback = False # Get rid of the first one representing the current page if self.backStack: self.backStack.pop() # Go to the previous page, if that page left an instruction for how to get back to it if self.backStack: self.backStack.pop()() else: self.gotoMainScreen() # If they have an unsaved post, ask them if they really want to leave. if self.unsavedDataCallback: self.askQuestion("Discard unsaved data?", 'yes', cb=f) else: f(True) def makeBackButton(self,width=1): btn1 = Button(text='Back') btn1.bind(on_press=self.goBack) return btn1 def hook_keyboard(self, window, key, *largs): if key == 27: self.goBack() return True def goToHelp(self,*a): dn = "builtin:help" if not dn in daemonconfig.userDatabases: daemonconfig.loadUserDatabase(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),'Drayer Documentation.toml'),dn) self.editStream(dn) def getPermission(self, type='all'): """ Since API 23, Android requires permission to be requested at runtime. This function requests permission and handles the response via a callback. The request will produce a popup if permissions have not already been been granted, otherwise it will do nothing. """ if platform == "android": from android.permissions import request_permissions, Permission if type == 'all': plist = [Permission.ACCESS_COARSE_LOCATION, Permission.ACCESS_FINE_LOCATION, Permission.MANAGE_EXTERNAL_STORAGE] if type == 'location': plist = [Permission.ACCESS_COARSE_LOCATION, Permission.ACCESS_FINE_LOCATION] if type == 'files': plist = [Permission.MANAGE_EXTERNAL_STORAGE] def callback(permissions, results): """ Defines the callback to be fired when runtime permission has been granted or denied. This is not strictly required, but added for the sake of completeness. """ if all([res for res in results]): print("callback. All permissions granted.") else: print("callback. Some permissions refused.") request_permissions(plist, callback)
class ServicesMixin(): def makeLocalServiceEditPage(self): screen = Screen(name='EditLocalService') self.servicesScreen = screen layout = BoxLayout(orientation='vertical', spacing=10) screen.add_widget(layout) self.localServiceEditorName = Label(size_hint=(1, None), halign="center", text="??????????") layout.add_widget(self.makeBackButton()) self.localServiceEditPanelScroll = ScrollView(size_hint=(1, 1)) self.localServiceEditPanel = BoxLayout(orientation='vertical', size_hint=(1, None)) self.localServiceEditPanel.bind( minimum_height=self.localServiceEditPanel.setter('height')) self.localServiceEditPanelScroll.add_widget(self.localServiceEditPanel) layout.add_widget(self.localServiceEditPanelScroll) return screen def editLocalService(self, name, c=None): if not c: c = configparser.RawConfigParser( dict_type=cidict.CaseInsensitiveDict) try: c.add_section("Service") except: pass try: c.add_section("Info") except: pass self.localServiceEditPanel.clear_widgets() self.localServiceEditorName.text = name def save(*a): logging.info("SAVE BUTTON WAS PRESSED") # On android this is the bg service's job daemonconfig.makeUserService( None, name, title=c['Info'].get("title", 'Untitled'), service=c['Service'].get("service", ""), port=c['Service'].get("port", ""), cacheInfo=c['Cache'], noStart=(platform == 'android'), useDHT=c['Access'].get("useDHT", "yes")) if platform == 'android': self.stop_service() self.start_service() self.goToLocalServices() def delete(*a): def f(n): if n and n == name: daemonconfig.delUserService(None, n) if platform == 'android': self.stop_service() self.start_service() self.goToLocalServices() self.askQuestion("Really delete?", name, f) self.localServiceEditPanel.add_widget( Label(size_hint=(1, None), halign="center", text='Service')) self.localServiceEditPanel.add_widget( self.settingButton(c, "Service", "service")) self.localServiceEditPanel.add_widget( self.settingButton(c, "Service", "port")) self.localServiceEditPanel.add_widget( self.settingButton(c, "Info", "title")) self.localServiceEditPanel.add_widget( Label(size_hint=(1, None), halign="center", text='Cache')) self.localServiceEditPanel.add_widget( Label(size_hint=(1, None), text='Cache mode only works for static content')) self.localServiceEditPanel.add_widget( self.settingButton(c, "Cache", "directory")) self.localServiceEditPanel.add_widget( self.settingButton(c, "Cache", "maxAge")) self.localServiceEditPanel.add_widget( Label(size_hint=(1, None), text='Try to refresh after maxAge seconds(default 1 week)')) self.localServiceEditPanel.add_widget( self.settingButton(c, "Cache", "maxSize", '256')) self.localServiceEditPanel.add_widget( Label(size_hint=(1, None), text='Max size to use for the cache in MB')) self.localServiceEditPanel.add_widget( self.settingButton(c, "Cache", "downloadRateLimit", '1200')) self.localServiceEditPanel.add_widget( Label(size_hint=(1, None), text='Max MB per hour to download')) self.localServiceEditPanel.add_widget( self.settingButton(c, "Cache", "dynamicContent", 'no')) self.localServiceEditPanel.add_widget( Label( size_hint=(1, None), text= 'Allow executing code in protected @mako files in the cache dir. yes to enable. Do not use with untrusted @mako' )) self.localServiceEditPanel.add_widget( self.settingButton(c, "Cache", "allowListing", 'no')) self.localServiceEditPanel.add_widget( Label(size_hint=(1, None), text='Allow directory listing of cached content')) self.localServiceEditPanel.add_widget( Label( size_hint=(1, None), text= 'Directory names are subfolders within the HardlineP2P cache folder,\nand can also be used to share\nstatic files by leaving the service blank.' )) self.localServiceEditPanel.add_widget( Label(size_hint=(1, None), halign="center", text='Access Settings')) self.localServiceEditPanel.add_widget( Label(size_hint=(1, None), text='Cache mode only works for static content')) self.localServiceEditPanel.add_widget( self.settingButton(c, "Access", "useDHT", 'yes')) self.localServiceEditPanel.add_widget( Label( size_hint=(1, None), text= 'DHT Discovery uses a proxy server on Android. \nDisabling this saves bandwidth but makes access from outside your network\nunreliable.' )) btn1 = Button(text='Save Changes') btn1.bind(on_press=save) self.localServiceEditPanel.add_widget(btn1) btn2 = Button(text='Delete this service') btn2.bind(on_press=delete) self.localServiceEditPanel.add_widget(btn2) self.screenManager.current = "EditLocalService" def makeButtonForLocalService(self, name, c=None): "Make a button that, when pressed, edits the local service in the title" btn = Button(text=name) def f(*a): self.editLocalService(name, c) btn.bind(on_press=f) return btn def makeLocalServicesPage(self): screen = Screen(name='LocalServices') self.servicesScreen = screen layout = BoxLayout(orientation='vertical', spacing=10) screen.add_widget(layout) label = Label( size_hint=(1, None), halign="center", text= 'WARNING: Running a local service may use a lot of data and battery.\nChanges may require service restart.' ) labelw = Label( size_hint=(1, None), halign="center", text= 'WARNING 2: This app currently prefers the external SD card for almost everything including the keys.' ) layout.add_widget(self.makeBackButton()) layout.add_widget(label) layout.add_widget(labelw) btn2 = Button(text='Create a service') btn2.bind(on_press=self.promptAddService) layout.add_widget(btn2) self.localServicesListBoxScroll = ScrollView(size_hint=(1, 1)) self.localServicesListBox = BoxLayout(orientation='vertical', size_hint=(1, None), spacing=10) self.localServicesListBox.bind( minimum_height=self.localServicesListBox.setter('height')) self.localServicesListBoxScroll.add_widget(self.localServicesListBox) layout.add_widget(self.localServicesListBoxScroll) return screen def promptAddService(self, *a, **k): def f(v): if v: self.editLocalService(v) self.askQuestion("New Service filename?", cb=f) def goToLocalServices(self, *a): "Go to a page wherein we can list user-modifiable services." self.localServicesListBox.clear_widgets() try: s = daemonconfig.listServices(None) time.sleep(0.5) for i in s: self.localServicesListBox.add_widget( self.makeButtonForLocalService(i, s[i])) except Exception: logging.info(traceback.format_exc()) self.screenManager.current = "LocalServices"
class CardArray(MDBoxLayout): def __init__(self, **kwargs): super(CardArray, self).__init__(**kwargs) self.popup_container = MDBoxLayout(orientation='vertical', spacing=2, padding=2) container = MDBoxLayout(orientation='vertical', spacing=2, padding=2) popup_close_button = MDRoundFlatButton(text='Exit Panel', size_hint_x=.5, on_press=self.close_popup, pos_hint={ "center_x": .5, "center_y": .5 }, md_bg_color=(1, 0, 0, 1), text_color=(1, 0, 1, 1), font_size='18sp') container.add_widget(self.popup_container) container.add_widget(popup_close_button) container.add_widget(MDLabel(text='')) self.popup = Popup(size_hint=(.4, .3), content=container) def close_popup(self, *args): self.popup.dismiss() def open_popup(self, title='', label_text='', app=None, *args): self.popup.title = title if label_text in ('OFF', 'ON'): self.create_switch_control(app=app, text=label_text) else: self.create_numeric_control(app=app, text=label_text) self.popup.open() def create_numeric_control(self, app, text=''): grid = MDGridLayout(spacing=3, padding=3, cols=3) decrease_number_btn = MDFloatingActionButton( icon="minus", md_bg_color=app.theme_cls.primary_color, on_release=self.numeric_decrement) self.parameter_label = LabelButton( text=text, font_size=40, halign='center', theme_text_color="Custom", text_color=(1, .2, 1, 1), on_release=self.commit_parameter_value) increase_number_btn = MDFloatingActionButton( icon="plus", md_bg_color=app.theme_cls.primary_color, on_release=self.numeric_increment) self.popup_container.clear_widgets() grid.add_widget(decrease_number_btn) grid.add_widget(self.parameter_label) grid.add_widget(increase_number_btn) self.popup_container.add_widget(grid) def create_switch_control(self, text, app): self.switch_state = True if text == 'ON' else False grid = MDGridLayout(spacing=3, padding=3, cols=3) self.switch = MySwitch(width=dp(64), active=self.switch_state) self.switch.bind(on_press=self.switch_callback) self.parameter_label = LabelButton( text=text, font_size=40, halign='center', theme_text_color="Custom", text_color=(1, 0, 0, 1) if not self.switch.active else (0, 1, 0, 1), on_release=self.commit_parameter_value) commit_btn = MDRoundFlatIconButton( text="Commit", md_bg_color=app.theme_cls.primary_color, on_release=self.commit_parameter_value) self.popup_container.clear_widgets() grid.add_widget(self.parameter_label) grid.add_widget(self.switch) grid.add_widget(commit_btn) self.popup_container.add_widget(grid) def _numeric_buttons_callback(self, *args): widget = self.parameter_label value_char = '' if widget.text.isdigit(): value_digit = eval(widget.text) else: values = widget.text.split(':') value_char = values[0] + ':' value_digit = eval(values[-1]) return value_digit, value_char def numeric_decrement(self, *args): widget = self.parameter_label value_digit, value_char = self._numeric_buttons_callback() value_digit -= 1 widget.text = value_char + str(value_digit) def numeric_increment(self, *args): widget = self.parameter_label value_digit, value_char = self._numeric_buttons_callback() value_digit += 1 widget.text = value_char + str(value_digit) def commit_parameter_value(self, *args): print('value sent to the backend') def switch_callback(self, *args): print('switch function is called') self.switch_state = self.switch.active self.parameter_label.text = 'ON' if self.switch_state else 'OFF' self.parameter_label.text_color = (1, 0, 0, 1) if not self.switch_state else (0, 1, 0, 1)