mirror of
https://github.com/Stone-Red-Code/Markdowser.git
synced 2026-09-04 17:16:16 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e27a195781 | ||
|
|
15a46f392e | ||
|
|
839d9402e8 | ||
|
|
6ebf525b6f | ||
|
|
3de441a3de | ||
|
|
3436f358be | ||
|
|
0724aac442 | ||
|
|
07c982afd5 | ||
|
|
789350736e | ||
|
|
813f3b39a0 | ||
|
|
e943ed0bd7 | ||
|
|
27244ba2a3 | ||
|
|
551bb2ad90 | ||
|
|
32037ffcf8 | ||
|
|
7bb7a174ed | ||
|
|
2d75a1070b | ||
|
|
8d50d759c9 | ||
|
|
c5c55c1164 | ||
|
|
d8a3230552 | ||
|
|
657dbffcd0 | ||
|
|
a9bd9c8854 | ||
|
|
6beaa9c9e8 | ||
|
|
c68bfdb667 | ||
|
|
9e259a7215 | ||
|
|
281a59165c | ||
|
|
441c41543c | ||
|
|
fe43fe933d | ||
|
|
cb8d13fe08 | ||
|
|
87d9e7abb6 | ||
|
|
8ea493f2c8 | ||
|
|
6f797b8174 | ||
|
|
f53eb36dd0 | ||
|
|
aa1484f5d4 | ||
|
|
571d8c2b4b | ||
|
|
f7225494f6 | ||
|
|
b4edef9021 | ||
|
|
4f3c59a32c | ||
|
|
cbe32f7bdf | ||
|
|
e8d8745a2b | ||
|
|
bb78a4a067 | ||
|
|
7eb1694c44 | ||
|
|
b83f2bc5b6 | ||
|
|
942e58ac0f |
+140
@@ -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,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,12 +21,16 @@ It uses the [ReverseMarkdown](https://github.com/mysticmind/reversemarkdown-net)
|
|||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Third party licenses
|
## Third party licenses
|
||||||
|
|
||||||
- [Avalonia](https://github.com/AvaloniaUI/Avalonia) - [MIT](https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md)
|
- [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)
|
- [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
@@ -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.
|
||||||
|
|
||||||
@@ -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 “Ω” doesn’t 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>
|
||||||
@@ -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.
|
||||||
@@ -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.
@@ -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
|
||||||
Generated
+1
@@ -0,0 +1 @@
|
|||||||
|
Markdowser
|
||||||
@@ -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>
|
||||||
Generated
+6
@@ -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
@@ -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>
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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))
|
||||||
@@ -27,7 +33,15 @@ public class Settings
|
|||||||
{
|
{
|
||||||
if (File.Exists(Configuration.SettingsFilePath))
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -17,8 +23,30 @@ internal static class Program
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
_ = BuildAvaloniaApp()
|
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
|
||||||
.StartWithClassicDesktopLifetime(args);
|
{
|
||||||
|
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.
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
{
|
||||||
|
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" }];
|
public static void InvokeThemeChanged()
|
||||||
|
|
||||||
internal static void SetUrl(object sender, string url)
|
|
||||||
{
|
{
|
||||||
GlobalState.url = url;
|
ThemeChanged?.Invoke(null, EventArgs.Empty);
|
||||||
UrlChanged?.Invoke(sender, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void ReloadContent(object sender)
|
|
||||||
{
|
|
||||||
ContentReload?.Invoke(sender, EventArgs.Empty);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
{
|
||||||
|
FetchUrl(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Content = CurrentTabState.Content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringBuilder Content
|
public TabState CurrentTabState
|
||||||
{
|
{
|
||||||
get => content ?? DefaultContent;
|
get
|
||||||
set => this.RaiseAndSetIfChanged(ref content, value);
|
{
|
||||||
|
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,57 +177,60 @@ 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($"}-Transparent.png)")
|
.AppendLine($"}-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(ForwardEnabled));
|
this.RaisePropertyChanged(nameof(Icon));
|
||||||
this.RaisePropertyChanged(nameof(BackEnabled));
|
|
||||||
this.RaisePropertyChanged(nameof(Url));
|
|
||||||
|
|
||||||
if (sender == this)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FetchUrl();
|
|
||||||
};
|
|
||||||
|
|
||||||
GlobalState.ContentReload += (sender, _) =>
|
|
||||||
{
|
|
||||||
this.RaisePropertyChanged(nameof(Content));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
return Uri.TryCreate(uriString, UriKind.Absolute, out uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex("!\\[(.*?)\\]\\((.*?)\\)")]
|
private void FetchUrl(bool useCache = true)
|
||||||
private static partial Regex MarkdownImage();
|
|
||||||
|
|
||||||
private void FetchUrl()
|
|
||||||
{
|
{
|
||||||
|
if (IsBusy)
|
||||||
|
{
|
||||||
|
WindowNotificationManager.Show(new Notification("Busy", "The browser is currently busy.", NotificationType.Warning));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(Url))
|
if (string.IsNullOrWhiteSpace(Url))
|
||||||
{
|
{
|
||||||
content = null;
|
Content = DefaultContent;
|
||||||
_ = html.Clear();
|
Logger.LogDebug("URL is empty.");
|
||||||
ContentChanged();
|
|
||||||
Debug.WriteLine("URL is empty.");
|
|
||||||
CurrentTab.Header = "New Tab";
|
CurrentTab.Header = "New Tab";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsValidHttpUri(Url, out _))
|
if (!IsValidHttpUri(Url, out _))
|
||||||
{
|
{
|
||||||
// Search with duckduckgo
|
if (Url.StartsWith("//"))
|
||||||
Url = $"https://duckduckgo.com/html/?kd=-1&k1=-1&q={Uri.EscapeDataString(Url)}";
|
{
|
||||||
|
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();
|
httpClient.DefaultRequestHeaders.UserAgent.Clear();
|
||||||
_ = content.Clear();
|
if (!httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd(Settings.Current.UserAgent))
|
||||||
_ = html.Clear();
|
{
|
||||||
|
WindowNotificationManager.Show(new Notification("Invalid User Agent", "Failed to set user agent.", NotificationType.Error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
Progress = 0;
|
Progress = 0;
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
Debug.WriteLine("Fetching URL...");
|
Logger.LogInfo($"Fetching URL: {Url}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(Url);
|
ContentViewModelBase? cachedContent = cacheService.Get(Url);
|
||||||
|
if (cachedContent is not null && useCache)
|
||||||
Url = httpResponseMessage.RequestMessage?.RequestUri?.ToString() ?? Url;
|
|
||||||
|
|
||||||
if (!httpResponseMessage.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
Content = cachedContent;
|
||||||
_ = content.AppendLine($"# {(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}");
|
}
|
||||||
_ = content.AppendLine($"Failed to fetch URL: {httpResponseMessage.ReasonPhrase}");
|
else
|
||||||
ContentChanged();
|
{
|
||||||
return;
|
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();
|
Logger.LogInfo($"Fetched URL: {Url}");
|
||||||
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)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
StringBuilder errorMessage = new();
|
||||||
|
|
||||||
if (e.StatusCode is not null)
|
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}");
|
_ = errorMessage.AppendLine($"# {e.HttpRequestError}");
|
||||||
_ = content.AppendLine($"Failed to fetch URL: {e.Message}");
|
_ = errorMessage.AppendLine($"Failed to fetch URL: {e.Message}");
|
||||||
ContentChanged();
|
|
||||||
|
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
|
||||||
|
|
||||||
|
Logger.LogError(e.Message);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
StringBuilder errorMessage = new();
|
||||||
_ = content.AppendLine($"# {e.GetType().Name}");
|
|
||||||
_ = content.AppendLine($"Failed to fetch URL: {e.Message}");
|
_ = errorMessage.AppendLine($"# {e.GetType().Name}");
|
||||||
ContentChanged();
|
_ = 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
|
||||||
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);
|
IsBusy = false;
|
||||||
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()
|
private void UpdateProgress(ProcessingProgress progress)
|
||||||
{
|
{
|
||||||
this.RaisePropertyChanged(nameof(Content));
|
if (progress.IsBytes)
|
||||||
this.RaisePropertyChanged(nameof(RawHtmlViewModel));
|
{
|
||||||
this.RaisePropertyChanged(nameof(RawMarkdownViewModel));
|
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;
|
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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user