49 Commits
Author SHA1 Message Date
Stone_Red 1f341af237 Merge pull request #6 from Stone-Red-Code/develop
Change command name
2024-06-23 13:43:54 +02:00
Stone_Red e27a195781 Change command name 2024-06-23 13:43:20 +02:00
Stone_Red 179dbbce2b Merge pull request #5 from Stone-Red-Code/develop
Fix summary in snapcraft.yaml
2024-06-23 13:32:29 +02:00
Stone_Red 15a46f392e Fix summary in snapcraft.yaml 2024-06-23 13:32:06 +02:00
Stone_Red 1229f8dfe0 Merge pull request #4 from Stone-Red-Code/develop
Develop
2024-06-22 23:39:17 +02:00
Stone_Red 839d9402e8 Update libraries 2024-06-22 23:38:35 +02:00
Stone_Red 6ebf525b6f Update summary, fix tags and add description 2024-06-21 15:28:22 +02:00
Stone_Red 3de441a3de Update nuspec 2024-06-04 20:24:08 +02:00
Stone_Red 75bacf8117 Update README.md 2024-06-04 19:41:17 +02:00
Stone_Red 5c95bb2663 Merge pull request #3 from Stone-Red-Code/develop
Develop
2024-06-04 18:17:06 +02:00
Stone_Red 3436f358be Update version information 2024-06-04 18:15:45 +02:00
Stone_Red 0724aac442 Update installer 2024-06-04 18:11:47 +02:00
Stone_Red 05928fca82 Merge pull request #2 from Stone-Red-Code/develop
Develop
2024-06-04 18:08:43 +02:00
Stone_Red 07c982afd5 Merge branch 'main' into develop 2024-06-04 18:08:36 +02:00
Stone_Red 789350736e Add package manifests and update readme 2024-06-04 18:07:43 +02:00
Stone_Red 813f3b39a0 Small UI improvements 2024-06-04 16:52:56 +02:00
Stone_Red e943ed0bd7 Restore Content from TabState when switching tabs 2024-06-04 16:26:11 +02:00
Stone_Red 27244ba2a3 Try to open file if hyper link does not contain a valid URL 2024-06-04 16:19:36 +02:00
Stone_Red 551bb2ad90 Add download processor 2024-06-04 16:18:51 +02:00
Stone_Red 32037ffcf8 Add option to disable caching for a specific content type 2024-06-04 16:09:34 +02:00
Stone_Red 7bb7a174ed Show progress message on progress bar 2024-06-04 16:08:56 +02:00
Stone_Red 2d75a1070b Add new Raw Content view 2024-06-03 17:44:33 +02:00
Stone_Red 8d50d759c9 Add URL history to tabs 2024-05-17 10:47:32 +02:00
Stone_Red c5c55c1164 Merge pull request #1 from Dumpy250/feature/cache
DRAFT: Feature/cache
2024-05-16 19:39:18 +02:00
Stone_Red d8a3230552 Merge branch 'develop' into feature/cache 2024-05-16 19:39:01 +02:00
Stone_Red 657dbffcd0 Fix formatting and some warnings 2024-05-16 19:37:18 +02:00
Cameron a9bd9c8854 fix/clean caching 2024-05-15 22:01:26 -04:00
Stone_Red 6beaa9c9e8 Update window title 2024-05-07 18:01:49 +02:00
Stone_Red c68bfdb667 Remove margin from MarkdownContentView 2024-05-02 20:44:19 +02:00
Stone_Red 9e259a7215 Fix alignment issues 2024-05-02 19:44:48 +02:00
Stone_Red 281a59165c Add logging to HttpPathResolver.cs 2024-04-30 18:25:23 +02:00
Stone_Red 441c41543c Add logging 2024-04-30 18:21:55 +02:00
Stone_Red fe43fe933d Make paths cross-platform 2024-04-30 18:21:36 +02:00
Stone_Red cb8d13fe08 Fix closing tab causes browser to load the website of the first tab & some safety checks 2024-04-30 16:33:05 +02:00
Cameron 87d9e7abb6 fixes/reload button fix 2024-04-29 08:14:04 -04:00
Cameron 8ea493f2c8 tweak .gitconfig 2024-04-29 06:30:44 -04:00
Cameron 6f797b8174 implement cache attempt 2024-04-27 18:09:19 -04:00
Stone_Red f53eb36dd0 Add UserAgent setting 2024-04-27 20:11:40 +02:00
Stone_Red aa1484f5d4 Add Name and Description properties to IContentProcessor 2024-04-27 20:11:25 +02:00
Stone_Red 571d8c2b4b Don't try to handle svg images 2024-04-15 09:57:19 +02:00
Stone_Red f7225494f6 Fix default search engine URL 2024-04-11 17:11:07 +02:00
Stone_Red b4edef9021 Add common image processor 2024-04-11 10:08:38 +02:00
Stone_Red 4f3c59a32c Add search engine url setting 2024-04-10 11:14:20 +02:00
Stone_Red cbe32f7bdf Add F11 fullscreen functionality 2024-04-04 23:10:49 +02:00
Stone_Red e8d8745a2b Fix toggle sidebar keybinding and disable content panel when laoding 2024-04-04 21:31:17 +02:00
Stone_Red bb78a4a067 Implement text/html processor 2024-04-04 14:46:02 +02:00
Stone_Red 7eb1694c44 Update screenshots in README.md 2024-04-04 13:59:20 +02:00
Stone_Red b83f2bc5b6 Update README.md 2024-04-04 00:09:09 +02:00
Stone_Red 942e58ac0f Add basic content processing structure 2024-04-03 20:59:58 +02:00
53 changed files with 1704 additions and 351 deletions
+140
View File
@@ -396,3 +396,143 @@ FodyWeavers.xsd
# JetBrains Rider # JetBrains Rider
*.sln.iml *.sln.iml
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Rider ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
+8 -4
View File
@@ -8,8 +8,10 @@ It uses the [ReverseMarkdown](https://github.com/mysticmind/reversemarkdown-net)
## Setup ## Setup
1. Download one of the [releases](https://github.com/Stone-Red-Code/Markdowser/releases). 1. Download the latest release
1. Execute the `Markdowser-Installer.exe` file. - GitHub (Windows) [releases](https://github.com/Stone-Red-Code/Markdowser/releases)
- Chocolatey (Windows): `choco install Markdowser`
- Snapcraft (Linux): `snap install Markdowser`
1. Enter a URL in the address bar. 1. Enter a URL in the address bar.
1. Enjoy! 1. Enjoy!
@@ -19,8 +21,9 @@ It uses the [ReverseMarkdown](https://github.com/mysticmind/reversemarkdown-net)
## Preview ## Preview
![Screenshot 2024-04-03 173609](https://github.com/Stone-Red-Code/Markdowser/assets/56473591/e7a1b9f2-a416-448b-9e54-a91e5794ffb3) ![Markdowser](https://github.com/Stone-Red-Code/Markdowser/assets/56473591/d46af13f-3589-424b-b91f-0ebcb72eacc8)
![Screenshot 2024-04-03 172715](https://github.com/Stone-Red-Code/Markdowser/assets/56473591/e96c2f48-b5fc-4f76-b7d1-10aa07f3cfeb) ![Markdowser](https://github.com/Stone-Red-Code/Markdowser/assets/56473591/89818632-427c-40be-97a8-94ae0b4f8fd4)
## Third party licenses ## Third party licenses
@@ -28,3 +31,4 @@ It uses the [ReverseMarkdown](https://github.com/mysticmind/reversemarkdown-net)
- [Icons.Avalonia](https://github.com/Projektanker/Icons.Avalonia) - [MIT](https://github.com/Projektanker/Icons.Avalonia/blob/main/LICENSE) - [Icons.Avalonia](https://github.com/Projektanker/Icons.Avalonia) - [MIT](https://github.com/Projektanker/Icons.Avalonia/blob/main/LICENSE)
- [Semi.Avalonia](https://github.com/irihitech/Semi.Avalonia) - [MIT](https://github.com/irihitech/Semi.Avalonia/blob/main/LICENSE) - [Semi.Avalonia](https://github.com/irihitech/Semi.Avalonia) - [MIT](https://github.com/irihitech/Semi.Avalonia/blob/main/LICENSE)
- [ReverseMarkdown](https://github.com/mysticmind/reversemarkdown-net) - [MIT](https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE) - [ReverseMarkdown](https://github.com/mysticmind/reversemarkdown-net) - [MIT](https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE)
- [Syroot.Windows.IO.KnownFolders](https://gitlab.com/Syroot/KnownFolders) - [MIT](https://gitlab.com/Syroot/KnownFolders/-/blob/master/LICENSE)
+133
View File
@@ -0,0 +1,133 @@
## Summary
How do I create packages? See https://docs.chocolatey.org/en-us/create/create-packages
If you are submitting packages to the community feed (https://community.chocolatey.org)
always try to ensure you have read, understood and adhere to the create
packages wiki link above.
## Automatic Packaging Updates?
Consider making this package an automatic package, for the best
maintainability over time. Read up at https://docs.chocolatey.org/en-us/create/automatic-packages
## Shim Generation
Any executables you include in the package or download (but don't call
install against using the built-in functions) will be automatically shimmed.
This means those executables will automatically be included on the path.
Shim generation runs whether the package is self-contained or uses automation
scripts.
By default, these are considered console applications.
If the application is a GUI, you should create an empty file next to the exe
named 'name.exe.gui' e.g. 'bob.exe' would need a file named 'bob.exe.gui'.
See https://docs.chocolatey.org/en-us/create/create-packages#how-do-i-set-up-shims-for-applications-that-have-a-gui
If you want to ignore the executable, create an empty file next to the exe
named 'name.exe.ignore' e.g. 'bob.exe' would need a file named
'bob.exe.ignore'.
See https://docs.chocolatey.org/en-us/create/create-packages#how-do-i-exclude-executables-from-getting-shims
## Self-Contained?
If you have a self-contained package, you can remove the automation scripts
entirely and just include the executables, they will automatically get shimmed,
which puts them on the path. Ensure you have the legal right to distribute
the application though. See https://docs.chocolatey.org/en-us/information/legal.
You should read up on the Shim Generation section to familiarize yourself
on what to do with GUI applications and/or ignoring shims.
## Automation Scripts
You have a powerful use of Chocolatey, as you are using PowerShell. So you
can do just about anything you need. Choco has some very handy built-in
functions that you can use, these are sometimes called the helpers.
### Built-In Functions
https://docs.chocolatey.org/en-us/create/functions
A note about a couple:
* Get-ToolsLocation - used to get you the 'tools' root, which by default is set to 'c:\tools', not the chocolateyInstall bin folder - see https://docs.chocolatey.org/en-us/create/functions/get-toolslocation
* Install-BinFile - used for non-exe files - executables are automatically shimmed... - see https://docs.chocolatey.org/en-us/create/functions/install-binfile
* Uninstall-BinFile - used for non-exe files - executables are automatically shimmed - see https://docs.chocolatey.org/en-us/create/functions/uninstall-binfile
### Getting package specific information
Use the package parameters pattern - see https://docs.chocolatey.org/en-us/guides/create/parse-packageparameters-argument
### Need to mount an ISO?
https://docs.chocolatey.org/en-us/guides/create/mount-an-iso-in-chocolatey-package
### Environment Variables
Chocolatey makes a number of environment variables available (You can access any of these with $env:TheVariableNameBelow):
* TEMP/TMP - Overridden to the CacheLocation, but may be the same as the original TEMP folder
* ChocolateyInstall - Top level folder where Chocolatey is installed
* ChocolateyPackageName - The name of the package, equivalent to the `<id />` field in the nuspec
* ChocolateyPackageTitle - The title of the package, equivalent to the `<title />` field in the nuspec
* ChocolateyPackageVersion - The normalized version of the package, equivalent to a normalized edition of the `<version />` field in the nuspec
* ChocolateyPackageFolder - The top level location of the package folder - the folder where Chocolatey has downloaded and extracted the NuGet package, typically `C:\ProgramData\chocolatey\lib\packageName`.
#### Advanced Environment Variables
The following are more advanced settings:
* ChocolateyPackageParameters - Parameters to use with packaging, not the same as install arguments (which are passed directly to the native installer). Based on `--package-parameters`.
* CHOCOLATEY_VERSION - The version of Choco you normally see. Use if you are 'lighting' things up based on choco version. Otherwise take a dependency on the specific version you need.
* ChocolateyForceX86 = If available and set to 'true', then user has requested 32bit version. Automatically handled in built in Choco functions.
* OS_PLATFORM - Like Windows, macOS, Linux.
* OS_VERSION - The version of OS, like 10.0 something something for Windows.
* OS_NAME - The reported name of the OS.
* USER_NAME = The user name
* USER_DOMAIN = The user domain name (could also be local computer name)
* IS_PROCESSELEVATED = Is the process elevated?
* IS_SYSTEM = Is the user the system account?
* IS_REMOTEDESKTOP = Is the user in a terminal services session?
* ChocolateyToolsLocation - formerly 'ChocolateyBinRoot' ('ChocolateyBinRoot' will be removed with Chocolatey v2.0.0), this is where tools being installed outside of Chocolatey packaging will go.
#### Set By Options and Configuration
Some environment variables are set based on options that are passed, configuration and/or features that are turned on:
* ChocolateyEnvironmentDebug - Was `--debug` passed? If using the built-in PowerShell host, this is always true (but only logs debug messages to console if `--debug` was passed)
* ChocolateyEnvironmentVerbose - Was `--verbose` passed? If using the built-in PowerShell host, this is always true (but only logs verbose messages to console if `--verbose` was passed).
* ChocolateyExitOnRebootDetected - Are we exiting on a detected reboot? Set by ` --exit-when-reboot-detected` or the feature `exitOnRebootDetected`
* ChocolateyForce - Was `--force` passed?
* ChocolateyForceX86 - Was `-x86` passed?
* ChocolateyRequestTimeout - How long before a web request will time out. Set by config `webRequestTimeoutSeconds`
* ChocolateyResponseTimeout - How long to wait for a download to complete? Set by config `commandExecutionTimeoutSeconds`
* ChocolateyPowerShellHost - Are we using the built-in PowerShell host? Set by `--use-system-powershell` or the feature `powershellHost`
#### Business Edition Variables
* ChocolateyInstallArgumentsSensitive - Encrypted arguments passed from command line `--install-arguments-sensitive` that are not logged anywhere.
* ChocolateyPackageParametersSensitive - Package parameters passed from command line `--package-parameters-sensitive` that are not logged anywhere.
* ChocolateyLicensedVersion - What version is the licensed edition on?
* ChocolateyLicenseType - What edition / type of the licensed edition is installed?
* USER_CONTEXT - The original user context - different when self-service is used (Licensed)
#### Experimental Environment Variables
The following are experimental or use not recommended:
* OS_IS64BIT = This may not return correctly - it may depend on the process the app is running under
* CHOCOLATEY_VERSION_PRODUCT = the version of Choco that may match CHOCOLATEY_VERSION but may be different - based on git describe
* IS_ADMIN = Is the user an administrator? But doesn't tell you if the process is elevated.
* IS_REMOTE = Is the user in a remote session?
#### Not Useful Or Anti-Pattern If Used
* ChocolateyInstallOverride = Not for use in package automation scripts. Based on `--override-arguments` being passed.
* ChocolateyInstallArguments = The installer arguments meant for the native installer. You should use chocolateyPackageParameters instead. Based on `--install-arguments` being passed.
* ChocolateyIgnoreChecksums - Was `--ignore-checksums` passed or the feature `checksumFiles` turned off?
* ChocolateyAllowEmptyChecksums - Was `--allow-empty-checksums` passed or the feature `allowEmptyChecksums` turned on?
* ChocolateyAllowEmptyChecksumsSecure - Was `--allow-empty-checksums-secure` passed or the feature `allowEmptyChecksumsSecure` turned on?
* ChocolateyChecksum32 - Was `--download-checksum` passed?
* ChocolateyChecksumType32 - Was `--download-checksum-type` passed?
* ChocolateyChecksum64 - Was `--download-checksum-x64` passed?
* ChocolateyChecksumType64 - Was `--download-checksum-type-x64` passed?
* ChocolateyPackageExitCode - The exit code of the script that just ran - usually set by `Set-PowerShellExitCode`
* ChocolateyLastPathUpdate - Set by Chocolatey as part of install, but not used for anything in particular in packaging.
* ChocolateyProxyLocation - The explicit proxy location as set in the configuration `proxy`
* ChocolateyDownloadCache - Use available download cache? Set by `--skip-download-cache`, `--use-download-cache`, or feature `downloadCache`
* ChocolateyProxyBypassList - Explicitly set locations to ignore in configuration `proxyBypassList`
* ChocolateyProxyBypassOnLocal - Should the proxy bypass on local connections? Set based on configuration `proxyBypassOnLocal`
* http_proxy - Set by original `http_proxy` passthrough, or same as `ChocolateyProxyLocation` if explicitly set.
* https_proxy - Set by original `https_proxy` passthrough, or same as `ChocolateyProxyLocation` if explicitly set.
* no_proxy- Set by original `no_proxy` passthrough, or same as `ChocolateyProxyBypassList` if explicitly set.
+126
View File
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Read this before creating packages: https://docs.chocolatey.org/en-us/create/create-packages -->
<!-- It is especially important to read the above link to understand additional requirements when
publishing packages to the community feed aka dot org (https://community.chocolatey.org/packages). -->
<!-- Test your packages in a test environment:
https://github.com/chocolatey/chocolatey-test-environment -->
<!--
This is a nuspec. It mostly adheres to https://docs.nuget.org/create/Nuspec-Reference. Chocolatey
uses a special version of NuGet.Core that allows us to do more than was initially possible. As such
there are certain things to be aware of:
* the package xmlns schema url may cause issues with nuget.exe
* Any of the following elements can ONLY be used by choco tools - projectSourceUrl, docsUrl,
mailingListUrl, bugTrackerUrl, packageSourceUrl, provides, conflicts, replaces
* nuget.exe can still install packages with those elements but they are ignored. Any authoring
tools or commands will error on those elements
-->
<!-- You can embed software files directly into packages, as long as you are not bound by
distribution rights. -->
<!-- * If you are an organization making private packages, you probably have no issues here -->
<!-- * If you are releasing to the community feed, you need to consider distribution rights. -->
<!-- Do not remove this test for UTF-8: if “Ω” doesnt appear as greek uppercase omega letter
enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<!-- == PACKAGE SPECIFIC SECTION == -->
<!-- This section is about this package, although id and version have ties back to the software -->
<!-- id is lowercase and if you want a good separator for words, use '-', not '.'. Dots are only
acceptable as suffixes for certain types of packages, e.g. .install, .portable, .extension,
.template -->
<!-- If the software is cross-platform, attempt to use the same id as the debian/rpm package(s)
if possible. -->
<id>markdowser</id>
<!-- version should MATCH as closely as possible with the underlying software -->
<!-- Is the version a prerelease of a version?
https://docs.nuget.org/create/versioning#creating-prerelease-packages -->
<!-- Note that unstable versions like 0.0.1 can be considered a released version, but it's
possible that one can release a 0.0.1-beta before you release a 0.0.1 version. If the version
number is final, that is considered a released version and not a prerelease. -->
<version>0.2.0.0</version>
<!-- <packageSourceUrl>Where is this Chocolatey package located (think GitHub)? packageSourceUrl
is highly recommended for the community feed</packageSourceUrl>-->
<!-- owners is a poor name for maintainers of the package. It sticks around by this name for
compatibility reasons. It basically means you. -->
<!--<owners>__REPLACE_YOUR_NAME__</owners>-->
<!-- ============================== -->
<!-- == SOFTWARE SPECIFIC SECTION == -->
<!-- This section is about the software itself -->
<title>Markdowser</title>
<authors>Stone_Red</authors>
<!-- projectUrl is required for the community feed -->
<projectUrl>https://github.com/Stone-Red-Code/Markdowser</projectUrl>
<!-- There are a number of CDN Services that can be used for hosting the Icon for a package.
More information can be found here:
https://docs.chocolatey.org/en-us/create/create-packages#package-icon-guidelines -->
<!-- Here is an example using Githack -->
<iconUrl>https://content.stone-red.net/Markdowser-1-1.png</iconUrl>
<!-- <copyright>Year Software Vendor</copyright> -->
<!-- If there is a license Url available, it is required for the community feed -->
<licenseUrl>https://github.com/Stone-Red-Code/Markdowser/blob/main/LICENSE</licenseUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/Stone-Red-Code/Markdowser</projectSourceUrl>
<!--<docsUrl>At
what url are the software docs located?</docsUrl>-->
<!--<mailingListUrl></mailingListUrl>-->
<bugTrackerUrl>https://github.com/Stone-Red-Code/Markdowser/issues</bugTrackerUrl>
<tags>markdowser</tags>
<summary>A web browser that renders markdown instead of HTML</summary>
<description>
A web browser that renders markdown instead of HTML
**What is it?**
Markdowser is a web browser that renders Markdown instead of HTML.
It uses the ReverseMarkdown library to convert HTML websites to Markdown. This gets rid of all the clutter and is supposed to make the content more readable and consistent across different websites.
**Setup**
1. Download the latest release
- GitHub (Windows): https://github.com/Stone-Red-Code/Markdowser/releases
- Chocolatey (Windows): `choco install Markdowser`
- Snapcraft (Linux): `snap install Markdowser`
2. Enter a URL in the address bar.
3. Enjoy!
**Third party licenses**
- Avalonia (https://github.com/AvaloniaUI/Avalonia) - MIT (https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md)
- Icons.Avalonia (https://github.com/Projektanker/Icons.Avalonia) - MIT (https://github.com/Projektanker/Icons.Avalonia/blob/main/LICENSE)
- Semi.Avalonia (https://github.com/irihitech/Semi.Avalonia) - MIT (https://github.com/irihitech/Semi.Avalonia/blob/main/LICENSE)
- ReverseMarkdown (https://github.com/mysticmind/reversemarkdown-net) - MIT (https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE)
- Syroot.Windows.IO.KnownFolders (https://gitlab.com/Syroot/KnownFolders) - MIT (https://gitlab.com/Syroot/KnownFolders/-/blob/master/LICENSE)
</description>
<releaseNotes>https://github.com/Stone-Red-Code/Markdowser/releases/latest</releaseNotes>
<!-- =============================== -->
<!-- Specifying dependencies and version ranges?
https://docs.nuget.org/create/versioning#specifying-version-ranges-in-.nuspec-files -->
<!--<dependencies>
<dependency id="" version="__MINIMUM_VERSION__" />
<dependency id="" version="[__EXACT_VERSION__]" />
<dependency id="" version="[_MIN_VERSION_INCLUSIVE, MAX_VERSION_INCLUSIVE]" />
<dependency id="" version="[_MIN_VERSION_INCLUSIVE, MAX_VERSION_EXCLUSIVE)" />
<dependency id="" />
<dependency id="chocolatey-core.extension" version="1.1.0" />
</dependencies>-->
<!-- chocolatey-core.extension -
https://community.chocolatey.org/packages/chocolatey-core.extension -->
<!--<provides>NOT
YET IMPLEMENTED</provides>-->
<!--<conflicts>NOT
YET IMPLEMENTED</conflicts>-->
<!--<replaces>NOT
YET IMPLEMENTED</replaces>-->
</metadata>
<files>
<!-- this section controls what actually gets packaged into the Chocolatey package -->
<file src="tools\**" target="tools" />
<file src="..\src\Markdowser\bin\Release\net8.0\**" target="tools" />
</files>
</package>
+23
View File
@@ -0,0 +1,23 @@
From: https://github.com/Stone-Red-Code/Markdowser
MIT License
Copyright (c) 2024 Stone_Red
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+8
View File
@@ -0,0 +1,8 @@
VERIFICATION
Verification is intended to assist the Chocolatey moderators and community
in verifying that this package's contents are trustworthy.
Source Code: https://github.com/Stone-Red-Code/Markdowser
Releases: https://github.com/Stone-Red-Code/Markdowser/releases
Checksum: 5f956d038b46d035ef3b5e37a34ec865b6ababa4ec4b00fbba2fab60e9c8bb6583d70b5308a21e7643a28cae624158a27fd3a04e2043c555277a416529a2c06d
Veify using: certutil -hashfile C:\ProgramData\Chocolatey\lib\markdowser\Markdowser.exe sha512
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
name: markdowser # you probably want to 'snapcraft register <name>'
version: "0.2.0.0" # just for humans, typically '1.2+git' or '1.3.2'
grade: stable # must be 'stable' to release into candidate/stable channels
summary: A web browser that renders markdown instead of HTML # 79 char long summary
description: |
**Setup**
1. Download the latest release
- GitHub (Windows): https://github.com/Stone-Red-Code/Markdowser/releases
- Chocolatey (Windows): `choco install Markdowser`
- Snapcraft (Linux): `snap install Markdowser`
2. Enter a URL in the address bar.
3. Enjoy!
**Third party licenses**
- Avalonia (https://github.com/AvaloniaUI/Avalonia) - MIT (https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md)
- Icons.Avalonia (https://github.com/Projektanker/Icons.Avalonia) - MIT (https://github.com/Projektanker/Icons.Avalonia/blob/main/LICENSE)
- Semi.Avalonia (https://github.com/irihitech/Semi.Avalonia) - MIT (https://github.com/irihitech/Semi.Avalonia/blob/main/LICENSE)
- ReverseMarkdown (https://github.com/mysticmind/reversemarkdown-net) - MIT (https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE)
- Syroot.Windows.IO.KnownFolders (https://gitlab.com/Syroot/KnownFolders) - MIT (https://gitlab.com/Syroot/KnownFolders/-/blob/master/LICENSE)
base: core22 # the base snap is the execution environment for this snap
architectures:
- build-on: amd64
- build-on: arm64
confinement: strict # use 'strict' once you have the right plugs and slots
parts:
markdowser:
# See 'snapcraft plugins'
plugin: dotnet
dotnet-build-configuration: Release
dotnet-self-contained-runtime-identifier: linux-x64
source: ./src/Markdowser
build-packages:
- dotnet-sdk-8.0
stage-packages:
- libicu70
apps:
markdowser:
command: Markdowser
+1
View File
@@ -0,0 +1 @@
Markdowser
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+80
View File
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoGeneratedRunConfigurationManager">
<projectFile>Markdowser/Markdowser.csproj</projectFile>
</component>
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="35ad98ab-fce0-4c9e-9180-ec9795d8ed8e" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="DpaMonitoringSettings">
<option name="autoShow" value="false" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 6
}]]></component>
<component name="ProjectId" id="2fg8GifhQMdOrTygPnNJLDd0rxo" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
".NET Project.Markdowser.executor": "Run",
"git-widget-placeholder": "develop",
"node.js.selected.package.tslint": "(autodetect)"
},
"keyToStringList": {
"rider.external.source.directories": [
"C:\\Users\\Camer\\AppData\\Roaming\\JetBrains\\Rider2024.1\\resharper-host\\DecompilerCache",
"C:\\Users\\Camer\\AppData\\Roaming\\JetBrains\\Rider2024.1\\resharper-host\\SourcesCache",
"C:\\Users\\Camer\\AppData\\Local\\Symbols\\src"
]
}
}]]></component>
<component name="RunManager">
<configuration name="Markdowser" type="DotNetProject" factoryName=".NET Project" temporary="true">
<option name="EXE_PATH" value="$PROJECT_DIR$/Markdowser/bin/Debug/net8.0/Markdowser.exe" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Markdowser/bin/Debug/net8.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Markdowser/Markdowser.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
<recent_temporary>
<list>
<item itemvalue=".NET Project.Markdowser" />
</list>
</recent_temporary>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
<component name="VcsManagerConfiguration">
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
</component>
</project>
+21
View File
@@ -3,10 +3,15 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Styling; using Avalonia.Styling;
using CuteUtils.Logging;
using Markdowser.Models; using Markdowser.Models;
using Markdowser.Utilities;
using Markdowser.ViewModels; using Markdowser.ViewModels;
using Markdowser.Views; using Markdowser.Views;
using System.IO;
namespace Markdowser; namespace Markdowser;
public partial class App : Application public partial class App : Application
@@ -18,6 +23,11 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
if (!Directory.Exists(Configuration.ApplicationDataPath))
{
_ = Directory.CreateDirectory(Configuration.ApplicationDataPath);
}
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = new MainWindow desktop.MainWindow = new MainWindow
@@ -25,12 +35,23 @@ public partial class App : Application
DataContext = new MainWindowViewModel(), DataContext = new MainWindowViewModel(),
}; };
// Clear log files
GlobalState.Logger.ClearLogFile(LogSeverity.Debug);
GlobalState.Logger.ClearLogFile(LogSeverity.Info);
GlobalState.Logger.ClearLogFile(LogSeverity.Warn);
GlobalState.Logger.ClearLogFile(LogSeverity.Error);
GlobalState.Logger.ClearLogFile(LogSeverity.Fatal);
desktop.MainWindow.Closing += (sender, e) => desktop.MainWindow.Closing += (sender, e) =>
{ {
GlobalState.Logger.LogInfo("Saving settings...");
Settings.SaveSettings(); Settings.SaveSettings();
GlobalState.Logger.LogInfo("Settings saved.");
}; };
} }
GlobalState.Logger.LogInfo("Application started.");
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
if (Current is not null) if (Current is not null)
@@ -30,7 +30,7 @@ internal class ChangeThemeCommand : ICommand
ThemeVariant theme = app.ActualThemeVariant; ThemeVariant theme = app.ActualThemeVariant;
app.RequestedThemeVariant = theme == ThemeVariant.Dark ? ThemeVariant.Light : ThemeVariant.Dark; app.RequestedThemeVariant = theme == ThemeVariant.Dark ? ThemeVariant.Light : ThemeVariant.Dark;
Settings.Current.DarkMode = app.RequestedThemeVariant == ThemeVariant.Dark; Settings.Current.DarkMode = app.RequestedThemeVariant == ThemeVariant.Dark;
GlobalState.ReloadContent(this); GlobalState.InvokeThemeChanged();
} }
} }
} }
+1 -1
View File
@@ -21,6 +21,6 @@ internal class HomeCommand : ICommand
public void Execute(object? parameter) public void Execute(object? parameter)
{ {
GlobalState.Url = Settings.Current.HomeUrl ?? string.Empty; GlobalState.CurrentTabState.Url = Settings.Current.HomeUrl ?? string.Empty;
} }
} }
+21 -4
View File
@@ -2,6 +2,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Windows.Input; using System.Windows.Input;
namespace Markdowser.Commands; namespace Markdowser.Commands;
@@ -23,19 +24,35 @@ public class HyperlinkCommand : ICommand
{ {
if (parameter is string url) if (parameter is string url)
{ {
Debug.WriteLine($"Hyperlink clicked: {url}"); GlobalState.Logger.LogDebug($"Hyperlink clicked: {url}");
if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
{ {
GlobalState.Url = url; GlobalState.CurrentTabState.Url = url;
} }
else if (Uri.IsWellFormedUriString(url, UriKind.Relative) || url.StartsWith('/')) else if (Uri.IsWellFormedUriString(url, UriKind.Relative) || url.StartsWith('/'))
{ {
GlobalState.Url = new Uri(new Uri(GlobalState.Url), url).ToString(); GlobalState.CurrentTabState.Url = new Uri(new Uri(GlobalState.CurrentTabState.Url), url).ToString();
}
else if (File.Exists(url))
{
ProcessStartInfo processStartInfo = new()
{
FileName = url,
UseShellExecute = true
};
try
{
_ = Process.Start(processStartInfo);
}
catch (Exception ex)
{
GlobalState.Logger.LogError(ex.Message);
}
} }
else else
{ {
Debug.WriteLine($"Invalid URL: {url}"); GlobalState.Logger.LogDebug($"Invalid URL: {url}");
} }
} }
} }
+25 -12
View File
@@ -6,7 +6,8 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Version>0.1.0.0</Version> <Version>0.2.0.0</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -23,18 +24,30 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.10" /> <PackageReference Include="Avalonia" Version="11.0.11" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" /> <PackageReference Include="Avalonia.Desktop" Version="11.0.11" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.11" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.11" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.11" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.10" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.11" />
<PackageReference Include="CuteUtils" Version="1.0.0" />
<PackageReference Include="Markdown.Avalonia" Version="11.0.2" /> <PackageReference Include="Markdown.Avalonia" Version="11.0.2" />
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.1.2" /> <PackageReference Include="Projektanker.Icons.Avalonia" Version="9.3.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.1.2" /> <PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.3.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.1.2" /> <PackageReference Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.3.0" />
<PackageReference Include="ReverseMarkdown" Version="4.3.0" /> <PackageReference Include="ReverseMarkdown" Version="4.5.0" />
<PackageReference Include="Semi.Avalonia" Version="11.0.7.1" /> <PackageReference Include="Semi.Avalonia" Version="11.0.7.3" />
<PackageReference Include="Syroot.Windows.IO.KnownFolders" Version="1.3.0" />
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<Compile Update="Views\Content\CommonImageContentView.axaml.cs">
<DependentUpon>CommonImageContentView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Content\MarkdownContentView.axaml.cs">
<DependentUpon>MarkdownContentView.axaml</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
</Project> </Project>
+14
View File
@@ -1,6 +1,8 @@
using Markdowser.Utilities; using Markdowser.Utilities;
using System;
using System.IO; using System.IO;
using System.Reflection;
using System.Text.Json; using System.Text.Json;
namespace Markdowser.Models; namespace Markdowser.Models;
@@ -11,8 +13,12 @@ public class Settings
public bool DarkMode { get; set; } public bool DarkMode { get; set; }
public string SearchEngineUrl { get; set; } = "https://html.duckduckgo.com/html/?kd=-1&k1=-1&q={0}";
public string? HomeUrl { get; set; } public string? HomeUrl { get; set; }
public string UserAgent { get; set; } = $"{Assembly.GetExecutingAssembly().GetName().Name}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}";
public static void SaveSettings() public static void SaveSettings()
{ {
if (!Directory.Exists(Configuration.ApplicationDataPath)) if (!Directory.Exists(Configuration.ApplicationDataPath))
@@ -26,9 +32,17 @@ public class Settings
private static Settings LoadSettings() private static Settings LoadSettings()
{ {
if (File.Exists(Configuration.SettingsFilePath)) if (File.Exists(Configuration.SettingsFilePath))
{
try
{ {
return JsonSerializer.Deserialize<Settings>(File.ReadAllText(Configuration.SettingsFilePath)) ?? new Settings(); return JsonSerializer.Deserialize<Settings>(File.ReadAllText(Configuration.SettingsFilePath)) ?? new Settings();
} }
catch (Exception ex)
{
GlobalState.Logger.LogWarn(ex.Message);
return new Settings();
}
}
else else
{ {
return new Settings(); return new Settings();
+41
View File
@@ -0,0 +1,41 @@
using Markdowser.ViewModels;
using System;
using System.Collections.Generic;
namespace Markdowser.Models;
public class TabState
{
public event EventHandler? UrlChanged;
private string url = string.Empty;
public Stack<string> BackHistory { get; } = new();
public Stack<string> ForwardHistory { get; } = new();
public string Url
{
get => url;
set
{
if (url != value)
{
BackHistory.Push(url);
ForwardHistory.Clear();
}
url = value;
UrlChanged?.Invoke(this, EventArgs.Empty);
}
}
public ContentViewModelBase? Content { get; set; }
public void SetUrl(string url, object? sender = null)
{
this.url = url;
UrlChanged?.Invoke(sender ?? this, EventArgs.Empty);
}
}
@@ -0,0 +1,24 @@
using Markdowser.ViewModels;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
namespace Markdowser.Processing;
internal class ContentProcessorManager
{
private readonly List<IContentProcessor> processors = [];
public void RegisterProcessor(IContentProcessor processor)
{
processors.Add(processor);
}
public Task<ContentViewModelBase> ProcessContent(HttpResponseMessage httpResponseMessage, IProgress<ProcessingProgress> progress)
{
IContentProcessor? processor = processors.Find(processor => processor.CanProcess(httpResponseMessage.Content.Headers)) ?? throw new InvalidOperationException($"No processor found for content type {httpResponseMessage.Content.Headers.ContentType}");
return processor.Process(httpResponseMessage, progress);
}
}
@@ -0,0 +1,19 @@
using Markdowser.ViewModels;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace Markdowser.Processing;
internal interface IContentProcessor
{
string Name { get; }
string Description { get; }
bool CanProcess(HttpContentHeaders httpContentHeaders);
Task<ContentViewModelBase> Process(HttpResponseMessage httpResponseMessage, IProgress<ProcessingProgress> progress);
}
@@ -0,0 +1,12 @@
namespace Markdowser.Processing;
public readonly struct ProcessingProgress(long current, long total, string? message = null, bool isBytes = false)
{
public long Current { get; } = current;
public long Total { get; } = total;
public string? Message { get; } = message;
public bool IsBytes { get; } = isBytes;
public int Percentage => (int)((double)Current / Total * 100);
}
@@ -0,0 +1,31 @@
using Markdowser.ViewModels;
using Markdowser.ViewModels.Content;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace Markdowser.Processing.Processors;
internal class CommonImageProcessor : IContentProcessor
{
public string Name => "Common Image Processor";
public string Description => "Processes common image types (e.g. PNG, JPEG, GIF)";
public bool CanProcess(HttpContentHeaders httpContentHeaders)
{
if (httpContentHeaders.ContentType?.MediaType == "image/svg+xml")
{
return false;
}
return httpContentHeaders.ContentType?.MediaType?.StartsWith("image/") == true;
}
public async Task<ContentViewModelBase> Process(HttpResponseMessage httpResponseMessage, IProgress<ProcessingProgress> progress)
{
return new CommonImageContentViewModel(httpResponseMessage.RequestMessage?.RequestUri?.Host?.ToString() ?? "Image", await httpResponseMessage.Content.ReadAsStreamAsync());
}
}
@@ -0,0 +1,88 @@
using CuteUtils;
using Markdowser.Utilities;
using Markdowser.ViewModels;
using Markdowser.ViewModels.Content;
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace Markdowser.Processing.Processors;
internal class DownloadProcessor : IContentProcessor
{
public string Name => "Download Processor";
public string Description => "Processes downloadable content";
public bool CanProcess(HttpContentHeaders httpContentHeaders)
{
bool canProcess = httpContentHeaders.ContentDisposition?.FileName is not null;
// Check for common media types that are downloadable
if (!canProcess)
{
canProcess = httpContentHeaders.ContentType?.MediaType switch
{
"application/octet-stream" => true,
"application/force-download" => true,
"application/zip" => true,
"application/x-zip-compressed" => true,
"application/x-zip" => true,
"application/x-compressed" => true,
"multipart/x-zip" => true,
_ => false
};
}
return canProcess;
}
public async Task<ContentViewModelBase> Process(HttpResponseMessage httpResponseMessage, IProgress<ProcessingProgress> progress)
{
string? fileName = httpResponseMessage.Content.Headers.ContentDisposition?.FileName;
long contentLength = httpResponseMessage.Content.Headers.ContentLength ?? 0;
fileName ??= httpResponseMessage.RequestMessage?.RequestUri?.Segments[^1].ToFileName();
if (string.IsNullOrWhiteSpace(fileName))
{
fileName = "download.bin";
}
string tempFileName = fileName + ".tmp";
GlobalState.Logger.LogDebug($"Downloading {fileName}...");
using Stream contentStream = await httpResponseMessage.Content.ReadAsStreamAsync();
using FileStream fileStream = new(Path.Combine(Configuration.DownloadPath, tempFileName), FileMode.Create, FileAccess.Write);
while (true)
{
byte[] buffer = new byte[81920];
int bytesRead = await contentStream.ReadAsync(buffer);
if (bytesRead == 0)
{
break;
}
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
progress.Report(new ProcessingProgress(fileStream.Position, contentLength, message: $"Downloading \"{fileName}\"", true));
}
fileStream.Close();
File.Move(Path.Combine(Configuration.DownloadPath, tempFileName), Path.Combine(Configuration.DownloadPath, fileName), overwrite: true);
string content = $"# Downloaded File\n\nDownloaded file: [{fileName}]({Path.Combine(Configuration.DownloadPath, fileName)}) to {Configuration.DownloadPath}";
return new MarkdownContentViewModel(fileName, content)
{
Cacheable = false
};
}
}
@@ -0,0 +1,101 @@
using HtmlAgilityPack;
using Markdowser.Utilities;
using Markdowser.ViewModels;
using Markdowser.ViewModels.Content;
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Markdowser.Processing.Processors;
internal partial class HtmlProcessor : IContentProcessor
{
private readonly ReverseMarkdown.Converter markdownConverter = new();
public string Name => "HTML Processor";
public string Description => "Processes HTML content";
public HtmlProcessor()
{
markdownConverter.Config.UnknownTags = ReverseMarkdown.Config.UnknownTagsOption.Bypass;
markdownConverter.Config.SuppressDivNewlines = false;
markdownConverter.Config.SmartHrefHandling = false;
}
public bool CanProcess(HttpContentHeaders httpContentHeaders)
{
return httpContentHeaders.ContentType?.MediaType == "text/html";
}
public async Task<ContentViewModelBase> Process(HttpResponseMessage httpResponseMessage, IProgress<ProcessingProgress> progress)
{
StringBuilder html = new();
StringBuilder finalMarkdown = new();
long length = httpResponseMessage.Content.Headers.ContentLength ?? 0;
using Stream contentStream = await httpResponseMessage.Content.ReadAsStreamAsync();
using StreamReader streamReader = new(contentStream);
while (!streamReader.EndOfStream)
{
_ = html.AppendLine(await streamReader.ReadLineAsync());
progress.Report(new ProcessingProgress(Encoding.Default.GetByteCount(html.ToString()), length, "Downloading HTML...", true));
}
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html.ToString());
string title = htmlDoc.DocumentNode.SelectSingleNode("html/head/title")?.InnerText?.Trim() ?? httpResponseMessage.RequestMessage?.RequestUri?.ToString() ?? "Untitled";
GlobalState.Logger.LogDebug("Converting HTML to markdown...");
string markdown = markdownConverter.Convert(html.ToString());
GlobalState.Logger.LogDebug("Processing markdown...");
int currentLine = 0;
string[] lines = markdown.Split('\n');
foreach (string line in lines)
{
progress.Report(new ProcessingProgress(currentLine, lines.Length, "Processing markdown..."));
currentLine++;
string trimmedLine = line.Trim();
if (trimmedLine.All(c => c == '#'))
{
_ = finalMarkdown.Append(trimmedLine);
continue;
}
else if (MarkdownImage().IsMatch(trimmedLine))
{
_ = finalMarkdown.AppendLine()
.Append(trimmedLine)
.AppendLine()
.AppendLine();
continue;
}
_ = finalMarkdown.AppendLine(trimmedLine);
}
return new MarkdownContentViewModel(title, finalMarkdown.ToString())
{
RawContent = html.ToString()
};
}
[GeneratedRegex("!\\[(.*?)\\]\\((.*?)\\)")]
private static partial Regex MarkdownImage();
}
+28
View File
@@ -1,11 +1,17 @@
using Avalonia; using Avalonia;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Markdowser.Utilities;
using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome; using Projektanker.Icons.Avalonia.FontAwesome;
using Projektanker.Icons.Avalonia.MaterialDesign; using Projektanker.Icons.Avalonia.MaterialDesign;
using ReactiveUI;
using System; using System;
using System.Reactive;
using System.Threading.Tasks;
namespace Markdowser; namespace Markdowser;
@@ -16,10 +22,32 @@ internal static class Program
// yet and stuff might break. // yet and stuff might break.
[STAThread] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
GlobalState.Logger.LogFatal(e.ExceptionObject.ToString() ?? "Unknown error.");
};
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
GlobalState.Logger.LogFatal(e.Exception.ToString());
};
RxApp.DefaultExceptionHandler = Observer.Create<Exception>(ex =>
{
GlobalState.Logger.LogFatal(ex.ToString());
});
try
{ {
_ = BuildAvaloniaApp() _ = BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args); .StartWithClassicDesktopLifetime(args);
} }
catch (Exception ex)
{
GlobalState.Logger.LogFatal(ex.ToString());
}
}
// Avalonia configuration, don't remove; also used by visual designer. // Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
+50
View File
@@ -0,0 +1,50 @@
using System;
using System.Globalization;
namespace Markdowser.Utilities;
internal static class BytesToString
{
public static string Convert(long value)
{
string suffix;
double readable;
switch (Math.Abs(value))
{
case >= 0x1000000000000000:
suffix = "EiB";
readable = value >> 50;
break;
case >= 0x4000000000000:
suffix = "PiB";
readable = value >> 40;
break;
case >= 0x10000000000:
suffix = "TiB";
readable = value >> 30;
break;
case >= 0x40000000:
suffix = "GiB";
readable = value >> 20;
break;
case >= 0x100000:
suffix = "MiB";
readable = value >> 10;
break;
case >= 0x400:
suffix = "KiB";
readable = value;
break;
default:
return value.ToString("0 B");
}
return (readable / 1024).ToString("0.00 ", CultureInfo.InvariantCulture) + suffix;
}
}
+27
View File
@@ -0,0 +1,27 @@
using Markdowser.ViewModels;
using System.Collections.Generic;
namespace Markdowser.Utilities;
public class CacheService
{
private readonly Dictionary<string, ContentViewModelBase> cache = [];
public ContentViewModelBase? Get(string url)
{
if (cache.TryGetValue(url, out ContentViewModelBase? value))
{
return value;
}
else
{
return null;
}
}
public void Set(string url, ContentViewModelBase content)
{
cache[url] = content;
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ internal static class ColumnDefinition
} }
else if (!visibility) else if (!visibility)
{ {
_ = element.SetValue(LastWidthProperty, element.GetValue(Avalonia.Controls.ColumnDefinition.WidthProperty)); _ = element.SetValue(LastWidthProperty!, element.GetValue(Avalonia.Controls.ColumnDefinition.WidthProperty));
_ = element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, ZeroWidth); _ = element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, ZeroWidth);
} }
return visibility; return visibility;
+30 -3
View File
@@ -1,12 +1,39 @@
using System; using Syroot.Windows.IO;
using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
namespace Markdowser.Utilities; namespace Markdowser.Utilities;
internal static class Configuration internal static class Configuration
{ {
public static string DownloadPath => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); public static string DownloadPath
public static string ApplicationDataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(Markdowser)); {
get
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return KnownFolders.Downloads.Path;
}
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Downloads");
}
}
public static string ApplicationDataPath
{
get
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "StoneRed", nameof(Markdowser));
}
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StoneRed", nameof(Markdowser));
}
}
public static string SettingsFilePath => Path.Combine(ApplicationDataPath, "settings.json"); public static string SettingsFilePath => Path.Combine(ApplicationDataPath, "settings.json");
public static string LogFilePath => Path.Combine(ApplicationDataPath, $"{nameof(Markdowser)}.log");
} }
+39 -35
View File
@@ -1,51 +1,55 @@
using Avalonia.Controls; using CuteUtils.Logging;
using Markdowser.Models;
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
namespace Markdowser.Utilities; namespace Markdowser.Utilities;
[SuppressMessage("Major Code Smell", "S4220:Events should have proper arguments", Justification = "<Pending>")]
internal static class GlobalState internal static class GlobalState
{ {
public static event EventHandler<string>? UrlChanged; public static event EventHandler? ThemeChanged;
public static event EventHandler? ContentReload; public static TabState CurrentTabState { get; internal set; } = new();
private static string url = string.Empty; public static Logger Logger { get; } = new()
public static Stack<string> BackHistory { get; } = new();
public static Stack<string> ForwardHistory { get; } = new();
public static string Url
{ {
get => url; Config = new()
set
{ {
if (url != value) DebugConfig = new()
{ {
BackHistory.Push(url); LogTarget = LogTarget.DebugConsole
ForwardHistory.Clear(); },
} InfoConfig = new()
url = value;
UrlChanged?.Invoke(null, url);
}
}
public static ObservableCollection<TabItem> Tabs { get; internal set; } = [new TabItem() { Header = "New Tab" }];
internal static void SetUrl(object sender, string url)
{ {
GlobalState.url = url; LogTarget = LogTarget.DebugConsole | LogTarget.File,
UrlChanged?.Invoke(sender, url); FilePath = Configuration.LogFilePath
} },
WarnConfig = new()
internal static void ReloadContent(object sender)
{ {
ContentReload?.Invoke(sender, EventArgs.Empty); LogTarget = LogTarget.DebugConsole | LogTarget.File,
FilePath = Configuration.LogFilePath
},
ErrorConfig = new()
{
LogTarget = LogTarget.DebugConsole | LogTarget.File,
FilePath = Configuration.LogFilePath
},
FatalConfig = new()
{
LogTarget = LogTarget.DebugConsole | LogTarget.File,
FilePath = Configuration.LogFilePath
},
FormatConfig = new()
{
DebugConsoleFormat = new LogFormatBuilder().DateTime().Text(" ").LogSeverity(padding: -6).FilePath().Text(":").MemberName().Text(":").LineNumber().Text(Environment.NewLine).Message(),
FileFormat = new LogFormatBuilder().DateTime().Text(" ").LogSeverity(padding: -6).FilePath().Text(":").MemberName().Text(":").LineNumber().Text(Environment.NewLine).Message(),
}
}
};
public static void InvokeThemeChanged()
{
ThemeChanged?.Invoke(null, EventArgs.Empty);
} }
} }
+32 -5
View File
@@ -1,10 +1,15 @@
using Avalonia.Platform; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using Avalonia.Platform;
using Avalonia.Threading;
using Markdown.Avalonia.Utils; using Markdown.Avalonia.Utils;
using Markdowser.Models; using Markdowser.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -31,15 +36,27 @@ public class HttpPathResolver : IPathResolver
if (!Uri.IsWellFormedUriString(relativeOrAbsolutePath, UriKind.Absolute)) if (!Uri.IsWellFormedUriString(relativeOrAbsolutePath, UriKind.Absolute))
{ {
relativeOrAbsolutePath = new Uri(new Uri(GlobalState.Url), relativeOrAbsolutePath).ToString(); relativeOrAbsolutePath = new Uri(new Uri(GlobalState.CurrentTabState.Url), relativeOrAbsolutePath).ToString();
} }
Debug.WriteLine($"Resolving image: {relativeOrAbsolutePath}"); GlobalState.Logger.LogDebug($"Resolving image: {relativeOrAbsolutePath}");
HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(relativeOrAbsolutePath)!; HttpResponseMessage httpResponseMessage;
try
{
httpResponseMessage = await httpClient.GetAsync(relativeOrAbsolutePath)!;
}
catch (Exception ex)
{
// error with inluding url and message
ShowError($"Failed to fetch image: {ex.Message}\n{relativeOrAbsolutePath} - ");
return GetLogo();
}
if (!httpResponseMessage.IsSuccessStatusCode) if (!httpResponseMessage.IsSuccessStatusCode)
{ {
ShowError($"Failed to fetch image: {httpResponseMessage.ReasonPhrase}\n{relativeOrAbsolutePath}");
return GetLogo(); return GetLogo();
} }
@@ -55,4 +72,14 @@ public class HttpPathResolver : IPathResolver
return AssetLoader.Open(new Uri("avares://Markdowser/Assets/Markdowser-Light-Transparent.png"))!; return AssetLoader.Open(new Uri("avares://Markdowser/Assets/Markdowser-Light-Transparent.png"))!;
} }
private static void ShowError(string message)
{
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.DataContext is ViewModels.MainWindowViewModel vm)
{
Dispatcher.UIThread.Post(() => vm.WindowNotificationManager.Show(new Notification("Error", message, NotificationType.Error)));
}
GlobalState.Logger.LogError(message);
}
} }
+7 -5
View File
@@ -6,20 +6,22 @@ using Markdowser.ViewModels;
using System; using System;
namespace Markdowser; namespace Markdowser;
public class ViewLocator : IDataTemplate public class ViewLocator : IDataTemplate
{ {
public Control? Build(object? data) public Control? Build(object? data)
{ {
if (data is null) if (data is null)
{
return null; return null;
}
var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); string name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name); Type? type = Type.GetType(name);
if (type != null) if (type != null)
{ {
var control = (Control)Activator.CreateInstance(type)!; Control control = (Control)Activator.CreateInstance(type)!;
control.DataContext = data; control.DataContext = data;
return control; return control;
} }
@@ -29,6 +31,6 @@ public class ViewLocator : IDataTemplate
public bool Match(object? data) public bool Match(object? data)
{ {
return data is ViewModelBase; return data is ViewModelBase or ContentViewModelBase;
} }
} }
@@ -0,0 +1,9 @@
using Avalonia.Media.Imaging;
using System.IO;
namespace Markdowser.ViewModels.Content;
internal class CommonImageContentViewModel(string title, Stream stream) : ContentViewModelBase(title)
{
public Bitmap Image { get; set; } = Bitmap.DecodeToHeight(stream, 1080);
}
@@ -0,0 +1,6 @@
namespace Markdowser.ViewModels.Content;
public class MarkdownContentViewModel(string title, string markdown) : ContentViewModelBase(title)
{
public string Markdown { get; } = markdown;
}
@@ -0,0 +1,16 @@
using CuteUtils.Logging;
using Markdowser.Utilities;
namespace Markdowser.ViewModels;
public abstract class ContentViewModelBase(string title)
{
public string Title { get; } = title;
public string RawContent { get; init; } = string.Empty;
public bool Cacheable { get; init; } = true;
public Logger Logger => GlobalState.Logger;
}
+278 -193
View File
@@ -1,23 +1,21 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading; using Avalonia.Threading;
using HtmlAgilityPack;
using Markdowser.Models; using Markdowser.Models;
using Markdowser.Processing;
using Markdowser.Processing.Processors;
using Markdowser.Utilities; using Markdowser.Utilities;
using Markdowser.ViewModels.Content;
using ReactiveUI; using ReactiveUI;
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
@@ -26,45 +24,85 @@ namespace Markdowser.ViewModels;
public partial class MainWindowViewModel : ViewModelBase public partial class MainWindowViewModel : ViewModelBase
{ {
private readonly HttpClient httpClient = new(new HttpClientHandler() { AllowAutoRedirect = true }); private readonly HttpClient httpClient = new(new HttpClientHandler() { AllowAutoRedirect = true });
private readonly ReverseMarkdown.Converter markdownConverter = new(); private readonly ContentProcessorManager contentProcessorManager = new();
private readonly CacheService cacheService = new CacheService();
private readonly StringBuilder html = new();
private StringBuilder? content;
private TabItem currentTab = null!; private TabItem currentTab = null!;
private WindowState windowState;
private bool showSidePanel; private bool showSidePanel;
private bool isBusy; private bool isBusy;
private int progress; private double progress;
public ObservableCollection<TabItem> Tabs => GlobalState.Tabs; private double progressMax;
public string Title => $"{nameof(Markdowser)} - {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}"; private string progressText;
public ObservableCollection<TabItem> Tabs { get; } = [new TabItem() { Header = "New Tab" }];
public TabItem CurrentTab public TabItem CurrentTab
{ {
get => currentTab; get => currentTab;
set set
{ {
if (currentTab is not null) CurrentTabState.UrlChanged -= UrlChanged;
{
currentTab.Tag = Url;
}
_ = this.RaiseAndSetIfChanged(ref currentTab!, value); _ = this.RaiseAndSetIfChanged(ref currentTab!, value);
this.RaisePropertyChanged(nameof(CurrentTabState));
this.RaisePropertyChanged(nameof(Url));
this.RaisePropertyChanged(nameof(BackEnabled));
this.RaisePropertyChanged(nameof(ForwardEnabled));
Url = currentTab?.Tag?.ToString() ?? string.Empty; GlobalState.CurrentTabState = CurrentTabState;
CurrentTabState.UrlChanged += UrlChanged;
FetchUrl(); if (CurrentTabState.Content is null)
}
}
public StringBuilder Content
{ {
get => content ?? DefaultContent; FetchUrl(true);
set => this.RaiseAndSetIfChanged(ref content, value);
} }
else
{
Content = CurrentTabState.Content;
}
}
}
public TabState CurrentTabState
{
get
{
if (CurrentTab is null)
{
return new();
}
return Dispatcher.UIThread.Invoke(() =>
{
CurrentTab.Tag ??= new TabState();
return (TabState)CurrentTab.Tag;
});
}
}
public WindowState WindowState
{
get => windowState;
set => this.RaiseAndSetIfChanged(ref windowState, value);
}
public ContentViewModelBase Content
{
get => CurrentTabState.Content!;
set
{
CurrentTabState.Content = value;
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(RawContent));
}
}
public string RawContent => string.IsNullOrWhiteSpace(Content.RawContent) ? "No raw content." : Content.RawContent;
public string Url public string Url
{ {
get => GlobalState.Url; get => CurrentTabState.Url;
set => GlobalState.SetUrl(this, value); set => CurrentTabState.SetUrl(value, this);
} }
public bool ShowSidePanel public bool ShowSidePanel
@@ -79,7 +117,7 @@ public partial class MainWindowViewModel : ViewModelBase
set => this.RaiseAndSetIfChanged(ref isBusy, value); set => this.RaiseAndSetIfChanged(ref isBusy, value);
} }
public int Progress public double Progress
{ {
get => progress; get => progress;
set set
@@ -89,21 +127,32 @@ public partial class MainWindowViewModel : ViewModelBase
} }
} }
public double ProgressMax
{
get => progressMax;
set => _ = this.RaiseAndSetIfChanged(ref progressMax, value);
}
public string ProgressText
{
get => progressText;
set => this.RaiseAndSetIfChanged(ref progressText, value);
}
public SettingsViewModel SettingsViewModel => new SettingsViewModel(); public SettingsViewModel SettingsViewModel => new SettingsViewModel();
public RawHtmlViewModel RawHtmlViewModel => new RawHtmlViewModel(html);
public RawMarkdownViewModel RawMarkdownViewModel => new RawMarkdownViewModel(() => Content);
public bool CloseTabEnabled => Tabs.Count > 1; public bool CloseTabEnabled => Tabs.Count > 1;
public bool BackEnabled => GlobalState.BackHistory.Count > 0; public bool BackEnabled => CurrentTabState.BackHistory.Count > 0;
public bool ForwardEnabled => GlobalState.ForwardHistory.Count > 0; public bool ForwardEnabled => CurrentTabState.ForwardHistory.Count > 0;
public bool ProgressIndeterminate => Progress == 0; public bool ProgressIndeterminate => Progress == 0;
public ICommand Browse => ReactiveCommand.Create(FetchUrl); public ICommand Browse => ReactiveCommand.Create(() => FetchUrl(true));
public ICommand Reload => ReactiveCommand.Create(() => FetchUrl(false));
public ICommand Back => ReactiveCommand.Create(() => public ICommand Back => ReactiveCommand.Create(() =>
{ {
if (GlobalState.BackHistory.Count > 0) if (CurrentTabState.BackHistory.Count > 0)
{ {
GlobalState.ForwardHistory.Push(GlobalState.Url); CurrentTabState.ForwardHistory.Push(CurrentTabState.Url);
Url = GlobalState.BackHistory.Pop(); Url = CurrentTabState.BackHistory.Pop();
FetchUrl(); FetchUrl();
this.RaisePropertyChanged(nameof(BackEnabled)); this.RaisePropertyChanged(nameof(BackEnabled));
@@ -113,10 +162,10 @@ public partial class MainWindowViewModel : ViewModelBase
public ICommand Forward => ReactiveCommand.Create(() => public ICommand Forward => ReactiveCommand.Create(() =>
{ {
if (GlobalState.ForwardHistory.Count > 0) if (CurrentTabState.ForwardHistory.Count > 0)
{ {
GlobalState.BackHistory.Push(GlobalState.Url); CurrentTabState.BackHistory.Push(CurrentTabState.Url);
Url = GlobalState.ForwardHistory.Pop(); Url = CurrentTabState.ForwardHistory.Pop();
FetchUrl(); FetchUrl();
this.RaisePropertyChanged(nameof(ForwardEnabled)); this.RaisePropertyChanged(nameof(ForwardEnabled));
@@ -128,41 +177,224 @@ public partial class MainWindowViewModel : ViewModelBase
public ICommand CloseTab => ReactiveCommand.Create(() => public ICommand CloseTab => ReactiveCommand.Create(() =>
{ {
if (IsBusy)
{
WindowNotificationManager.Show(new Notification("Busy", "The browser is currently busy.", NotificationType.Warning));
return;
}
if (Tabs.Count > 1) if (Tabs.Count > 1)
{ {
int currentIndex = Tabs.IndexOf(CurrentTab); int currentIndex = Tabs.IndexOf(CurrentTab);
_ = Tabs.Remove(CurrentTab);
CurrentTab = currentIndex > 0 ? Tabs[currentIndex - 1] : Tabs[0]; CurrentTab = currentIndex > 0 ? Tabs[currentIndex - 1] : Tabs[0];
Tabs.RemoveAt(currentIndex);
this.RaisePropertyChanged(nameof(CloseTabEnabled)); this.RaisePropertyChanged(nameof(CloseTabEnabled));
} }
}); });
public ICommand NewTab => ReactiveCommand.Create(() => public ICommand NewTab => ReactiveCommand.Create(() =>
{ {
if (IsBusy)
{
WindowNotificationManager.Show(new Notification("Busy", "The browser is currently busy.", NotificationType.Warning));
return;
}
TabItem tab = new() { Header = "New Tab", Name = Guid.NewGuid().ToString() }; TabItem tab = new() { Header = "New Tab", Name = Guid.NewGuid().ToString() };
Tabs.Add(tab); Tabs.Add(tab);
CurrentTab = tab; CurrentTab = tab;
this.RaisePropertyChanged(nameof(CloseTabEnabled)); this.RaisePropertyChanged(nameof(CloseTabEnabled));
}); });
private StringBuilder DefaultContent => new StringBuilder() public ICommand ToggleFullScreen => ReactiveCommand.Create(() => WindowState = WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen);
private MarkdownContentViewModel DefaultContent => new("New Tab", new StringBuilder()
.AppendLine($"![Logo](avares://Markdowser/Assets/Markdowser-{(Settings.Current.DarkMode ? "Dark" : "Light")}-Transparent.png)") .AppendLine($"![Logo](avares://Markdowser/Assets/Markdowser-{(Settings.Current.DarkMode ? "Dark" : "Light")}-Transparent.png)")
.AppendLine() .AppendLine()
.AppendLine($"{nameof(Markdowser)} {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}\n") .AppendLine($"{nameof(Markdowser)} {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}\n")
.AppendLine("A markdown web browser.\n") .AppendLine("A markdown web browser.\n")
.AppendLine("[GitHub](https://github.me.stone-red.net/Markdowser)"); .AppendLine("[GitHub](https://github.me.stone-red.net/Markdowser)").ToString());
public MainWindowViewModel() public MainWindowViewModel()
{ {
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"{nameof(Markdowser)}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}");
httpClient.Timeout = TimeSpan.FromSeconds(10); httpClient.Timeout = TimeSpan.FromSeconds(10);
markdownConverter.Config.UnknownTags = ReverseMarkdown.Config.UnknownTagsOption.Bypass; Tabs[0].Tag = new TabState() { Content = DefaultContent };
markdownConverter.Config.SuppressDivNewlines = false;
markdownConverter.Config.SmartHrefHandling = false;
GlobalState.UrlChanged += (sender, url) => contentProcessorManager.RegisterProcessor(new HtmlProcessor());
contentProcessorManager.RegisterProcessor(new CommonImageProcessor());
contentProcessorManager.RegisterProcessor(new DownloadProcessor());
GlobalState.ThemeChanged += (s, e) =>
{
this.RaisePropertyChanged(nameof(Icon));
};
}
private static bool IsValidHttpUri(string uriString, [NotNullWhen(true)] out Uri? uri)
{
return Uri.TryCreate(uriString, UriKind.Absolute, out uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}
private void FetchUrl(bool useCache = true)
{
if (IsBusy)
{
WindowNotificationManager.Show(new Notification("Busy", "The browser is currently busy.", NotificationType.Warning));
return;
}
if (string.IsNullOrWhiteSpace(Url))
{
Content = DefaultContent;
Logger.LogDebug("URL is empty.");
CurrentTab.Header = "New Tab";
return;
}
if (!IsValidHttpUri(Url, out _))
{
if (Url.StartsWith("//"))
{
Url = $"https:{Url}";
}
else
{
// Search with duckduckgo
try
{
Url = string.Format(Settings.Current.SearchEngineUrl, Uri.EscapeDataString(Url));
}
catch (FormatException ex)
{
WindowNotificationManager.Show(new Notification("Invalid Search Engine URL", $"{ex.Message}", NotificationType.Error));
}
}
}
httpClient.DefaultRequestHeaders.UserAgent.Clear();
if (!httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd(Settings.Current.UserAgent))
{
WindowNotificationManager.Show(new Notification("Invalid User Agent", "Failed to set user agent.", NotificationType.Error));
return;
}
IsBusy = true;
Progress = 0;
_ = Task.Run(async () =>
{
Logger.LogInfo($"Fetching URL: {Url}");
try
{
ContentViewModelBase? cachedContent = cacheService.Get(Url);
if (cachedContent is not null && useCache)
{
Content = cachedContent;
}
else
{
HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(Url, HttpCompletionOption.ResponseHeadersRead);
string? newUrl = httpResponseMessage.RequestMessage?.RequestUri?.ToString();
string oldUrl = Url;
if (newUrl is not null && newUrl != Url)
{
Dispatcher.UIThread.Post(() => WindowNotificationManager.Show(new Notification("Redirected", $"Redirected from\n{oldUrl}\nto\n{newUrl}", NotificationType.Warning)));
Url = newUrl;
}
if (!httpResponseMessage.IsSuccessStatusCode)
{
IsBusy = false;
StringBuilder errorMessage = new();
_ = errorMessage.AppendLine($"# {(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}");
_ = errorMessage.AppendLine($"Failed to fetch URL: {httpResponseMessage.ReasonPhrase}");
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
return;
}
Content = await contentProcessorManager.ProcessContent(httpResponseMessage, new Progress<ProcessingProgress>(UpdateProgress));
if (Content.Cacheable)
{
if (oldUrl != Url)
{
cacheService.Set(oldUrl, Content);
}
cacheService.Set(Url, Content);
}
}
Dispatcher.UIThread.Post(() => CurrentTab.Header = Content.Title);
Logger.LogInfo($"Fetched URL: {Url}");
}
catch (HttpRequestException e)
{
StringBuilder errorMessage = new();
if (e.StatusCode is not null)
{
_ = errorMessage.AppendLine($"# {(int)e.StatusCode} {e.StatusCode}");
Dispatcher.UIThread.Post(() => CurrentTab.Header = $"Error: {(int)e.StatusCode} {e.StatusCode}");
}
else
{
Dispatcher.UIThread.Post(() => CurrentTab.Header = $"Error: {e.GetType().Name}");
}
_ = errorMessage.AppendLine($"# {e.HttpRequestError}");
_ = errorMessage.AppendLine($"Failed to fetch URL: {e.Message}");
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
Logger.LogError(e.Message);
}
catch (Exception e)
{
StringBuilder errorMessage = new();
_ = errorMessage.AppendLine($"# {e.GetType().Name}");
_ = errorMessage.AppendLine($"Failed to fetch URL: {e.Message}");
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
Dispatcher.UIThread.Post(() => CurrentTab.Header = $"Error: {e.GetType().Name}");
Logger.LogError(e.Message);
}
finally
{
IsBusy = false;
}
});
}
private void UpdateProgress(ProcessingProgress progress)
{
if (progress.IsBytes)
{
ProgressText = $"{progress.Message} ({{1:0}}%) {BytesToString.Convert((long)Progress)}/{BytesToString.Convert((long)ProgressMax)}";
}
else
{
ProgressText = $"{progress.Message} ({{1:0}}%) {{0}}/{{3}}";
}
ProgressMax = progress.Total;
Progress = progress.Current;
}
private void UrlChanged(object? sender, EventArgs e)
{ {
this.RaisePropertyChanged(nameof(ForwardEnabled)); this.RaisePropertyChanged(nameof(ForwardEnabled));
this.RaisePropertyChanged(nameof(BackEnabled)); this.RaisePropertyChanged(nameof(BackEnabled));
@@ -174,152 +406,5 @@ public partial class MainWindowViewModel : ViewModelBase
} }
FetchUrl(); FetchUrl();
};
GlobalState.ContentReload += (sender, _) =>
{
this.RaisePropertyChanged(nameof(Content));
};
}
private static bool IsValidHttpUri(string uriString, [NotNullWhen(true)] out Uri? uri)
{
return Uri.TryCreate(uriString, UriKind.Absolute, out uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}
[GeneratedRegex("!\\[(.*?)\\]\\((.*?)\\)")]
private static partial Regex MarkdownImage();
private void FetchUrl()
{
if (string.IsNullOrWhiteSpace(Url))
{
content = null;
_ = html.Clear();
ContentChanged();
Debug.WriteLine("URL is empty.");
CurrentTab.Header = "New Tab";
return;
}
if (!IsValidHttpUri(Url, out _))
{
// Search with duckduckgo
Url = $"https://duckduckgo.com/html/?kd=-1&k1=-1&q={Uri.EscapeDataString(Url)}";
}
content ??= new StringBuilder();
_ = content.Clear();
_ = html.Clear();
IsBusy = true;
Progress = 0;
_ = Task.Run(async () =>
{
Debug.WriteLine("Fetching URL...");
try
{
HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(Url);
Url = httpResponseMessage.RequestMessage?.RequestUri?.ToString() ?? Url;
if (!httpResponseMessage.IsSuccessStatusCode)
{
IsBusy = false;
_ = content.AppendLine($"# {(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}");
_ = content.AppendLine($"Failed to fetch URL: {httpResponseMessage.ReasonPhrase}");
ContentChanged();
return;
}
long length = httpResponseMessage.Content.Headers.ContentLength ?? 0;
using Stream contentStream = await httpResponseMessage.Content.ReadAsStreamAsync();
using StreamReader streamReader = new(contentStream);
while (!streamReader.EndOfStream)
{
_ = html.AppendLine(await streamReader.ReadLineAsync());
Progress = (int)((double)contentStream.Position / length * 100);
}
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html.ToString());
string title = htmlDoc.DocumentNode.SelectSingleNode("html/head/title")?.InnerText ?? Url;
Dispatcher.UIThread.Post(() =>
{
CurrentTab.Header = title?.Trim() ?? Url;
});
}
catch (HttpRequestException e)
{
IsBusy = false;
if (e.StatusCode is not null)
{
_ = content.AppendLine($"# {(int)e.StatusCode} {e.StatusCode}");
}
_ = content.AppendLine($"# {e.HttpRequestError}");
_ = content.AppendLine($"Failed to fetch URL: {e.Message}");
ContentChanged();
}
catch (Exception e)
{
IsBusy = false;
_ = content.AppendLine($"# {e.GetType().Name}");
_ = content.AppendLine($"Failed to fetch URL: {e.Message}");
ContentChanged();
}
Progress = 0;
Debug.WriteLine("Converting HTML to markdown...");
string markdown = markdownConverter.Convert(html.ToString());
Debug.WriteLine("Processing markdown...");
int currentLine = 0;
string[] lines = markdown.Split('\n');
foreach (string line in lines)
{
Progress = (int)((double)currentLine / lines.Length * 100);
currentLine++;
string trimmedLine = line.Trim();
if (trimmedLine.All(c => c == '#'))
{
_ = Content.Append(trimmedLine);
continue;
}
else if (MarkdownImage().IsMatch(trimmedLine))
{
_ = content.AppendLine()
.Append(trimmedLine)
.AppendLine()
.AppendLine();
continue;
}
_ = content.AppendLine(trimmedLine);
}
ContentChanged();
IsBusy = false;
});
}
private void ContentChanged()
{
this.RaisePropertyChanged(nameof(Content));
this.RaisePropertyChanged(nameof(RawHtmlViewModel));
this.RaisePropertyChanged(nameof(RawMarkdownViewModel));
} }
} }
@@ -1,8 +0,0 @@
using System.Text;
namespace Markdowser.ViewModels;
public class RawHtmlViewModel(StringBuilder html) : ViewModelBase
{
public StringBuilder Html => html;
}
@@ -1,9 +0,0 @@
using System;
using System.Text;
namespace Markdowser.ViewModels;
public class RawMarkdownViewModel(Func<StringBuilder> markdown) : ViewModelBase
{
public StringBuilder Markdown => markdown();
}
@@ -10,6 +10,18 @@ public class SettingsViewModel : ViewModelBase
set => Settings.Current.HomeUrl = value; set => Settings.Current.HomeUrl = value;
} }
public string SearchEngineUrl
{
get => Settings.Current.SearchEngineUrl;
set => Settings.Current.SearchEngineUrl = value;
}
public string UserAgent
{
get => Settings.Current.UserAgent;
set => Settings.Current.UserAgent = value;
}
public bool DarkMode public bool DarkMode
{ {
get => Settings.Current.DarkMode; get => Settings.Current.DarkMode;
+47 -1
View File
@@ -1,6 +1,52 @@
using ReactiveUI; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Platform;
using CuteUtils.Logging;
using Markdowser.Models;
using Markdowser.Utilities;
using ReactiveUI;
using System.Diagnostics;
using System.Reflection;
namespace Markdowser.ViewModels; namespace Markdowser.ViewModels;
public class ViewModelBase : ReactiveObject public class ViewModelBase : ReactiveObject
{ {
private WindowNotificationManager? windowNotificationManager;
public string Title
{
get
{
string title = nameof(Markdowser);
Assembly assembly = Assembly.GetExecutingAssembly();
FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location);
string? version = fileVersionInfo.ProductVersion;
#if DEBUG
return $"{title} - Dev {version}";
#else
return $"{title} - {version}";
#endif
}
}
public WindowIcon Icon => Settings.Current.DarkMode ? new WindowIcon(AssetLoader.Open(new("avares://Markdowser/Assets/Markdowser-Dark-Transparent.ico"))) : new WindowIcon(AssetLoader.Open(new("avares://Markdowser/Assets/Markdowser-Light-Transparent.ico")));
public WindowNotificationManager WindowNotificationManager => windowNotificationManager!;
public Logger Logger => GlobalState.Logger;
protected internal void InitializeWindowNotificationManager(Window window)
{
TopLevel? topLevel = TopLevel.GetTopLevel(window);
windowNotificationManager = new WindowNotificationManager(topLevel)
{
Position = NotificationPosition.TopRight
};
}
} }
@@ -0,0 +1,18 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Markdowser.ViewModels"
xmlns:cmd="using:Markdowser.Commands"
xmlns:utils="using:Markdowser.Utilities"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:md="https://github.com/whistyun/Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
xmlns:content="clr-namespace:Markdowser.ViewModels.Content"
x:DataType="content:CommonImageContentViewModel"
x:Class="Markdowser.Views.Content.CommonImageContentView">
<Grid>
<Image Source="{Binding Image}"/>
</Grid>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Markdowser.Views.Content;
public partial class CommonImageContentView : UserControl
{
public CommonImageContentView()
{
InitializeComponent();
}
}
@@ -0,0 +1,72 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Markdowser.ViewModels"
xmlns:cmd="using:Markdowser.Commands"
xmlns:utils="using:Markdowser.Utilities"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:md="https://github.com/whistyun/Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
xmlns:content="clr-namespace:Markdowser.ViewModels.Content"
x:DataType="content:MarkdownContentViewModel"
x:Class="Markdowser.Views.Content.MarkdownContentView">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<utils:HttpPathResolver x:Key="HttpPathResolver" />
<cmd:HyperlinkCommand x:Key="HyperlinkCommand" />
<cmd:ChangeThemeCommand x:Key="ChangeThemeCommand" />
<cmd:HomeCommand x:Key="HomeCommand" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<md:MarkdownScrollViewer Margin="10 0 10 0" Markdown="{Binding Markdown}">
<md:MarkdownScrollViewer.Styles>
<Style Selector="ctxt|CTextBlock">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading1">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading2">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading3">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading4">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading5">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading6">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CHyperlink">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLink}" />
</Style>
<Style Selector="ctxt|CHyperlink:pointerover">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLinkPointerover}" />
</Style>
<Style Selector="ctxt|CHyperlink:pressed">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLinkVisited}" />
</Style>
<Style Selector="ctxt|CImage">
<Setter Property="FittingWhenProtrude" Value="True" />
<Setter Property="LayoutWidth" Value="500" />
<Setter Property="SaveAspectRatio" Value="True" />
</Style>
</md:MarkdownScrollViewer.Styles>
<md:MarkdownScrollViewer.Plugins>
<md:MdAvPlugins PathResolver="{StaticResource HttpPathResolver}" HyperlinkCommand="{StaticResource HyperlinkCommand}" />
</md:MarkdownScrollViewer.Plugins>
</md:MarkdownScrollViewer>
</Grid>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Markdowser.Views.Content;
public partial class MarkdownContentView : UserControl
{
public MarkdownContentView()
{
InitializeComponent();
}
}
+50 -64
View File
@@ -5,14 +5,13 @@
xmlns:utils="using:Markdowser.Utilities" xmlns:utils="using:Markdowser.Utilities"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:md="https://github.com/whistyun/Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
xmlns:i="https://github.com/projektanker/icons.avalonia" xmlns:i="https://github.com/projektanker/icons.avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Markdowser.Views.MainWindow" x:Class="Markdowser.Views.MainWindow"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
Icon="/Assets/Markdowser-Light-Transparent.ico" Icon="{Binding Icon}"
Title="{Binding Title}"> Title="{Binding Title}"
Loaded="Window_Loaded" WindowState="{Binding WindowState}">
<Design.DataContext> <Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE, <!-- This only sets the DataContext for the previewer in an IDE,
@@ -24,8 +23,6 @@
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary> <ResourceDictionary>
<utils:HttpPathResolver x:Key="HttpPathResolver" />
<cmd:HyperlinkCommand x:Key="HyperlinkCommand" />
<cmd:ChangeThemeCommand x:Key="ChangeThemeCommand" /> <cmd:ChangeThemeCommand x:Key="ChangeThemeCommand" />
<cmd:HomeCommand x:Key="HomeCommand" /> <cmd:HomeCommand x:Key="HomeCommand" />
</ResourceDictionary> </ResourceDictionary>
@@ -33,11 +30,31 @@
</ResourceDictionary> </ResourceDictionary>
</Window.Resources> </Window.Resources>
<Window.KeyBindings>
<KeyBinding Gesture="Ctrl+R" Command="{Binding Reload}" />
<KeyBinding Gesture="F5" Command="{Binding Reload}" />
<KeyBinding Gesture="Alt+Home" Command="{StaticResource HomeCommand}" />
<KeyBinding Gesture="Alt+H" Command="{StaticResource HomeCommand}" />
<KeyBinding Gesture="Alt+Left" Command="{Binding Back}" />
<KeyBinding Gesture="Alt+Right" Command="{Binding Forward}" />
<KeyBinding Gesture="Ctrl+T" Command="{Binding NewTab}" />
<KeyBinding Gesture="Ctrl+W" Command="{Binding CloseTab}" />
<KeyBinding Gesture="Alt+S" Command="{Binding ToggleSidePanel}" />
<KeyBinding Gesture="F11" Command="{Binding ToggleFullScreen}" />
</Window.KeyBindings>
<Grid RowDefinitions="Auto, *, Auto"> <Grid RowDefinitions="Auto, *, Auto">
<Grid Grid.Row="0" ColumnDefinitions="*, Auto, Auto"> <Grid Grid.Row="0" ColumnDefinitions="*, Auto, Auto">
<TabControl Grid.Column="0" Theme="{DynamicResource ScrollTabControl}" SelectedItem="{Binding CurrentTab, Mode=TwoWay}" ItemsSource="{Binding Tabs}" /> <TabControl Grid.Column="0" SelectedItem="{Binding CurrentTab, Mode=TwoWay}" ItemsSource="{Binding Tabs}" IsEnabled="{Binding !IsBusy}" />
<Button Grid.Column="1" Command="{Binding CloseTab}" IsEnabled="{Binding CloseTabEnabled}" HorizontalAlignment="Right" VerticalAlignment="Top" Height="32" i:Attached.Icon="fa-solid fa-xmark" /> <Button Grid.Column="1" Command="{Binding CloseTab}" HorizontalAlignment="Right" VerticalAlignment="Stretch" i:Attached.Icon="fa-solid fa-xmark">
<Button Grid.Column="2" Command="{Binding NewTab}" HorizontalAlignment="Right" VerticalAlignment="Top" Height="32" i:Attached.Icon="fa-solid fa-plus" /> <Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="CloseTabEnabled" />
<Binding Path="!IsBusy" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<Button Grid.Column="2" Command="{Binding NewTab}" HorizontalAlignment="Right" VerticalAlignment="Stretch" i:Attached.Icon="fa-solid fa-plus" IsEnabled="{Binding !IsBusy}" />
</Grid> </Grid>
<Grid Grid.Row="1"> <Grid Grid.Row="1">
@@ -47,48 +64,8 @@
<ColumnDefinition Width="*" utils:ColumnDefinition.IsVisible="{Binding ShowSidePanel}" /> <ColumnDefinition Width="*" utils:ColumnDefinition.IsVisible="{Binding ShowSidePanel}" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<md:MarkdownScrollViewer Margin="10 0 10 10" IsEnabled="{Binding !IsBusy}" Markdown="{Binding Content}"> <ContentControl IsEnabled="{Binding !IsBusy}" Content="{Binding Content}" />
<md:MarkdownScrollViewer.Styles>
<Style Selector="ctxt|CTextBlock">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading1">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading2">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading3">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading4">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading5">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading6">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CHyperlink">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLink}" />
</Style>
<Style Selector="ctxt|CHyperlink:pointerover">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLinkPointerover}" />
</Style>
<Style Selector="ctxt|CHyperlink:pressed">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLinkVisited}" />
</Style>
<Style Selector="ctxt|CImage">
<Setter Property="FittingWhenProtrude" Value="True" />
<Setter Property="LayoutWidth" Value="500" />
<Setter Property="SaveAspectRatio" Value="True" />
</Style>
</md:MarkdownScrollViewer.Styles>
<md:MarkdownScrollViewer.Plugins>
<md:MdAvPlugins PathResolver="{StaticResource HttpPathResolver}" HyperlinkCommand="{StaticResource HyperlinkCommand}" />
</md:MarkdownScrollViewer.Plugins>
</md:MarkdownScrollViewer>
<GridSplitter Grid.Column="1" IsVisible="{Binding ShowSidePanel}" Background="{DynamicResource SemiColorTertiary}" ResizeDirection="Columns" /> <GridSplitter Grid.Column="1" IsVisible="{Binding ShowSidePanel}" Background="{DynamicResource SemiColorTertiary}" ResizeDirection="Columns" />
<TabControl Grid.Column="2" IsVisible="{Binding ShowSidePanel}" Margin="10"> <TabControl Grid.Column="2" IsVisible="{Binding ShowSidePanel}" Margin="10">
<TabItem Header="Settings"> <TabItem Header="Settings">
@@ -96,14 +73,9 @@
<ContentControl Margin="0 10 0 0" Content="{Binding SettingsViewModel}" /> <ContentControl Margin="0 10 0 0" Content="{Binding SettingsViewModel}" />
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
<TabItem Header="Markdown"> <TabItem Header="Raw Content">
<ScrollViewer> <ScrollViewer>
<ContentControl Margin="0 10 0 0" Content="{Binding RawMarkdownViewModel}" /> <SelectableTextBlock Margin="0 10 0 0" Text="{Binding RawContent}" TextWrapping="Wrap" />
</ScrollViewer>
</TabItem>
<TabItem Header="HTML">
<ScrollViewer>
<ContentControl Margin="0 10 0 0" Content="{Binding RawHtmlViewModel}" />
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
</TabControl> </TabControl>
@@ -111,13 +83,27 @@
<Border Grid.Row="2" BorderThickness="0 2 0 0" CornerRadius="0" BorderBrush="{DynamicResource SemiColorTertiary}"> <Border Grid.Row="2" BorderThickness="0 2 0 0" CornerRadius="0" BorderBrush="{DynamicResource SemiColorTertiary}">
<Grid ColumnDefinitions="Auto, Auto, Auto, Auto, *, Auto"> <Grid ColumnDefinitions="Auto, Auto, Auto, Auto, *, Auto">
<Button Grid.Column="0" i:Attached.Icon="fa-solid fa-caret-left" IsEnabled="{Binding BackEnabled}" Command="{Binding Back}" /> <Button Grid.Column="0" i:Attached.Icon="fa-solid fa-caret-left" Command="{Binding Back}">
<Button Grid.Column="1" i:Attached.Icon="fa-solid fa-caret-right" IsEnabled="{Binding ForwardEnabled}" Command="{Binding Forward}" /> <Button.IsEnabled>
<Button Grid.Column="2" i:Attached.Icon="fa-solid fa-rotate-right" Command="{Binding Browse}" /> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Button Grid.Column="3" i:Attached.Icon="fa-solid fa-house" Command="{StaticResource HomeCommand}" /> <Binding Path="BackEnabled" />
<Binding Path="!IsBusy" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<Button Grid.Column="1" i:Attached.Icon="fa-solid fa-caret-right" Command="{Binding Forward}">
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="ForwardEnabled" />
<Binding Path="!IsBusy" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<Button Grid.Column="2" i:Attached.Icon="fa-solid fa-rotate-right" Command="{Binding Reload}" IsEnabled="{Binding !IsBusy}" />
<Button Grid.Column="3" i:Attached.Icon="fa-solid fa-house" Command="{StaticResource HomeCommand}" IsEnabled="{Binding !IsBusy}" />
<Grid Grid.Column="4"> <Grid Grid.Column="4">
<TextBox x:Name="urlBar" IsVisible="{Binding !IsBusy}" Watermark="Search DuckDuckGo or type a URL" Text="{Binding Url}" BorderThickness="0"> <TextBox IsVisible="{Binding !IsBusy}" Watermark="Search or type a URL" Text="{Binding Url}" BorderThickness="0">
<TextBox.KeyBindings> <TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding Browse}" /> <KeyBinding Gesture="Enter" Command="{Binding Browse}" />
</TextBox.KeyBindings> </TextBox.KeyBindings>
@@ -128,7 +114,7 @@
</TextBox.Styles> </TextBox.Styles>
</TextBox> </TextBox>
<ProgressBar IsVisible="{Binding IsBusy}" IsIndeterminate="{Binding ProgressIndeterminate}" ShowProgressText="{Binding !ProgressIndeterminate}" Maximum="100" Height="32" Grid.Row="2" Value="{Binding Progress}" /> <ProgressBar IsVisible="{Binding IsBusy}" IsIndeterminate="{Binding ProgressIndeterminate}" ShowProgressText="{Binding !ProgressIndeterminate}" ProgressTextFormat="{Binding ProgressText}" Height="32" Grid.Row="2" Value="{Binding Progress}" Maximum="{Binding ProgressMax}" />
</Grid> </Grid>
<Button Grid.Column="5" Command="{Binding ToggleSidePanel}" HorizontalAlignment="Right" i:Attached.Icon="fa-solid fa-table-columns" /> <Button Grid.Column="5" Command="{Binding ToggleSidePanel}" HorizontalAlignment="Right" i:Attached.Icon="fa-solid fa-table-columns" />
+8
View File
@@ -8,4 +8,12 @@ public partial class MainWindow : Window
{ {
InitializeComponent(); InitializeComponent();
} }
private void Window_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (DataContext is ViewModels.MainWindowViewModel vm)
{
vm.InitializeWindowNotificationManager(this);
}
}
} }
-10
View File
@@ -1,10 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Markdowser.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:RawHtmlViewModel"
x:Class="Markdowser.Views.RawHtmlView">
<SelectableTextBlock Text="{Binding Html}" TextWrapping="Wrap" />
</UserControl>
-11
View File
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace Markdowser.Views;
public partial class RawHtmlView : UserControl
{
public RawHtmlView()
{
InitializeComponent();
}
}
@@ -1,10 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Markdowser.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:RawMarkdownViewModel"
x:Class="Markdowser.Views.RawMarkdownView">
<SelectableTextBlock Text="{Binding Markdown}" TextWrapping="Wrap" />
</UserControl>
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace Markdowser.Views;
public partial class RawMarkdownView : UserControl
{
public RawMarkdownView()
{
InitializeComponent();
}
}
+6
View File
@@ -23,7 +23,13 @@
<TextBlock Text="Dark Mode" /> <TextBlock Text="Dark Mode" />
<ToggleSwitch IsChecked="{Binding DarkMode}" Command="{StaticResource ChangeThemeCommand}" /> <ToggleSwitch IsChecked="{Binding DarkMode}" Command="{StaticResource ChangeThemeCommand}" />
<TextBlock Text="Search Engine URL" />
<TextBox Text="{Binding SearchEngineUrl}" Watermark="URL with {0} as encoded search query" />
<TextBlock Text="Home URL" /> <TextBlock Text="Home URL" />
<TextBox Text="{Binding HomeUrl}" Watermark="URL, or leave empty to use default" /> <TextBox Text="{Binding HomeUrl}" Watermark="URL, or leave empty to use default" />
<TextBlock Text="User Agent" />
<TextBox Text="{Binding UserAgent}" Watermark="User agent string" />
</StackPanel> </StackPanel>
</UserControl> </UserControl>