From 54aeb257c9d9e9acbb3cfddc8f115c11307a3bb9 Mon Sep 17 00:00:00 2001 From: juju2143 Date: Mon, 2 Oct 2023 14:49:24 -0400 Subject: [PATCH] First commit --- .gitignore | 400 ++++++++++++++++++++++++++++++++ Gui.cs | 109 +++++++++ LICENSE | 137 +++++++++++ MainUI.cs | 50 ++++ Media/ImageMediaHandler.cs | 62 +++++ Media/MediaHandler.cs | 76 ++++++ Media/PlainMediaHandler.cs | 32 +++ Program.cs | 33 +++ Protocols/AboutProtoHandler.cs | 76 ++++++ Protocols/DataProtoHandler.cs | 45 ++++ Protocols/FileProtoHandler.cs | 25 ++ Protocols/GeminiProtoHandler.cs | 69 ++++++ Protocols/HttpProtoHandler.cs | 33 +++ Protocols/ProtoHandler.cs | 90 +++++++ Protocols/lib/Gemini.cs | 122 ++++++++++ README.md | 3 + Tab.cs | 77 ++++++ Telemetry.cs | 25 ++ shoko.csproj | 23 ++ 19 files changed, 1487 insertions(+) create mode 100644 .gitignore create mode 100644 Gui.cs create mode 100644 LICENSE create mode 100644 MainUI.cs create mode 100644 Media/ImageMediaHandler.cs create mode 100644 Media/MediaHandler.cs create mode 100644 Media/PlainMediaHandler.cs create mode 100644 Program.cs create mode 100644 Protocols/AboutProtoHandler.cs create mode 100644 Protocols/DataProtoHandler.cs create mode 100644 Protocols/FileProtoHandler.cs create mode 100644 Protocols/GeminiProtoHandler.cs create mode 100644 Protocols/HttpProtoHandler.cs create mode 100644 Protocols/ProtoHandler.cs create mode 100644 Protocols/lib/Gemini.cs create mode 100644 README.md create mode 100644 Tab.cs create mode 100644 Telemetry.cs create mode 100644 shoko.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0bbfd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,400 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +imgui.ini \ No newline at end of file diff --git a/Gui.cs b/Gui.cs new file mode 100644 index 0000000..fe561bb --- /dev/null +++ b/Gui.cs @@ -0,0 +1,109 @@ +using ImGuiNET; +using Microsoft.VisualBasic; + +namespace Shoko; + +/// +/// This class mainly wraps ImGui calls to use lambdas instead of BeginX/EndX, or those that gives out a boolean as a return parameter +/// +static class Gui +{ + public static void Window(ReadOnlySpan name, Action f) + { + if(ImGui.Begin(name)) + { + f(); + } + ImGui.End(); + } + public static void Window(ReadOnlySpan name, ImGuiWindowFlags flags, Action f) + { + if(ImGui.Begin(name, flags)) + { + f(); + } + ImGui.End(); + } + public static void Window(ReadOnlySpan name, ref bool open, Action f) + { + if(ImGui.Begin(name, ref open)) + { + f(); + } + ImGui.End(); + } + public static void Window(ReadOnlySpan name, ref bool open, ImGuiWindowFlags flags, Action f) + { + if(ImGui.Begin(name, ref open, flags)) + { + f(); + } + ImGui.End(); + } + public static void Menu(ReadOnlySpan label, Action f) + { + if(ImGui.BeginMenu(label)) + { + f(); + ImGui.EndMenu(); + } + } + public static void MainMenuBar(Action f) + { + if(ImGui.BeginMainMenuBar()) + { + f(); + ImGui.EndMainMenuBar(); + } + } + public static void MenuItem(ReadOnlySpan name, ReadOnlySpan shortcut, Action f) + { + bool result = false; + ImGui.MenuItem(name, shortcut, ref result); + if(result) f(); + } + public static void Button(ReadOnlySpan label, Action f) + { + if(ImGui.Button(label)) f(); + } + public static void MenuBar(Action f) + { + if(ImGui.BeginMenuBar()) + { + f(); + ImGui.EndMenuBar(); + } + } + public static void TreeNode(ReadOnlySpan label, Action f) + { + if(ImGui.TreeNode(label)) + { + f(); + ImGui.TreePop(); + } + } + public static void Popup(ReadOnlySpan str_id, Action f) + { + if(ImGui.BeginPopup(str_id)) + { + f(); + ImGui.EndPopup(); + } + } + public static void Popup(ReadOnlySpan str_id, ImGuiWindowFlags flags, Action f) + { + if(ImGui.BeginPopup(str_id, flags)) + { + f(); + ImGui.EndPopup(); + } + } + public static void PopupModal(ReadOnlySpan name, Action f) + { + if(ImGui.BeginPopupModal(name)) + { + f(); + ImGui.EndPopup(); + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..53411d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,137 @@ +Licence Libre du Québec – Permissive (LiLiQ-P) + +Version 1.1 + +1. Préambule +Cette licence s'applique à tout logiciel distribué dont le titulaire du droit d'auteur précise qu'il est sujet aux termes de la Licence Libre du Québec – Permissive (LiLiQ-P) (ci-après appelée la « licence »). + +2. Définitions +Dans la présente licence, à moins que le contexte n'indique un sens différent, on entend par: +« concédant » : le titulaire du droit d'auteur sur le logiciel, ou toute personne dûment autorisée par ce dernier à accorder la présente licence; +« contributeur » : le titulaire du droit d'auteur ou toute personne autorisée par ce dernier à soumettre au concédant une contribution. Un contributeur dont sa contribution est incorporée au logiciel est considéré comme un concédant en regard de sa contribution; +« contribution » : tout logiciel original, ou partie de logiciel original soumis et destiné à être incorporé dans le logiciel; +« distribution » : le fait de délivrer une copie du logiciel; +« licencié » : toute personne qui possède une copie du logiciel et qui exerce les droits concédés par la licence; +« logiciel » : une œuvre protégée par le droit d'auteur, telle qu'un programme d'ordinateur et sa documentation, pour laquelle le titulaire du droit d'auteur a précisé qu'elle est sujette aux termes de la présente licence; +« logiciel dérivé » : tout logiciel original réalisé par un licencié, autre que le logiciel ou un logiciel modifié, qui produit ou reproduit la totalité ou une partie importante du logiciel; +« logiciel modifié » : toute modification par un licencié de l'un des fichiers source du logiciel ou encore tout nouveau fichier source qui incorpore le logiciel ou une partie importante de ce dernier. + +3. Licence de droit d'auteur +Sous réserve des termes de la licence, le concédant accorde au licencié une licence non exclusive et libre de redevances lui permettant d’exercer les droits suivants sur le logiciel : +1° Produire ou reproduire la totalité ou une partie importante; +2° Exécuter ou représenter la totalité ou une partie importante en public; +3° Publier la totalité ou une partie importante; +4° Sous-licencier sous une autre licence libre, approuvée ou certifiée par la Free Software Foundation ou l'Open Source Initiative. + +Cette licence est accordée sans limite territoriale et sans limite de temps. + +L'exercice complet de ces droits est sujet à la distribution par le concédant du code source du logiciel, lequel doit être sous une forme permettant d'y apporter des modifications. Le concédant peut aussi distribuer le logiciel accompagné d'une offre de distribuer le code source du logiciel, sans frais supplémentaires, autres que ceux raisonnables afin de permettre la livraison du code source. Cette offre doit être valide pendant une durée raisonnable. + +4. Distribution +Le licencié peut distribuer des copies du logiciel, d'un logiciel modifié ou dérivé, sous réserve de respecter les conditions suivantes : +1° Le logiciel doit être accompagné d'un exemplaire de cette licence; +2° Si le logiciel a été modifié, le licencié doit en faire la mention, de préférence dans chacun des fichiers modifiés dont la nature permet une telle mention; +3° Les étiquettes ou mentions faisant état des droits d'auteur, des marques de commerce, des garanties ou de la paternité concernant le logiciel ne doivent pas être modifiées ou supprimées, à moins que ces étiquettes ou mentions ne soient inapplicables à un logiciel modifié ou dérivé donné. + +5. Contributions +Sous réserve d'une entente distincte, toute contribution soumise par un contributeur au concédant pour inclusion dans le logiciel sera soumise aux termes de cette licence. + +6. Marques de commerce +La licence n'accorde aucune permission particulière qui permettrait d'utiliser les marques de commerce du concédant, autre que celle requise permettant d'identifier la provenance du logiciel. + +7. Garanties +Sauf mention contraire, le concédant distribue le logiciel sans aucune garantie, aux risques et périls de l'acquéreur de la copie du logiciel, et ce, sans assurer que le logiciel puisse répondre à un besoin particulier ou puisse donner un résultat quelconque. + +Sans lier le concédant d'une quelconque manière, rien n'empêche un licencié d'offrir ou d'exclure des garanties ou du support. + +8. Responsabilité +Le licencié est responsable de tout préjudice résultant de l'exercice des droits accordés par la licence. + +Le concédant ne saurait être tenu responsable de dommages subis par le licencié ou par des tiers, pour quelque cause que ce soit en lien avec la licence et les droits qui y sont accordés. + +9. Résiliation +La présente licence est automatiquement résiliée dès que les droits qui y sont accordés ne sont pas exercés conformément aux termes qui y sont stipulés. + +Toutefois, si le défaut est corrigé dans un délai de 30 jours de sa prise de connaissance par la personne en défaut, et qu'il s'agit du premier défaut, la licence est accordée de nouveau. + +Pour tout défaut subséquent, le consentement exprès du concédant est nécessaire afin que la licence soit accordée de nouveau. + +10. Version de la licence +Le Centre de services partagés du Québec, ses ayants cause ou toute personne qu'il désigne, peuvent diffuser des versions révisées ou modifiées de cette licence. Chaque version recevra un numéro unique. Si un logiciel est déjà soumis aux termes d'une version spécifique, c'est seulement cette version qui liera les parties à la licence. + +Le concédant peut aussi choisir de concéder la licence sous la version actuelle ou toute version ultérieure, auquel cas le licencié peut choisir sous quelle version la licence lui est accordée. + +11. Divers +Dans la mesure où le concédant est un ministère, un organisme public ou une personne morale de droit public, créés en vertu d'une loi de l'Assemblée nationale du Québec, la licence est régie par le droit applicable au Québec et en cas de contestation, les tribunaux du Québec seront seuls compétents. + +La présente licence peut être distribuée sans conditions particulières. Toutefois, une version modifiée doit être distribuée sous un nom différent. Toute référence au Centre de services partagés du Québec, et, le cas échéant, ses ayant cause, doit être retirée, autre que celle permettant d'identifier la provenance de la licence. + +--- + +Québec Free and Open-Source Licence – Permissive (LiLiQ-P) + +Version 1.1 + +1. Preamble +This licence applies to any distributed software stipulated by its copyright owner to be subject to the terms of the Québec Free and Open-Source Licence – Permissive (LiLiQ-P) (hereinafter referred to as the “licence”). + +2. Definitions +Unless the context indicates otherwise, the following terms are used in this licence: +“contribution”: any original software or part of original software submitted and intended to be integrated into the software; +“contributor”: the copyright owner or any person authorized by the copyright owner to submit a contribution to the licensor. A contributor whose contribution is integrated into the software is considered a licensor with respect to that contribution; +“derived software”: any original software developed by a licensee, other than the software or modified software, that produces or reproduces all or a substantial part of the software; +“distribution”: the act of delivering a copy of the software; +“licensee”: any person possessing a copy of the software who exercises the rights granted by the licence; +“licensor”: the software copyright owner or any person duly authorized by the copyright owner to grant this licence; +“modified software”: any modification made by a licensee to one of the software’s source code files, or any new source code file that integrates the software or a substantial part of it; +“software”: a copyright-protected work such as a computer program and its documentation, stipulated by the copyright owner to be subject to the terms of this licence. + +3. Copyright licence +Subject to the terms of this licence, the licensor grants the licensee a non-exclusive, royalty-free licence allowing the licensee to exercise the following rights regarding the software: +(1) Produce or reproduce the software or a substantial part thereof; +(2) Perform the software or any substantial part of it in public; +(3) Publish the software or any substantial part of it. +(4) Sub-license under another free or open-source licence approved or certified by the Free Software Foundation or the Open Source Initiative. + +This licence is granted on a world-wide, perpetual basis. + +Full exercise of these rights is subject to distribution by the licensor of the software source code in a form allowing it to be modified. The licensor may also distribute the software, along with an offer to distribute the software source code, without additional charges other than reasonable charges for delivery of the source code. That offer must be valid for a reasonable period of time. + +4. Distribution +The licensee may distribute copies of the software, modified software or derived software, subject to the following conditions: +(1) The software must be accompanied by a copy of this licence. +(2) If the software has been modified, the licensee must mention this, preferably in every modified file that allows for such a mention. +(3) Software copyright, trademark, warranty or attribution labels or notices must not be modified or removed, unless the labels or notices do not apply to specific modified or derived software. + +5. Contributions +Subject to a separate agreement, every contribution submitted by a contributor to the licensor for inclusion in the software is subject to the terms of this licence. + +6. Trademarks +This licence does not grant any special permission to use the licensor’s trademarks, except as needed to describe the origin of the software. + +7. Warranties +Unless otherwise specified, the licensor distributes the software without any warranty, at the risk of the acquirer of a copy of the software, and without any warranty that the software is suited to any specific need or will yield any specific results. + +Without binding the licensor in any way, nothing prevents a licensee from offering or excluding warranties or support. + +8. Liability +The licensee is liable for any prejudice resulting from the exercise of the rights granted under the licence. + +The licensor cannot be held liable for any prejudice sustained by the licensee or third parties for any reason whatsoever related to the licence and the rights it grants. + +9. Termination +This licence is automatically terminated should the rights it grants fail to be exercised in accordance with the terms of the licence. + +However, if the failure is remedied within 30 days after its discovery by the person in default and it is the first failure, the licence will be granted once again. + +For any subsequent failure, the licensor’s express consent is required for the licence to be granted once again. + +10. Licence version +The Centre de services partagés du Québec, its successors or any person it designates may release revised or modified versions of this licence. Each version will be given a unique number. If software is already subject to the terms of a specific version, the parties to the licence will be bound solely by that version. + +The licensor may also expressly choose to grant the licence in its current version or any subsequent version, in which case the licensee may choose the license version to be granted. + +11. Miscellaneous +To the extent that the licensor is a government department, public body or legal person established in the public interest and created under a law of the National Assembly of Québec, the licence is governed by the laws applicable in Québec and, in the event of a dispute, the courts of Québec have sole jurisdiction. + +This licence may be distributed without any special conditions. However, a modified version must be distributed under a different name. Any reference to the Centre de services partagés du Québec or its successors, where applicable, must be withdrawn, except as needed to describe the origin of the licence. \ No newline at end of file diff --git a/MainUI.cs b/MainUI.cs new file mode 100644 index 0000000..687c896 --- /dev/null +++ b/MainUI.cs @@ -0,0 +1,50 @@ +using System.Reflection; +using ImGuiNET; + +namespace Shoko; + +static class MainUI +{ + static string txtURL = ""; + + public static List tabs = new List(); + + public static void NewTab(string url) + { + tabs.Add(new Tab(url)); + } + + public static bool Render() + { + bool quit = true; + ImGui.DockSpaceOverViewport(); + + Gui.MainMenuBar(()=> + { + Gui.Menu("File", ()=> + { + Gui.MenuItem("Quit", null, ()=> quit = false); + }); + Gui.Menu("Help", ()=> + { + Gui.MenuItem("About", null, ()=> NewTab("about:")); + }); + + ImGui.InputText("##url", ref txtURL, 1024); + + Gui.Button("Go", ()=>{ + NewTab(txtURL); + txtURL = ""; + }); + }); + + tabs = tabs.Where(x=>x.IsOpen).ToList(); + + foreach (var tab in tabs) + { + tab.Render(); + } + + return quit; + } +} \ No newline at end of file diff --git a/Media/ImageMediaHandler.cs b/Media/ImageMediaHandler.cs new file mode 100644 index 0000000..9de0d5c --- /dev/null +++ b/Media/ImageMediaHandler.cs @@ -0,0 +1,62 @@ +using ImGuiNET; +using Raylib_cs; +using rlImGui_cs; + +namespace Shoko; + +[MediaType("image/png")] +[MediaType("image/gif")] +[MediaType("image/qoi")] +[MediaType("image/x-qoi")] +class ImageMediaHandler : MediaHandler +{ + Dictionary MediaTypes = new(){ + ["image/png"] = ".png", + ["image/gif"] = ".gif", + ["image/qoi"] = ".qoi", + ["image/x-qoi"] = ".qoi", + }; + Texture2D Texture; + float Zoom = 1; + public ImageMediaHandler(ProtoHandler content) + { + Content = content; + } + + public override void Load() + { + using(var memory = new MemoryStream()) + { + Content.Content.CopyTo(memory); + var image = Raylib.LoadImageFromMemory(MediaTypes[Content.MediaType], memory.ToArray()); + Texture = Raylib.LoadTextureFromImage(image); + Raylib.UnloadImage(image); + } + } + + public override void Render() + { + if(Zoom == 1) + rlImGui.Image(Texture); + else + rlImGui.ImageSize(Texture, (int)(Texture.width*Zoom), (int)(Texture.height*Zoom)); + } + + public override void MenuBar() + { + Gui.Menu("Image", ()=>{ + ImGui.Text(string.Format("{0}, {1}x{2}", Content.MediaType, Texture.width, Texture.height)); + ImGui.SliderFloat("Zoom", ref Zoom, 0.0625f, 16f, (Zoom*100f).ToString("0.0"), ImGuiSliderFlags.Logarithmic); + }); + } + + public virtual void Render(int width, int height) + { + rlImGui.ImageSize(Texture, width, height); + } + + ~ImageMediaHandler() + { + Raylib.UnloadTexture(Texture); + } +} \ No newline at end of file diff --git a/Media/MediaHandler.cs b/Media/MediaHandler.cs new file mode 100644 index 0000000..8c43667 --- /dev/null +++ b/Media/MediaHandler.cs @@ -0,0 +1,76 @@ +using System.Reflection; +using System.Text.RegularExpressions; +using ImGuiNET; + +namespace Shoko; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +class MediaTypeAttribute : Attribute +{ + public string MediaType; + public MediaTypeAttribute(string type) + { + MediaType = type; + } +} + +class MediaHandler +{ + public ProtoHandler Content; + public string Title; + public MediaHandler() + { + + } + public MediaHandler(ProtoHandler content) + { + Content = content; + } + public virtual void Load() + { + } + public virtual void Render() + { + Title = "Error"; + ImGui.Text("unknown media type: " + Content.MediaType); + } + public virtual void MenuBar() + { + } + + /// + /// Get the appropriate media handler for the URL + /// + /// + /// + public static MediaHandler GetHandler(ProtoHandler content) + { + var types = Assembly.GetAssembly(typeof(MediaHandler)).GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(MediaHandler))); + Type type; + try + { + type = types.Where(t=>t.GetCustomAttributes().Any(x => x.MediaType == content.MediaType)) + .Single(); + } + catch + { + var rgx = new Regex("/.*"); + var mediatype = rgx.Replace(content.MediaType, "/*"); + type = types.Where(t=>t.GetCustomAttributes().Any(x => x.MediaType == mediatype)) + .SingleOrDefault(typeof(MediaHandler)); + } + return (MediaHandler)Activator.CreateInstance(type, content); + } + + public static string[] SupportedMedia + { + get + { + return Assembly.GetAssembly(typeof(MediaHandler)).GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(MediaHandler))) + .SelectMany(t => t.GetCustomAttributes().Select(x => x.MediaType)) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/Media/PlainMediaHandler.cs b/Media/PlainMediaHandler.cs new file mode 100644 index 0000000..83b00f9 --- /dev/null +++ b/Media/PlainMediaHandler.cs @@ -0,0 +1,32 @@ +using ImGuiNET; + +namespace Shoko; + +[MediaType("text/plain")] +[MediaType("text/*")] +class PlainMediaHandler : MediaHandler +{ + List lines; + public PlainMediaHandler(ProtoHandler content) + { + Content = content; + lines = new List(); + } + + public override void Load() + { + Title = new UriBuilder(Content.URL).Path; + var reader = new StreamReader(Content.Content); + string line; + while((line = reader.ReadLine()) is not null) + { + lines.Add(line); + } + } + + public override void Render() + { + foreach(var line in lines) + ImGui.TextUnformatted(line); + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..25f8477 --- /dev/null +++ b/Program.cs @@ -0,0 +1,33 @@ +using ImGuiNET; +using Raylib_cs; +using rlImGui_cs; + +namespace Shoko; + +class Program +{ + static void Main(string[] args) + { + Raylib.SetConfigFlags(ConfigFlags.FLAG_WINDOW_RESIZABLE | ConfigFlags.FLAG_WINDOW_MAXIMIZED); + Raylib.InitWindow(1024, 768, "Shoko"); + rlImGui.Setup(true); + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable; + bool quit = true; + if(args.Length > 0) + foreach (var arg in args) + MainUI.NewTab(arg); + while(!Raylib.WindowShouldClose() && quit) + { + Raylib.BeginDrawing(); + Raylib.ClearBackground(Color.WHITE); + rlImGui.Begin(); + + quit = MainUI.Render(); + + rlImGui.End(); + Raylib.EndDrawing(); + } + rlImGui.Shutdown(); + Raylib.CloseWindow(); + } +} \ No newline at end of file diff --git a/Protocols/AboutProtoHandler.cs b/Protocols/AboutProtoHandler.cs new file mode 100644 index 0000000..bb894b8 --- /dev/null +++ b/Protocols/AboutProtoHandler.cs @@ -0,0 +1,76 @@ +using System.Reflection; +using System.Text; +using ImGuiNET; +using Raylib_cs; + +namespace Shoko; + +[Protocol("about")] +[Protocol("shoko")] +class AboutProtoHandler : ProtoHandler +{ + public AboutProtoHandler(Uri url) + { + URL = url; + } + + public override void Load() + { + var path = new UriBuilder(URL).Path; + Content = new MemoryStream(new byte[]{}); + + switch(path) + { + case "blank": + case "": + case "about": + case "shoko": + case "version": + case "demo": + break; + default: + throw new NotImplementedException(); + } + + MediaType = "text/plain"; + Status = "OK"; + Loaded = true; + } + public override void Render() + { + var path = new UriBuilder(URL).Path; + + switch(path) + { + case "": + case "about": + case "shoko": + case "version": + var info = Assembly.GetExecutingAssembly().GetName(); + ImGui.Text("硝子 (shōko) v"+info.Version.ToString()); + ImGui.Text("an experimental browser"); + ImGui.Separator(); + ImGui.Text("(c) 2023 a39 studios"); + ImGui.Text("licensed under LiLiQ-P"); + ImGui.Separator(); + ImGui.Text(".NET v" + Environment.Version.ToString()); + ImGui.Text("Dear ImGui v" + ImGui.GetVersion()); + ImGui.Text("Raylib v" + Raylib.RAYLIB_VERSION); + ImGui.Separator(); + Gui.TreeNode("Supported protos", ()=>{ + foreach(var proto in SupportedProtos) + ImGui.BulletText(proto); + }); + Gui.TreeNode("Supported media", ()=>{ + foreach(var media in MediaHandler.SupportedMedia) + ImGui.BulletText(media); + }); + break; + case "demo": + ImGui.ShowDemoWindow(); + break; + default: + break; + } + } +} \ No newline at end of file diff --git a/Protocols/DataProtoHandler.cs b/Protocols/DataProtoHandler.cs new file mode 100644 index 0000000..f6e9071 --- /dev/null +++ b/Protocols/DataProtoHandler.cs @@ -0,0 +1,45 @@ +using System.Text; + +namespace Shoko; + +[Protocol("data")] +class DataProtoHandler : ProtoHandler +{ + public DataProtoHandler(Uri url) + { + URL = url; + } + + public override void Load() + { + var data = new UriBuilder(URL).Path; + + var parts = data.Split(","); + var type = parts[0].Split(";", StringSplitOptions.TrimEntries).ToList(); + var dict = new Dictionary(); + MediaType = type.Count > 0 && type[0].Length > 0 ? type[0] : "text/plain"; + type.RemoveAt(0); + + foreach (var item in type) + { + var val = item.Split("=", 2); + dict[val[0]] = val.Length > 1 ? val[1] : ""; + } + + if(dict.ContainsKey("base64")) + { + Content = new MemoryStream(Convert.FromBase64String(parts[1])); + } + else + { + Content = new MemoryStream(Encoding.UTF8.GetBytes(parts[1])); + } + + MediaTypeParams = dict; + Status = "OK"; + Loaded = true; + } + public override void Render() + { + } +} \ No newline at end of file diff --git a/Protocols/FileProtoHandler.cs b/Protocols/FileProtoHandler.cs new file mode 100644 index 0000000..c6c3121 --- /dev/null +++ b/Protocols/FileProtoHandler.cs @@ -0,0 +1,25 @@ +namespace Shoko; + +[Protocol("file")] +class FileProtoHandler : ProtoHandler +{ + public FileProtoHandler(Uri url) + { + URL = url; + } + + public override void Load() + { + var file = new UriBuilder(URL).Path; + + var stream = new FileStream(file, FileMode.Open); + + Content = stream; + MediaType = "text/plain"; // TODO: magic numbers + Status = "OK"; + Loaded = true; + } + public override void Render() + { + } +} \ No newline at end of file diff --git a/Protocols/GeminiProtoHandler.cs b/Protocols/GeminiProtoHandler.cs new file mode 100644 index 0000000..83dd34d --- /dev/null +++ b/Protocols/GeminiProtoHandler.cs @@ -0,0 +1,69 @@ +using System.Text; +using ImGuiNET; + +namespace Shoko; + +[Protocol("gemini")] +class GeminiProtoHandler : ProtoHandler +{ + string txtQuery = ""; + public GeminiProtoHandler(Uri url) + { + URL = url; + } + + public override void Load() + { + var gemini = new Gemini(URL); + gemini.Connect(); + var header = gemini.ReadHeader(); + var meta = header.Split(" ", 2, StringSplitOptions.TrimEntries); + Status = meta[0]; + + MediaType = "text/plain"; + + if(Status.StartsWith('2')) + { + if(meta.Length > 1) + { + var dict = new Dictionary(); + var type = meta[1].Split(";", StringSplitOptions.TrimEntries).ToList(); + MediaType = type[0]; + + foreach (var item in type) + { + var val = item.Split("=", 2); + dict[val[0]] = val.Length > 1 ? val[1] : ""; + } + + MediaTypeParams = dict; + } + Content = gemini.sslStream; + } + else + { + byte[] content = new byte[]{}; + if(meta.Length > 1) + content = Encoding.UTF8.GetBytes(meta[1]); + Content = new MemoryStream(content); + } + + Loaded = true; + } + + public override void Render() + { + if(Status.StartsWith('1')) + { + ImGui.InputText("##query", ref txtQuery, 1024); + + Gui.Button("Submit", ()=>{ + var uri = new UriBuilder(URL) + { + Query = txtQuery + }; + CurrentTab.Load(uri.Uri.ToString()); + }); + } + } +} \ No newline at end of file diff --git a/Protocols/HttpProtoHandler.cs b/Protocols/HttpProtoHandler.cs new file mode 100644 index 0000000..33a58bc --- /dev/null +++ b/Protocols/HttpProtoHandler.cs @@ -0,0 +1,33 @@ +using System.Reflection; + +namespace Shoko; + +[Protocol("http")] +[Protocol("https")] +class HttpProtoHandler : ProtoHandler +{ + public HttpProtoHandler(Uri url) + { + URL = url; + } + + public override void Load() + { + HttpClient client = new(){ + BaseAddress = URL + }; + client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 shoko/" + Assembly.GetExecutingAssembly().GetName().Version.ToString()); + + var response = client.GetAsync(URL).Result; + + Headers = response.Content.Headers; + Content = response.Content.ReadAsStream(); + MediaType = response.Content.Headers.ContentType.MediaType ?? "text/html"; + MediaTypeParams = response.Content.Headers.ContentType.Parameters.Select(x => new KeyValuePair(x.Name, x.Value)); + Status = string.Format("{0} {1}", (int)response.StatusCode, response.StatusCode.ToString()); + Loaded = true; + } + public override void Render() + { + } +} \ No newline at end of file diff --git a/Protocols/ProtoHandler.cs b/Protocols/ProtoHandler.cs new file mode 100644 index 0000000..78e440b --- /dev/null +++ b/Protocols/ProtoHandler.cs @@ -0,0 +1,90 @@ +using System.Diagnostics; +using System.Reflection; +using System.Text; + +namespace Shoko; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +class ProtocolAttribute : Attribute +{ + public string Protocol; + public ProtocolAttribute(string proto) + { + Protocol = proto; + } +} + +class ProtoHandler +{ + public Tab CurrentTab; + public Uri URL; + public string MediaType; + public Stream Content; + public string Status; + public bool Loaded = false; + public IEnumerable>> Headers; + public IEnumerable> MediaTypeParams; + public ProtoHandler() + { + + } + public ProtoHandler(Uri url) + { + URL = url; + } + public virtual void Load() + { + Content = new MemoryStream(Encoding.UTF8.GetBytes("error: no handler for this scheme")); + MediaType = "text/plain"; + } + + public virtual void Render() + { + Gui.Button("Open with external program", ()=>{ + try + { + if(OperatingSystem.IsLinux()) + Process.Start("xdg-open", URL.ToString()); + else if(OperatingSystem.IsMacOS()) + Process.Start("open", URL.ToString()); + else if(OperatingSystem.IsWindows()) + Process.Start(URL.ToString()); + } + catch{} + }); + } + + public virtual void MenuBar() + { + } + + /// + /// Get the appropriate protocol handler for the URL + /// + /// + /// + public static ProtoHandler GetHandler(Uri url) + { + var proto = new UriBuilder(url).Scheme; + var type = Assembly.GetAssembly(typeof(ProtoHandler)).GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(ProtoHandler)) && t.GetCustomAttributes().Any(x => x.Protocol == proto)) + .SingleOrDefault(typeof(ProtoHandler)); + return (ProtoHandler)Activator.CreateInstance(type, url); + } + + public static ProtoHandler GetHandler(string url) + { + return GetHandler(new Uri(url)); + } + + public static string[] SupportedProtos + { + get + { + return Assembly.GetAssembly(typeof(ProtoHandler)).GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(ProtoHandler))) + .SelectMany(t => t.GetCustomAttributes().Select(x => x.Protocol)) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/Protocols/lib/Gemini.cs b/Protocols/lib/Gemini.cs new file mode 100644 index 0000000..9140f9a --- /dev/null +++ b/Protocols/lib/Gemini.cs @@ -0,0 +1,122 @@ +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace Shoko; + +class Gemini +{ + public Uri Url { get; set; } + public X509CertificateCollection ClientCertificates = null; + public X509Certificate ServerCertificate + { + get => _serverCertificate; + } + X509Certificate _serverCertificate = null; + TcpClient client; + public SslStream sslStream; + public Gemini(string url, X509CertificateCollection certificates) + { + Url = new Uri(url); + ClientCertificates = certificates; + } + public Gemini(Uri url, X509CertificateCollection certificates) + { + Url = url; + ClientCertificates = certificates; + } + public Gemini(string url) + { + Url = new Uri(url); + } + public Gemini(Uri url) + { + Url = url; + } + bool ValidateServerCertificate( + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + /* if (sslPolicyErrors == SslPolicyErrors.None) + return true; */ + // TODO: validate user certificates + //Console.WriteLine("Certificate error: {0}", sslPolicyErrors); + _serverCertificate = certificate; + return true; + } + public void Connect() + { + var port = Url.Port; + if(port < 0) port = 1965; + client = new TcpClient(Url.DnsSafeHost, port); + sslStream = new SslStream(client.GetStream(), false, + new RemoteCertificateValidationCallback(ValidateServerCertificate), null); + try + { + sslStream.AuthenticateAsClient(new SslClientAuthenticationOptions{ + TargetHost = Url.Host, + ClientCertificates = ClientCertificates, + EnabledSslProtocols = SslProtocols.Tls12, + //ApplicationProtocols = { new SslApplicationProtocol(), new SslApplicationProtocol("gemini") } + }); + } + catch(AuthenticationException) + { + client.Close(); + throw; + } + byte[] message = Encoding.UTF8.GetBytes(Url.AbsoluteUri+"\r\n"); + + sslStream.Write(message); + sslStream.Flush(); + } + public string ReadHeader() + { + StringBuilder message = new StringBuilder(); + int b; + do + { + b = sslStream.ReadByte(); + message.Append((char)b); + } while(b != 10 && b != -1); + return message.ToString(); + } + public int Read(byte[] buffer, int offset, int count) => sslStream.Read(buffer, offset, count); + public string ReadAll() + { + byte[] buffer = new byte[2048]; + StringBuilder messageData = new StringBuilder(); + int bytes = -1; + do + { + bytes = Read(buffer, 0, buffer.Length); + + Decoder decoder = Encoding.UTF8.GetDecoder(); + char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)]; + decoder.GetChars(buffer, 0, bytes, chars, 0); + messageData.Append(chars); + } while (bytes != 0); + + return messageData.ToString(); + } + public string ReadBase64() + { + IEnumerable array = new byte[0]; + int bytes = -1; + byte[] buffer = new byte[2048]; + do + { + bytes = Read(buffer, 0, buffer.Length); + var buffer2 = new byte[bytes]; + Array.Copy(buffer, buffer2, bytes); + array = array.Concat(buffer2); + } while (bytes != 0); + + return Convert.ToBase64String(array.ToArray()); + } + public void Close() => client.Close(); +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..28dedcd --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Shōko + +Shōko (硝子) is a browser and file viewer written in C#, supporting many protocols, schemes, and media types. It is designed to be simple and very extensible. \ No newline at end of file diff --git a/Tab.cs b/Tab.cs new file mode 100644 index 0000000..e519b7f --- /dev/null +++ b/Tab.cs @@ -0,0 +1,77 @@ +using HtmlAgilityPack; +using ImGuiNET; + +namespace Shoko; + +class Tab +{ + ProtoHandler Handler; + MediaHandler Document; + public bool IsOpen = true; + Exception Error = null; + + string txtURL = ""; + + public Tab(string url) + { + Load(url); + txtURL = url; + } + + public void Load(string url) + { + Error = null; + try + { + Handler = ProtoHandler.GetHandler(url); + Handler.CurrentTab = this; + Handler.Load(); + Document = MediaHandler.GetHandler(Handler); + Document.Load(); + txtURL = Handler.URL.ToString(); + } + catch(Exception ex) + { + Error = ex; + } + } + + public void Render() + { + var title = txtURL; + if(Document is not null) + { + title = Document.Title; + } + Gui.Window(title+"###"+GetHashCode().ToString(), ref IsOpen, ImGuiWindowFlags.MenuBar | ImGuiWindowFlags.HorizontalScrollbar, ()=> + { + Gui.MenuBar(()=>{ + Gui.Menu("File", ()=>{ + Gui.MenuItem("Close", null, ()=> IsOpen = false); + }); + + if(Error is null) + { + Handler.MenuBar(); + Document.MenuBar(); + } + + ImGui.InputText("##url", ref txtURL, 1024); + + Gui.Button("Go", ()=>{ + Load(txtURL); + }); + }); + if(Error is not null) + { + ImGui.Text("error: can't load page"); + ImGui.Text(Error.Message); + } + else + { + Document.Render(); + Handler.Render(); + } + }); + } +} \ No newline at end of file diff --git a/Telemetry.cs b/Telemetry.cs new file mode 100644 index 0000000..4c2013f --- /dev/null +++ b/Telemetry.cs @@ -0,0 +1,25 @@ +//////////////////////////////////////////////////////////////////////////////// +// // +// // +// // +// // +// // +// // +// // +// // +// // +// // +// // +// [THIS FILE INTENTIONALLY LEFT BLANK] // +// // +// // +// // +// // +// // +// // +// // +// // +// // +// // +// // +//////////////////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/shoko.csproj b/shoko.csproj new file mode 100644 index 0000000..76478f7 --- /dev/null +++ b/shoko.csproj @@ -0,0 +1,23 @@ + + + + Exe + net7.0 + enable + disable + true + + Shōko + 0.0.1 + an experimental browser + a39 studios + © 2023 + + + + + + + + +