43 Commits
Author SHA1 Message Date
Stone_Red e27a195781 Change command name 2024-06-23 13:43:20 +02:00
Stone_Red 15a46f392e Fix summary in snapcraft.yaml 2024-06-23 13:32:06 +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 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 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 1706 additions and 351 deletions
+140
View File
@@ -396,3 +396,143 @@ FodyWeavers.xsd
# JetBrains Rider
*.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
+12 -6
View File
@@ -8,8 +8,10 @@ It uses the [ReverseMarkdown](https://github.com/mysticmind/reversemarkdown-net)
## Setup
1. Download one of the [releases](https://github.com/Stone-Red-Code/Markdowser/releases).
1. Execute the `Markdowser-Installer.exe` file.
1. Download the latest release
- 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. Enjoy!
@@ -19,12 +21,16 @@ It uses the [ReverseMarkdown](https://github.com/mysticmind/reversemarkdown-net)
## Preview
![Screenshot 2024-04-03 173609](https://github.com/Stone-Red-Code/Markdowser/assets/56473591/e7a1b9f2-a416-448b-9e54-a91e5794ffb3)
![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/c57414b7-05e2-45af-868a-e0df61d885c4)
![Markdowser](https://github.com/Stone-Red-Code/Markdowser/assets/56473591/eae99633-5d4e-4e8e-9904-708ad7fd2bb8)
## 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)
- [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)
+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.Styling;
using CuteUtils.Logging;
using Markdowser.Models;
using Markdowser.Utilities;
using Markdowser.ViewModels;
using Markdowser.Views;
using System.IO;
namespace Markdowser;
public partial class App : Application
@@ -18,6 +23,11 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
if (!Directory.Exists(Configuration.ApplicationDataPath))
{
_ = Directory.CreateDirectory(Configuration.ApplicationDataPath);
}
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
@@ -25,12 +35,23 @@ public partial class App : Application
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) =>
{
GlobalState.Logger.LogInfo("Saving settings...");
Settings.SaveSettings();
GlobalState.Logger.LogInfo("Settings saved.");
};
}
GlobalState.Logger.LogInfo("Application started.");
base.OnFrameworkInitializationCompleted();
if (Current is not null)
@@ -30,7 +30,7 @@ internal class ChangeThemeCommand : ICommand
ThemeVariant theme = app.ActualThemeVariant;
app.RequestedThemeVariant = theme == ThemeVariant.Dark ? ThemeVariant.Light : 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)
{
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.Diagnostics;
using System.IO;
using System.Windows.Input;
namespace Markdowser.Commands;
@@ -23,19 +24,35 @@ public class HyperlinkCommand : ICommand
{
if (parameter is string url)
{
Debug.WriteLine($"Hyperlink clicked: {url}");
GlobalState.Logger.LogDebug($"Hyperlink clicked: {url}");
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
GlobalState.Url = url;
GlobalState.CurrentTabState.Url = url;
}
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
{
Debug.WriteLine($"Invalid URL: {url}");
GlobalState.Logger.LogDebug($"Invalid URL: {url}");
}
}
}
+25 -12
View File
@@ -6,7 +6,8 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Version>0.1.0.0</Version>
<Version>0.2.0.0</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
<ItemGroup>
@@ -23,18 +24,30 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.10" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
<PackageReference Include="Avalonia" Version="11.0.11" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.11" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.11" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.11" />
<!--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 Include="Avalonia.ReactiveUI" Version="11.0.10" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.11" />
<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="Projektanker.Icons.Avalonia" Version="9.1.2" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.1.2" />
<PackageReference Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.1.2" />
<PackageReference Include="ReverseMarkdown" Version="4.3.0" />
<PackageReference Include="Semi.Avalonia" Version="11.0.7.1" />
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.3.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.3.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.3.0" />
<PackageReference Include="ReverseMarkdown" Version="4.5.0" />
<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>
</Project>
+15 -1
View File
@@ -1,6 +1,8 @@
using Markdowser.Utilities;
using System;
using System.IO;
using System.Reflection;
using System.Text.Json;
namespace Markdowser.Models;
@@ -11,8 +13,12 @@ public class Settings
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 UserAgent { get; set; } = $"{Assembly.GetExecutingAssembly().GetName().Name}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}";
public static void SaveSettings()
{
if (!Directory.Exists(Configuration.ApplicationDataPath))
@@ -27,7 +33,15 @@ public class Settings
{
if (File.Exists(Configuration.SettingsFilePath))
{
return JsonSerializer.Deserialize<Settings>(File.ReadAllText(Configuration.SettingsFilePath)) ?? new Settings();
try
{
return JsonSerializer.Deserialize<Settings>(File.ReadAllText(Configuration.SettingsFilePath)) ?? new Settings();
}
catch (Exception ex)
{
GlobalState.Logger.LogWarn(ex.Message);
return new Settings();
}
}
else
{
+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();
}
+30 -2
View File
@@ -1,11 +1,17 @@
using Avalonia;
using Avalonia.ReactiveUI;
using Markdowser.Utilities;
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome;
using Projektanker.Icons.Avalonia.MaterialDesign;
using ReactiveUI;
using System;
using System.Reactive;
using System.Threading.Tasks;
namespace Markdowser;
@@ -17,8 +23,30 @@ internal static class Program
[STAThread]
public static void Main(string[] args)
{
_ = BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(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()
.StartWithClassicDesktopLifetime(args);
}
catch (Exception ex)
{
GlobalState.Logger.LogFatal(ex.ToString());
}
}
// Avalonia configuration, don't remove; also used by visual designer.
+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)
{
_ = 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);
}
return visibility;
+30 -3
View File
@@ -1,12 +1,39 @@
using System;
using Syroot.Windows.IO;
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Markdowser.Utilities;
internal static class Configuration
{
public static string DownloadPath => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
public static string ApplicationDataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(Markdowser));
public static string DownloadPath
{
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 LogFilePath => Path.Combine(ApplicationDataPath, $"{nameof(Markdowser)}.log");
}
+37 -33
View File
@@ -1,51 +1,55 @@
using Avalonia.Controls;
using CuteUtils.Logging;
using Markdowser.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
namespace Markdowser.Utilities;
[SuppressMessage("Major Code Smell", "S4220:Events should have proper arguments", Justification = "<Pending>")]
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 Stack<string> BackHistory { get; } = new();
public static Stack<string> ForwardHistory { get; } = new();
public static string Url
public static Logger Logger { get; } = new()
{
get => url;
set
Config = new()
{
if (url != value)
DebugConfig = new()
{
BackHistory.Push(url);
ForwardHistory.Clear();
LogTarget = LogTarget.DebugConsole
},
InfoConfig = new()
{
LogTarget = LogTarget.DebugConsole | LogTarget.File,
FilePath = Configuration.LogFilePath
},
WarnConfig = new()
{
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(),
}
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)
public static void InvokeThemeChanged()
{
GlobalState.url = url;
UrlChanged?.Invoke(sender, url);
}
internal static void ReloadContent(object sender)
{
ContentReload?.Invoke(sender, EventArgs.Empty);
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 Markdowser.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
@@ -31,15 +36,27 @@ public class HttpPathResolver : IPathResolver
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)
{
ShowError($"Failed to fetch image: {httpResponseMessage.ReasonPhrase}\n{relativeOrAbsolutePath}");
return GetLogo();
}
@@ -55,4 +72,14 @@ public class HttpPathResolver : IPathResolver
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;
namespace Markdowser;
public class ViewLocator : IDataTemplate
{
public Control? Build(object? data)
{
if (data is null)
{
return null;
}
var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
string name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
Type? type = Type.GetType(name);
if (type != null)
{
var control = (Control)Activator.CreateInstance(type)!;
Control control = (Control)Activator.CreateInstance(type)!;
control.DataContext = data;
return control;
}
@@ -29,6 +31,6 @@ public class ViewLocator : IDataTemplate
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;
}
+235 -150
View File
@@ -1,23 +1,21 @@
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using HtmlAgilityPack;
using Markdowser.Models;
using Markdowser.Processing;
using Markdowser.Processing.Processors;
using Markdowser.Utilities;
using Markdowser.ViewModels.Content;
using ReactiveUI;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Input;
@@ -26,45 +24,85 @@ namespace Markdowser.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
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 WindowState windowState;
private bool showSidePanel;
private bool isBusy;
private int progress;
public ObservableCollection<TabItem> Tabs => GlobalState.Tabs;
public string Title => $"{nameof(Markdowser)} - {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}";
private double progress;
private double progressMax;
private string progressText;
public ObservableCollection<TabItem> Tabs { get; } = [new TabItem() { Header = "New Tab" }];
public TabItem CurrentTab
{
get => currentTab;
set
{
if (currentTab is not null)
{
currentTab.Tag = Url;
}
CurrentTabState.UrlChanged -= UrlChanged;
_ = 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)
{
FetchUrl(true);
}
else
{
Content = CurrentTabState.Content;
}
}
}
public StringBuilder Content
public TabState CurrentTabState
{
get => content ?? DefaultContent;
set => this.RaiseAndSetIfChanged(ref content, value);
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
{
get => GlobalState.Url;
set => GlobalState.SetUrl(this, value);
get => CurrentTabState.Url;
set => CurrentTabState.SetUrl(value, this);
}
public bool ShowSidePanel
@@ -79,7 +117,7 @@ public partial class MainWindowViewModel : ViewModelBase
set => this.RaiseAndSetIfChanged(ref isBusy, value);
}
public int Progress
public double Progress
{
get => progress;
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 RawHtmlViewModel RawHtmlViewModel => new RawHtmlViewModel(html);
public RawMarkdownViewModel RawMarkdownViewModel => new RawMarkdownViewModel(() => Content);
public bool CloseTabEnabled => Tabs.Count > 1;
public bool BackEnabled => GlobalState.BackHistory.Count > 0;
public bool ForwardEnabled => GlobalState.ForwardHistory.Count > 0;
public bool BackEnabled => CurrentTabState.BackHistory.Count > 0;
public bool ForwardEnabled => CurrentTabState.ForwardHistory.Count > 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(() =>
{
if (GlobalState.BackHistory.Count > 0)
if (CurrentTabState.BackHistory.Count > 0)
{
GlobalState.ForwardHistory.Push(GlobalState.Url);
Url = GlobalState.BackHistory.Pop();
CurrentTabState.ForwardHistory.Push(CurrentTabState.Url);
Url = CurrentTabState.BackHistory.Pop();
FetchUrl();
this.RaisePropertyChanged(nameof(BackEnabled));
@@ -113,10 +162,10 @@ public partial class MainWindowViewModel : ViewModelBase
public ICommand Forward => ReactiveCommand.Create(() =>
{
if (GlobalState.ForwardHistory.Count > 0)
if (CurrentTabState.ForwardHistory.Count > 0)
{
GlobalState.BackHistory.Push(GlobalState.Url);
Url = GlobalState.ForwardHistory.Pop();
CurrentTabState.BackHistory.Push(CurrentTabState.Url);
Url = CurrentTabState.ForwardHistory.Pop();
FetchUrl();
this.RaisePropertyChanged(nameof(ForwardEnabled));
@@ -128,57 +177,60 @@ public partial class MainWindowViewModel : ViewModelBase
public ICommand CloseTab => ReactiveCommand.Create(() =>
{
if (IsBusy)
{
WindowNotificationManager.Show(new Notification("Busy", "The browser is currently busy.", NotificationType.Warning));
return;
}
if (Tabs.Count > 1)
{
int currentIndex = Tabs.IndexOf(CurrentTab);
_ = Tabs.Remove(CurrentTab);
CurrentTab = currentIndex > 0 ? Tabs[currentIndex - 1] : Tabs[0];
Tabs.RemoveAt(currentIndex);
this.RaisePropertyChanged(nameof(CloseTabEnabled));
}
});
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() };
Tabs.Add(tab);
CurrentTab = tab;
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()
.AppendLine($"{nameof(Markdowser)} {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}\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()
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"{nameof(Markdowser)}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}");
httpClient.Timeout = TimeSpan.FromSeconds(10);
markdownConverter.Config.UnknownTags = ReverseMarkdown.Config.UnknownTagsOption.Bypass;
markdownConverter.Config.SuppressDivNewlines = false;
markdownConverter.Config.SmartHrefHandling = false;
Tabs[0].Tag = new TabState() { Content = DefaultContent };
GlobalState.UrlChanged += (sender, url) =>
contentProcessorManager.RegisterProcessor(new HtmlProcessor());
contentProcessorManager.RegisterProcessor(new CommonImageProcessor());
contentProcessorManager.RegisterProcessor(new DownloadProcessor());
GlobalState.ThemeChanged += (s, e) =>
{
this.RaisePropertyChanged(nameof(ForwardEnabled));
this.RaisePropertyChanged(nameof(BackEnabled));
this.RaisePropertyChanged(nameof(Url));
if (sender == this)
{
return;
}
FetchUrl();
};
GlobalState.ContentReload += (sender, _) =>
{
this.RaisePropertyChanged(nameof(Content));
this.RaisePropertyChanged(nameof(Icon));
};
}
@@ -187,139 +239,172 @@ public partial class MainWindowViewModel : ViewModelBase
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()
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 = null;
_ = html.Clear();
ContentChanged();
Debug.WriteLine("URL is empty.");
Content = DefaultContent;
Logger.LogDebug("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)}";
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));
}
}
}
content ??= new StringBuilder();
_ = content.Clear();
_ = html.Clear();
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 () =>
{
Debug.WriteLine("Fetching URL...");
Logger.LogInfo($"Fetching URL: {Url}");
try
{
HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(Url);
Url = httpResponseMessage.RequestMessage?.RequestUri?.ToString() ?? Url;
if (!httpResponseMessage.IsSuccessStatusCode)
ContentViewModelBase? cachedContent = cacheService.Get(Url);
if (cachedContent is not null && useCache)
{
IsBusy = false;
_ = content.AppendLine($"# {(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}");
_ = content.AppendLine($"Failed to fetch URL: {httpResponseMessage.ReasonPhrase}");
ContentChanged();
return;
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);
}
}
long length = httpResponseMessage.Content.Headers.ContentLength ?? 0;
Dispatcher.UIThread.Post(() => CurrentTab.Header = Content.Title);
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;
});
Logger.LogInfo($"Fetched URL: {Url}");
}
catch (HttpRequestException e)
{
IsBusy = false;
StringBuilder errorMessage = new();
if (e.StatusCode is not null)
{
_ = content.AppendLine($"# {(int)e.StatusCode} {e.StatusCode}");
_ = 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}");
}
_ = content.AppendLine($"# {e.HttpRequestError}");
_ = content.AppendLine($"Failed to fetch URL: {e.Message}");
ContentChanged();
_ = 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)
{
IsBusy = false;
_ = content.AppendLine($"# {e.GetType().Name}");
_ = content.AppendLine($"Failed to fetch URL: {e.Message}");
ContentChanged();
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);
}
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)
finally
{
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);
IsBusy = false;
}
ContentChanged();
IsBusy = false;
});
}
private void ContentChanged()
private void UpdateProgress(ProcessingProgress progress)
{
this.RaisePropertyChanged(nameof(Content));
this.RaisePropertyChanged(nameof(RawHtmlViewModel));
this.RaisePropertyChanged(nameof(RawMarkdownViewModel));
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(BackEnabled));
this.RaisePropertyChanged(nameof(Url));
if (sender == this)
{
return;
}
FetchUrl();
}
}
@@ -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;
}
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
{
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;
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:d="http://schemas.microsoft.com/expression/blend/2008"
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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Markdowser.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/Markdowser-Light-Transparent.ico"
Title="{Binding Title}">
Icon="{Binding Icon}"
Title="{Binding Title}"
Loaded="Window_Loaded" WindowState="{Binding WindowState}">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
@@ -24,8 +23,6 @@
<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>
@@ -33,11 +30,31 @@
</ResourceDictionary>
</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 Grid.Row="0" ColumnDefinitions="*, Auto, Auto">
<TabControl Grid.Column="0" Theme="{DynamicResource ScrollTabControl}" SelectedItem="{Binding CurrentTab, Mode=TwoWay}" ItemsSource="{Binding Tabs}" />
<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="2" Command="{Binding NewTab}" HorizontalAlignment="Right" VerticalAlignment="Top" Height="32" i:Attached.Icon="fa-solid fa-plus" />
<TabControl Grid.Column="0" SelectedItem="{Binding CurrentTab, Mode=TwoWay}" ItemsSource="{Binding Tabs}" IsEnabled="{Binding !IsBusy}" />
<Button Grid.Column="1" Command="{Binding CloseTab}" HorizontalAlignment="Right" VerticalAlignment="Stretch" i:Attached.Icon="fa-solid fa-xmark">
<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.Row="1">
@@ -47,48 +64,8 @@
<ColumnDefinition Width="*" utils:ColumnDefinition.IsVisible="{Binding ShowSidePanel}" />
</Grid.ColumnDefinitions>
<md:MarkdownScrollViewer Margin="10 0 10 10" IsEnabled="{Binding !IsBusy}" Markdown="{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>
<ContentControl IsEnabled="{Binding !IsBusy}" Content="{Binding Content}" />
<GridSplitter Grid.Column="1" IsVisible="{Binding ShowSidePanel}" Background="{DynamicResource SemiColorTertiary}" ResizeDirection="Columns" />
<TabControl Grid.Column="2" IsVisible="{Binding ShowSidePanel}" Margin="10">
<TabItem Header="Settings">
@@ -96,14 +73,9 @@
<ContentControl Margin="0 10 0 0" Content="{Binding SettingsViewModel}" />
</ScrollViewer>
</TabItem>
<TabItem Header="Markdown">
<TabItem Header="Raw Content">
<ScrollViewer>
<ContentControl Margin="0 10 0 0" Content="{Binding RawMarkdownViewModel}" />
</ScrollViewer>
</TabItem>
<TabItem Header="HTML">
<ScrollViewer>
<ContentControl Margin="0 10 0 0" Content="{Binding RawHtmlViewModel}" />
<SelectableTextBlock Margin="0 10 0 0" Text="{Binding RawContent}" TextWrapping="Wrap" />
</ScrollViewer>
</TabItem>
</TabControl>
@@ -111,13 +83,27 @@
<Border Grid.Row="2" BorderThickness="0 2 0 0" CornerRadius="0" BorderBrush="{DynamicResource SemiColorTertiary}">
<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="1" i:Attached.Icon="fa-solid fa-caret-right" IsEnabled="{Binding ForwardEnabled}" Command="{Binding Forward}" />
<Button Grid.Column="2" i:Attached.Icon="fa-solid fa-rotate-right" Command="{Binding Browse}" />
<Button Grid.Column="3" i:Attached.Icon="fa-solid fa-house" Command="{StaticResource HomeCommand}" />
<Button Grid.Column="0" i:Attached.Icon="fa-solid fa-caret-left" Command="{Binding Back}">
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<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">
<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>
<KeyBinding Gesture="Enter" Command="{Binding Browse}" />
</TextBox.KeyBindings>
@@ -128,7 +114,7 @@
</TextBox.Styles>
</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>
<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();
}
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" />
<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" />
<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>
</UserControl>