First commit

This commit is contained in:
Yuki 2023-10-02 14:49:24 -04:00
commit 54aeb257c9
19 changed files with 1487 additions and 0 deletions

400
.gitignore vendored Normal file
View File

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

109
Gui.cs Normal file
View File

@ -0,0 +1,109 @@
using ImGuiNET;
using Microsoft.VisualBasic;
namespace Shoko;
/// <summary>
/// This class mainly wraps ImGui calls to use lambdas instead of BeginX/EndX, or those that gives out a boolean as a return parameter
/// </summary>
static class Gui
{
public static void Window(ReadOnlySpan<char> name, Action f)
{
if(ImGui.Begin(name))
{
f();
}
ImGui.End();
}
public static void Window(ReadOnlySpan<char> name, ImGuiWindowFlags flags, Action f)
{
if(ImGui.Begin(name, flags))
{
f();
}
ImGui.End();
}
public static void Window(ReadOnlySpan<char> name, ref bool open, Action f)
{
if(ImGui.Begin(name, ref open))
{
f();
}
ImGui.End();
}
public static void Window(ReadOnlySpan<char> name, ref bool open, ImGuiWindowFlags flags, Action f)
{
if(ImGui.Begin(name, ref open, flags))
{
f();
}
ImGui.End();
}
public static void Menu(ReadOnlySpan<char> 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<char> name, ReadOnlySpan<char> shortcut, Action f)
{
bool result = false;
ImGui.MenuItem(name, shortcut, ref result);
if(result) f();
}
public static void Button(ReadOnlySpan<char> 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<char> label, Action f)
{
if(ImGui.TreeNode(label))
{
f();
ImGui.TreePop();
}
}
public static void Popup(ReadOnlySpan<char> str_id, Action f)
{
if(ImGui.BeginPopup(str_id))
{
f();
ImGui.EndPopup();
}
}
public static void Popup(ReadOnlySpan<char> str_id, ImGuiWindowFlags flags, Action f)
{
if(ImGui.BeginPopup(str_id, flags))
{
f();
ImGui.EndPopup();
}
}
public static void PopupModal(ReadOnlySpan<char> name, Action f)
{
if(ImGui.BeginPopupModal(name))
{
f();
ImGui.EndPopup();
}
}
}

137
LICENSE Normal file
View File

@ -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 dexercer 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 softwares 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 licensors 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 licensors 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.

50
MainUI.cs Normal file
View File

@ -0,0 +1,50 @@
using System.Reflection;
using ImGuiNET;
namespace Shoko;
static class MainUI
{
static string txtURL = "";
public static List<Tab> tabs = new List<Tab>();
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;
}
}

View File

@ -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<string, string> 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);
}
}

76
Media/MediaHandler.cs Normal file
View File

@ -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()
{
}
/// <summary>
/// Get the appropriate media handler for the URL
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
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<MediaTypeAttribute>().Any(x => x.MediaType == content.MediaType))
.Single();
}
catch
{
var rgx = new Regex("/.*");
var mediatype = rgx.Replace(content.MediaType, "/*");
type = types.Where(t=>t.GetCustomAttributes<MediaTypeAttribute>().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<MediaTypeAttribute>().Select(x => x.MediaType))
.ToArray();
}
}
}

View File

@ -0,0 +1,32 @@
using ImGuiNET;
namespace Shoko;
[MediaType("text/plain")]
[MediaType("text/*")]
class PlainMediaHandler : MediaHandler
{
List<string> lines;
public PlainMediaHandler(ProtoHandler content)
{
Content = content;
lines = new List<string>();
}
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);
}
}

33
Program.cs Normal file
View File

@ -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();
}
}

View File

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

View File

@ -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<string, string>();
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()
{
}
}

View File

@ -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()
{
}
}

View File

@ -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<string, string>();
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());
});
}
}
}

View File

@ -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<string, string>(x.Name, x.Value));
Status = string.Format("{0} {1}", (int)response.StatusCode, response.StatusCode.ToString());
Loaded = true;
}
public override void Render()
{
}
}

90
Protocols/ProtoHandler.cs Normal file
View File

@ -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<KeyValuePair<string,IEnumerable<string>>> Headers;
public IEnumerable<KeyValuePair<string,string>> 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()
{
}
/// <summary>
/// Get the appropriate protocol handler for the URL
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
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<ProtocolAttribute>().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<ProtocolAttribute>().Select(x => x.Protocol))
.ToArray();
}
}
}

122
Protocols/lib/Gemini.cs Normal file
View File

@ -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<byte> 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();
}

3
README.md Normal file
View File

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

77
Tab.cs Normal file
View File

@ -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();
}
});
}
}

25
Telemetry.cs Normal file
View File

@ -0,0 +1,25 @@
////////////////////////////////////////////////////////////////////////////////
// //
// //
// //
// //
// //
// //
// //
// //
// //
// //
// //
// [THIS FILE INTENTIONALLY LEFT BLANK] //
// //
// //
// //
// //
// //
// //
// //
// //
// //
// //
// //
////////////////////////////////////////////////////////////////////////////////

23
shoko.csproj Normal file
View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Product>Shōko</Product>
<Version>0.0.1</Version>
<Description>an experimental browser</Description>
<Company>a39 studios</Company>
<Copyright>© 2023</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Raylib-cs" Version="4.5.0.4" />
<PackageReference Include="rlImgui-cs" Version="1.0.3" />
<PackageReference Include="ImGui.NET" Version="1.89.9.2" />
</ItemGroup>
</Project>