Merge pull request #85 from mateuszskoczek/dev

Dev
This commit is contained in:
2024-04-23 12:01:07 +02:00
committed by GitHub
Unverified
532 changed files with 15047 additions and 8332 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.exe filter=lfs diff=lfs merge=lfs -text

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 94 KiB

18
.github/config/gitversion.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
next-version: 1.0.0
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
branches:
master:
regex: ^master$
mode: ContinuousDelivery
increment: Patch
tag: ''
is-release-branch: true
test:
regex: ^features
mode: ContinuousDelivery
increment: Patch
tag: ''
is-release-branch: true
source-branches: []

26
.github/workflows/pull_request_dev.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Build application on dev pull request
on:
pull_request:
branches:
- "dev"
paths:
- "VDownload**"
jobs:
build:
name: Build
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build

View File

@@ -0,0 +1,26 @@
name: Build application on master pull request
on:
pull_request:
branches:
- "master"
paths:
- "VDownload**"
jobs:
build:
name: Build
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build

26
.github/workflows/push_dev.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Build application on dev push
on:
push:
branches:
- "dev"
paths:
- "VDownload**"
jobs:
build:
name: Build
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build

66
.github/workflows/push_master.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: Build and publish application on master push
on:
push:
branches:
- "master"
- "features/github_actions"
#paths:
#- "VDownload**"
jobs:
build:
name: Build
runs-on: windows-latest
env:
MSBUILD_PATH: C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\devenv.com
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup GitVersion
uses: gittools/actions/gitversion/setup@v0.9.7
with:
versionSpec: 5.x
- name: Determine Version
uses: gittools/actions/gitversion/execute@v0.9.7
id: gitversion
with:
useConfigFile: true
configFilePath: .github/config/gitversion.yml
- name: Set version in VDownload.csproj file
id: package_version
uses: KageKirin/set-csproj-version@v0
with:
file: VDownload/VDownload.csproj
version: ${{steps.gitversion.outputs.SemVer}}
- name: Set version in Package.appxmanifest file
uses: Nambers/ReplaceStringInFile@v1.3
with:
path: VDownload/Package.appxmanifest
oldString: 0\.0\.0\.0
newString: ${{steps.gitversion.outputs.SemVer}}.0
showFileContent: true
escapeBackslash: true
- name: Set version in app.manifest file
uses: Nambers/ReplaceStringInFile@v1.3
with:
path: VDownload/app.manifest
oldString: 0\.0\.0\.0
newString: ${{steps.gitversion.outputs.SemVer}}.0
showFileContent: true
escapeBackslash: true
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Build application
run: dotnet build -o build
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: build
path: build

10
CHANGELOG.md Normal file
View File

@@ -0,0 +1,10 @@
## Changelog
* Migration to Windows App SDK and WinUI 3
* Code refactoring, MVVM pattern and Dependency Injection applied
* Authentication data encryption added
* Replacing native Windows media transcoding libraries with FFmpeg
* Home - Cancel all button added
* Subscriptions and Home - Error display improved
* Subscriptions - New videos counter removed
* Minor interface changes

View File

@@ -10,8 +10,8 @@ VDownload is universal video downloader written in .NET/C# and Universal Windows
## Download and installation ## Download and installation
- Microsoft Store
- GitHub Releases - GitHub Releases
- Microsoft Store (in the future)
@@ -19,26 +19,24 @@ VDownload is universal video downloader written in .NET/C# and Universal Windows
#### Supported sources #### Supported sources
- Twitch (VODs, clips and channels) - Twitch (VODs, clips and channels)
- YouTube (Videos, playlists and channels)
#### Features #### Features
- Intuitive and easy to use - Intuitive and easy to use
- Modern GUI, consistent with Windows, touchscreen friendly - WinUI - Modern GUI, consistent with Windows, touchscreen friendly
- Multiple video service support - one app for all - Multiple video service support - one app for all
- Video downloading - Video downloading
- Audio-only, video-only and audio-video downloading - Audio-only, video-only and audio-video downloading
- Time trimming - download only the fragment you need - Time trimming - download only the fragment you need
- Task scheduling - start downloading after a certain amount of time
- Playlist downloading - Playlist downloading
- Playlist filtering - download only the videos you need - Playlist filtering - download only the videos you need
- Playlist subscription - download new videos from saved playlists - Playlist subscription - download new videos from saved playlists
- Downloading queue - Downloading queue
- Use of native Windows media transcoding libraries - no FFmpeg included - FFmpeg transcoding
- Highly configurable - from default video downloading parameters to transcoding algorithm - Highly configurable - from default video downloading parameters to transcoding options
and everything for free with open source code and all of this for free
@@ -46,26 +44,10 @@ and everything for free with open source code
VDownload is a project, which I develop in my free time as a hobby and a form of learning. VDownload is a project, which I develop in my free time as a hobby and a form of learning.
As the application is not 100% resistant to external factors (e.g. changes in APIs) and to be fair to creators and video services, it is available for free. As the application is not 100% resistant to external factors (e.g. changes in services APIs) and to be fair to creators and video services, it is available for free with open source code.
If you want to support me and the development of the project you can: If you want to support me and the development of the project you can:
- **Donate me** via Paypal (link in the "Sponsor this project" section in Github repository and in the app itself in the "About" page) - **Donate me** via Paypal (link in the "Sponsor this project" section in Github repository and in the app itself in the "About" page)
- **Report bugs and errors** in the "Issues" section of Github repository - **Report bugs and errors** in the "Issues" section of Github repository
- **Propose new features and support of video services** in the "Issues" section of Github repository - **Propose new features and support of video services** in the "Issues" section of Github repository
- **Translate the application into a language you know** - **Translate the application into a language you know**
#### App development status
At the moment, the application is in the **early stage of development**. Only source code is available to download. Check "App development plans" to learn about further project development plans.
#### App development plans
- **1.0-prerelease1** - with Twitch support only, all video and playlist download options and playlist subscription *[Q2 2022]*
- **1.0-prerelease2** - Youtube support and issues from 1.0-prelease1 closed *[Q3 2022]*
- **1.0** - issues from 1.0-prelease2 closed and support for Polish (and maybe German) language *[Q3 2022]*
- **1.?** - new video services and language support and closing issues from previous versions. *[?]*
- **2.0** - switch from Universal Windows Platform to Windows App SDK *[when Windows App SDK will be more developed and will offer elements which I use in UWP (e.g. Mica)]*
**dates are indicative only and may change*

View File

@@ -1,12 +0,0 @@
namespace VDownload.Core.Enums
{
public enum AudioFileExtension
{
MP3 = 3,
FLAC = 4,
WAV = 5,
M4A = 6,
ALAC = 7,
WMA = 8,
}
}

View File

@@ -1,9 +0,0 @@
namespace VDownload.Core.Enums
{
public enum DownloadTasksAddingRequestSource
{
Video,
Playlist,
Subscriptions,
}
}

View File

@@ -1,15 +0,0 @@
namespace VDownload.Core.Enums
{
public enum MediaFileExtension
{
MP4 = VideoFileExtension.MP4,
WMV = VideoFileExtension.WMV,
HEVC = VideoFileExtension.HEVC,
MP3 = AudioFileExtension.MP3,
FLAC = AudioFileExtension.FLAC,
WAV = AudioFileExtension.WAV,
M4A = AudioFileExtension.M4A,
ALAC = AudioFileExtension.ALAC,
WMA = AudioFileExtension.WMA,
}
}

View File

@@ -1,9 +0,0 @@
namespace VDownload.Core.Enums
{
public enum MediaType
{
AudioVideo = 0,
OnlyAudio = 1,
OnlyVideo = 2,
}
}

View File

@@ -1,8 +0,0 @@
namespace VDownload.Core.Enums
{
public enum PlaylistSource
{
TwitchChannel,
Null
}
}

View File

@@ -1,9 +0,0 @@
namespace VDownload.Core.Enums
{
public enum VideoFileExtension
{
MP4 = 0,
WMV = 1,
HEVC = 2,
}
}

View File

@@ -1,9 +0,0 @@
namespace VDownload.Core.Enums
{
public enum VideoSource
{
TwitchVod,
TwitchClip,
Null
}
}

View File

@@ -1,61 +0,0 @@
using System;
using VDownload.Core.Enums;
namespace VDownload.Core.EventArgs
{
public class DownloadTaskStatusChangedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public DownloadTaskStatusChangedEventArgs(DownloadTaskStatus status)
{
Status = status;
}
public DownloadTaskStatusChangedEventArgs(DownloadTaskStatus status, DateTime scheduledFor)
{
Status = status;
ScheduledFor = scheduledFor;
}
public DownloadTaskStatusChangedEventArgs(DownloadTaskStatus status, double progress)
{
Status = status;
if (Status == DownloadTaskStatus.Downloading)
{
DownloadingProgress = progress;
}
else if (Status == DownloadTaskStatus.Processing)
{
ProcessingProgress = progress;
}
}
public DownloadTaskStatusChangedEventArgs(DownloadTaskStatus status, TimeSpan elapsedTime)
{
Status = status;
ElapsedTime = elapsedTime;
}
public DownloadTaskStatusChangedEventArgs(DownloadTaskStatus status, Exception exception)
{
Status = status;
Exception = exception;
}
#endregion
#region PROPERTIES
public DownloadTaskStatus Status { get; private set; }
public DateTime ScheduledFor { get; private set; }
public double DownloadingProgress { get; private set; }
public double ProcessingProgress { get; private set; }
public TimeSpan ElapsedTime { get; private set; }
public Exception Exception { get; private set; }
#endregion
}
}

View File

@@ -1,27 +0,0 @@
using VDownload.Core.Enums;
using VDownload.Core.Structs;
namespace VDownload.Core.EventArgs
{
public class DownloadTasksAddingRequestedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public DownloadTasksAddingRequestedEventArgs(DownloadTask[] downloadTasks, DownloadTasksAddingRequestSource requestSource)
{
DownloadTasks = downloadTasks;
RequestSource = requestSource;
}
#endregion
#region PROPERTIES
public DownloadTask[] DownloadTasks { get; private set; }
public DownloadTasksAddingRequestSource RequestSource { get; private set; }
#endregion
}
}

View File

@@ -1,24 +0,0 @@
using VDownload.Core.Interfaces;
namespace VDownload.Core.EventArgs
{
public class PlaylistSearchSuccessedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public PlaylistSearchSuccessedEventArgs(IPlaylist playlist)
{
Playlist = playlist;
}
#endregion
#region PROPERTIES
public IPlaylist Playlist { get; private set; }
#endregion
}
}

View File

@@ -1,24 +0,0 @@
namespace VDownload.Core.EventArgs
{
public class ProgressChangedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public ProgressChangedEventArgs(double progress, bool isCompleted = false)
{
Progress = progress;
IsCompleted = isCompleted;
}
#endregion
#region PROPERTIES
public double Progress { get; private set; }
public bool IsCompleted { get; private set; }
#endregion
}
}

View File

@@ -1,24 +0,0 @@
using VDownload.Core.Interfaces;
namespace VDownload.Core.EventArgs
{
public class SubscriptionLoadSuccessedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public SubscriptionLoadSuccessedEventArgs(IVideo[] videos)
{
Videos = videos;
}
#endregion
#region PROPERTIES
public IVideo[] Videos { get; private set; }
#endregion
}
}

View File

@@ -1,24 +0,0 @@
using VDownload.Core.Interfaces;
namespace VDownload.Core.EventArgs
{
public class VideoSearchSuccessedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public VideoSearchSuccessedEventArgs(IVideo video)
{
Video = video;
}
#endregion
#region PROPERTIES
public IVideo Video { get; set; }
#endregion
}
}

View File

@@ -1,11 +0,0 @@
using System;
namespace VDownload.Core.Exceptions
{
public class MediaNotFoundException : Exception
{
public MediaNotFoundException() { }
public MediaNotFoundException(string message) : base(message) { }
public MediaNotFoundException(string message, Exception inner) : base(message, inner) { }
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.Exceptions
{
public class SubscriptionExistsException : Exception
{
public SubscriptionExistsException() { }
public SubscriptionExistsException(string message) : base(message) { }
public SubscriptionExistsException(string message, Exception inner) : base(message, inner) { }
}
}

View File

@@ -1,11 +0,0 @@
using System;
namespace VDownload.Core.Exceptions
{
public class TwitchAccessTokenNotFoundException : Exception
{
public TwitchAccessTokenNotFoundException() { }
public TwitchAccessTokenNotFoundException(string message) : base(message) { }
public TwitchAccessTokenNotFoundException(string message, Exception inner) :base(message, inner) { }
}
}

View File

@@ -1,11 +0,0 @@
using System;
namespace VDownload.Core.Exceptions
{
public class TwitchAccessTokenNotValidException : Exception
{
public TwitchAccessTokenNotValidException() { }
public TwitchAccessTokenNotValidException(string message) : base(message) { }
public TwitchAccessTokenNotValidException(string message, Exception inner) : base(message, inner) { }
}
}

View File

@@ -1,66 +0,0 @@
using System;
using System.Linq;
using System.Text;
namespace VDownload.Core.Extensions
{
public static class TimeSpanExtension
{
// To string (TH:)MM:SS
public static string ToStringOptTHBaseMMSS(this TimeSpan timeSpan, params TimeSpan[] formatBase)
{
StringBuilder formattedTimeSpan = new StringBuilder();
int maxTHLength = 0;
foreach (TimeSpan format in formatBase.Concat(new TimeSpan[] { timeSpan }))
{
int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0;
if (THLength > maxTHLength)
{
maxTHLength = THLength;
}
}
if (maxTHLength > 0)
{
formattedTimeSpan.Append($"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:");
}
formattedTimeSpan.Append(maxTHLength == 0 ? $"{timeSpan.Minutes}:" : $"{timeSpan.Minutes:00}:");
formattedTimeSpan.Append($"{timeSpan.Seconds:00}");
return formattedTimeSpan.ToString();
}
// To string ((TH:)MM:)SS
public static string ToStringOptTHMMBaseSS(this TimeSpan timeSpan, params TimeSpan[] formatBase)
{
StringBuilder formattedTimeSpan = new StringBuilder();
int maxTHLength = 0;
foreach (TimeSpan format in formatBase.Concat(new TimeSpan[] { timeSpan }))
{
int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0;
if (THLength > maxTHLength)
{
maxTHLength = THLength;
}
}
if (maxTHLength > 0)
{
formattedTimeSpan.Append($"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:");
}
bool MM = false;
if (Math.Floor(timeSpan.TotalMinutes) > 0 || maxTHLength > 0)
{
formattedTimeSpan.Append(maxTHLength > 0 ? $"{timeSpan.Minutes:00}:" : $"{timeSpan.Minutes}:");
MM = true;
}
formattedTimeSpan.Append(MM ? $"{timeSpan.Seconds:00}" : $"{timeSpan.Seconds}");
return formattedTimeSpan.ToString();
}
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
namespace VDownload.Core.Interfaces
{
public interface IPlaylist
{
#region PROPERTIES
PlaylistSource Source { get; }
string ID { get; }
Uri Url { get; }
string Name { get; }
IVideo[] Videos { get; }
#endregion
#region METHODS
Task GetMetadataAsync(CancellationToken cancellationToken = default);
Task GetVideosAsync(CancellationToken cancellationToken = default);
Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default);
#endregion
}
}

View File

@@ -1,49 +0,0 @@
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Structs;
using Windows.Storage;
namespace VDownload.Core.Interfaces
{
public interface IVideo
{
#region PROPERTIES
VideoSource Source { get; }
string ID { get; }
Uri Url { get; }
string Title { get; }
string Author { get; }
DateTime Date { get; }
TimeSpan Duration { get; }
long Views { get; }
Uri Thumbnail { get; }
BaseStream[] BaseStreams { get; }
#endregion
#region METHODS
Task GetMetadataAsync(CancellationToken cancellationToken = default);
Task GetStreamsAsync(CancellationToken cancellationToken = default);
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default);
#endregion
#region EVENTS
event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
#endregion
}
}

View File

@@ -1,76 +0,0 @@
using System.Collections.Generic;
using VDownload.Core.Enums;
using Windows.Media.Editing;
using Windows.Media.Transcoding;
using Windows.Storage;
namespace VDownload.Core.Services
{
public static class Config
{
#region CONSTANTS
private static readonly Dictionary<string, object> DefaultSettings = new Dictionary<string, object>()
{
{ "delete_temp_on_start", true },
{ "twitch_vod_passive_trim", true },
{ "twitch_vod_downloading_chunk_retry_after_error", false },
{ "twitch_vod_downloading_chunk_max_retries", 10 },
{ "twitch_vod_downloading_chunk_retries_delay", 5000 },
{ "media_transcoding_use_hardware_acceleration", true },
{ "media_transcoding_algorithm", (int)MediaVideoProcessingAlgorithm.MrfCrf444 },
{ "media_editing_algorithm", (int)MediaTrimmingPreference.Fast },
{ "default_max_playlist_videos", 0 },
{ "default_media_type", (int)MediaType.AudioVideo },
{ "default_filename", "[<date_pub:yyyy.MM.dd>] <title>" },
{ "default_video_extension", (int)VideoFileExtension.MP4 },
{ "default_audio_extension", (int)AudioFileExtension.MP3 },
{ "custom_media_location", false },
{ "max_active_video_task", 5 },
{ "replace_output_file_if_exists", false },
{ "remove_task_when_successfully_ended", false },
{ "delete_task_temp_when_ended_with_error", false },
{ "show_notification_when_task_ended_successfully", false },
{ "show_notification_when_task_ended_unsuccessfully", false },
{ "show_warning_when_task_starts_on_metered_network", true },
{ "delay_task_when_queued_task_starts_on_metered_network", true }
};
#endregion
#region PUBLIC METHODS
public static object GetValue(string key)
{
return ApplicationData.Current.LocalSettings.Values[key];
}
public static void SetValue(string key, object value)
{
ApplicationData.Current.LocalSettings.Values[key] = value;
}
public static void SetDefault()
{
foreach (KeyValuePair<string, object> s in DefaultSettings)
{
ApplicationData.Current.LocalSettings.Values[s.Key] = s.Value;
}
}
public static void Rebuild()
{
foreach (KeyValuePair<string, object> s in DefaultSettings)
{
if (!ApplicationData.Current.LocalSettings.Values.ContainsKey(s.Key))
{
ApplicationData.Current.LocalSettings.Values[s.Key] = s.Value;
}
}
}
#endregion
}
}

View File

@@ -1,178 +0,0 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.EventArgs;
using VDownload.Core.Interfaces;
using VDownload.Core.Services;
using Windows.ApplicationModel.ExtendedExecution;
using Windows.Storage;
using Windows.Storage.AccessCache;
namespace VDownload.Core.Structs
{
public class DownloadTask
{
#region CONSTRUCTORS
public DownloadTask(string id, IVideo video, MediaType mediaType, BaseStream selectedStream, TrimData trim, OutputFile file, double schedule)
{
Id = id;
Video = video;
MediaType = mediaType;
SelectedStream = selectedStream;
Trim = trim;
File = file;
Schedule = schedule;
Status = DownloadTaskStatus.Idle;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status);
CancellationTokenSource = new CancellationTokenSource();
}
#endregion
#region PROPERTIES
public string Id { get; set; }
public IVideo Video { get; set; }
public MediaType MediaType { get; set; }
public BaseStream SelectedStream { get; set; }
public TrimData Trim { get; set; }
public OutputFile File { get; set; }
public double Schedule { get; set; }
public DownloadTaskStatus Status { get; private set; }
public DownloadTaskStatusChangedEventArgs LastStatusChangedEventArgs { get; private set; }
public CancellationTokenSource CancellationTokenSource { get; private set; }
#endregion
#region METHODS
public async Task Run(bool delayWhenOnMeteredConnection)
{
StatusChanged.Invoke(this, new DownloadTaskStatusChangedEventArgs(Status));
CancellationTokenSource = new CancellationTokenSource();
if (Schedule > 0)
{
DateTime scheduleFor = DateTime.Now.AddMinutes(Schedule);
Status = DownloadTaskStatus.Scheduled;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, scheduleFor);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
while (DateTime.Now < scheduleFor && !CancellationTokenSource.Token.IsCancellationRequested)
{
await Task.Delay(100);
}
}
Status = DownloadTaskStatus.Queued;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
await DownloadTasksCollectionManagement.WaitInQueue(delayWhenOnMeteredConnection, CancellationTokenSource.Token);
if (!CancellationTokenSource.Token.IsCancellationRequested)
{
Status = DownloadTaskStatus.Downloading;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, 0);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
StorageFolder tempFolder;
if (StorageApplicationPermissions.FutureAccessList.ContainsItem("custom_temp_location"))
{
tempFolder = await StorageApplicationPermissions.FutureAccessList.GetFolderAsync("custom_temp_location");
}
else
{
tempFolder = ApplicationData.Current.TemporaryFolder;
}
tempFolder = await tempFolder.CreateFolderAsync(Id);
bool endedWithError = false;
try
{
CancellationTokenSource.Token.ThrowIfCancellationRequested();
Stopwatch taskStopwatch = Stopwatch.StartNew();
ExtendedExecutionSession session = new ExtendedExecutionSession { Reason = ExtendedExecutionReason.Unspecified };
await session.RequestExtensionAsync();
CancellationTokenSource.Token.ThrowIfCancellationRequested();
Video.DownloadingProgressChanged += DownloadingProgressChanged;
Video.ProcessingProgressChanged += ProcessingProgressChanged;
StorageFile tempOutputFile = await Video.DownloadAndTranscodeAsync(tempFolder, SelectedStream, File.Extension, MediaType, Trim, CancellationTokenSource.Token);
session.Dispose();
Status = DownloadTaskStatus.Finalizing;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
StorageFile outputFile = await File.Create();
CancellationTokenSource.Token.ThrowIfCancellationRequested();
await tempOutputFile.MoveAndReplaceAsync(outputFile);
taskStopwatch.Stop();
Status = DownloadTaskStatus.EndedSuccessfully;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, taskStopwatch.Elapsed);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
}
catch (Exception ex)
{
endedWithError = true;
Status = DownloadTaskStatus.EndedUnsuccessfully;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, ex);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
}
finally
{
if (!endedWithError || (bool)Config.GetValue("delete_task_temp_when_ended_with_error"))
{
// Delete temporary files
await tempFolder.DeleteAsync();
}
}
}
else
{
Status = DownloadTaskStatus.EndedUnsuccessfully;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, new OperationCanceledException(CancellationTokenSource.Token));
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
}
}
private void DownloadingProgressChanged(object sender, ProgressChangedEventArgs e)
{
Status = DownloadTaskStatus.Downloading;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, e.Progress);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
}
private void ProcessingProgressChanged(object sender, ProgressChangedEventArgs e)
{
Status = DownloadTaskStatus.Processing;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, e.Progress);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
}
#endregion
#region EVENT
public event EventHandler<DownloadTaskStatusChangedEventArgs> StatusChanged;
#endregion
}
}

View File

@@ -1,64 +0,0 @@
using Microsoft.Toolkit.Uwp.Connectivity;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Structs;
namespace VDownload.Core.Services
{
public static class DownloadTasksCollectionManagement
{
#region PROPERTIES
private static readonly Dictionary<string, DownloadTask> ChangeableDownloadTasksCollection = new Dictionary<string, DownloadTask>();
public static readonly ReadOnlyDictionary<string, DownloadTask> DownloadTasksCollection = new ReadOnlyDictionary<string, DownloadTask>(ChangeableDownloadTasksCollection);
#endregion
#region PUBLIC METHODS
public static string GenerateID()
{
char[] idChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
int idLength = 10;
string id;
do
{
id = "";
while (id.Length < idLength)
{
id += idChars[new Random().Next(0, idChars.Length)];
}
} while (DownloadTasksCollection.Keys.Contains(id));
return id;
}
public static void Add(DownloadTask downloadTask, string id)
{
ChangeableDownloadTasksCollection[id] = downloadTask;
}
public static void Remove(string id)
{
ChangeableDownloadTasksCollection.Remove(id);
}
public static async Task WaitInQueue(bool delayWhenOnMeteredConnection, CancellationToken cancellationToken = default)
{
while ((ChangeableDownloadTasksCollection.Values.Where((DownloadTask task) => task.Status == Enums.DownloadTaskStatus.Downloading || task.Status == Enums.DownloadTaskStatus.Processing || task.Status == Enums.DownloadTaskStatus.Finalizing).Count() >= (int)Config.GetValue("max_active_video_task") || (delayWhenOnMeteredConnection && NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection)) && !cancellationToken.IsCancellationRequested)
{
await Task.Delay(100);
}
}
#endregion
}
}

View File

@@ -1,146 +0,0 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Structs;
using Windows.Foundation;
using Windows.Media.Editing;
using Windows.Media.MediaProperties;
using Windows.Media.Transcoding;
using Windows.Storage;
using Windows.Storage.Streams;
namespace VDownload.Core.Services
{
public class MediaProcessor
{
#region PUBLIC METHODS
public async Task Run(StorageFile mediaFile, MediaFileExtension extension, MediaType mediaType, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
MediaTranscoder mediaTranscoder = new MediaTranscoder
{
HardwareAccelerationEnabled = (bool)Config.GetValue("media_transcoding_use_hardware_acceleration"),
VideoProcessingAlgorithm = (MediaVideoProcessingAlgorithm)Config.GetValue("media_transcoding_algorithm"),
};
if (trim.Start != null) mediaTranscoder.TrimStartTime = trim.Start;
if (trim.End != null) mediaTranscoder.TrimStopTime = trim.End;
using (IRandomAccessStream openedOutputFile = await outputFile.OpenAsync(FileAccessMode.ReadWrite))
{
PrepareTranscodeResult transcodingPreparated = await mediaTranscoder.PrepareStreamTranscodeAsync(await mediaFile.OpenAsync(FileAccessMode.Read), openedOutputFile, await GetMediaEncodingProfile(mediaFile, extension, mediaType));
IAsyncActionWithProgress<double> transcodingTask = transcodingPreparated.TranscodeAsync();
await transcodingTask.AsTask(cancellationToken, new Progress<double>((percent) => { ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(percent)); }));
cancellationToken.ThrowIfCancellationRequested();
await openedOutputFile.FlushAsync();
transcodingTask.Close();
}
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
}
public async Task Run(StorageFile audioFile, StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
MediaComposition mediaEditor = new MediaComposition();
cancellationToken.ThrowIfCancellationRequested();
Task<MediaClip> getVideoFileTask = MediaClip.CreateFromFileAsync(videoFile).AsTask();
Task<BackgroundAudioTrack> getAudioFileTask = BackgroundAudioTrack.CreateFromFileAsync(audioFile).AsTask();
await Task.WhenAll(getVideoFileTask, getAudioFileTask);
MediaClip videoElement = getVideoFileTask.Result;
if (trim.Start != null) videoElement.TrimTimeFromStart = trim.Start;
if (trim.End != null) videoElement.TrimTimeFromEnd = trim.End;
BackgroundAudioTrack audioElement = getAudioFileTask.Result;
if (trim.Start != null) audioElement.TrimTimeFromStart = trim.Start;
if (trim.End != null) audioElement.TrimTimeFromEnd = trim.End;
mediaEditor.Clips.Add(videoElement);
mediaEditor.BackgroundAudioTracks.Add(audioElement);
var renderOperation = mediaEditor.RenderToFileAsync(outputFile, (MediaTrimmingPreference)Config.GetValue("media_editing_algorithm"), await GetMediaEncodingProfile(videoFile, audioFile, (MediaFileExtension)extension, MediaType.AudioVideo));
renderOperation.Progress += (info, progress) => { ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(progress)); };
cancellationToken.ThrowIfCancellationRequested();
await renderOperation.AsTask(cancellationToken);
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
}
public async Task Run(StorageFile audioFile, AudioFileExtension extension, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default)
{
await Run(audioFile, (MediaFileExtension)extension, MediaType.OnlyAudio, outputFile, trim, cancellationToken);
}
public async Task Run(StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default)
{
await Run(videoFile, (MediaFileExtension)extension, MediaType.OnlyVideo, outputFile, trim, cancellationToken);
}
#endregion
#region PRIVATE METHODS
private static async Task<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile videoFile, StorageFile audioFile, MediaFileExtension extension, MediaType mediaType)
{
MediaEncodingProfile profile;
switch (extension)
{
default:
case MediaFileExtension.MP4: profile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p); break;
case MediaFileExtension.WMV: profile = MediaEncodingProfile.CreateWmv(VideoEncodingQuality.HD1080p); break;
case MediaFileExtension.HEVC: profile = MediaEncodingProfile.CreateHevc(VideoEncodingQuality.HD1080p); break;
case MediaFileExtension.MP3: profile = MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High); break;
case MediaFileExtension.FLAC: profile = MediaEncodingProfile.CreateFlac(AudioEncodingQuality.High); break;
case MediaFileExtension.WAV: profile = MediaEncodingProfile.CreateWav(AudioEncodingQuality.High); break;
case MediaFileExtension.M4A: profile = MediaEncodingProfile.CreateM4a(AudioEncodingQuality.High); break;
case MediaFileExtension.ALAC: profile = MediaEncodingProfile.CreateAlac(AudioEncodingQuality.High); break;
case MediaFileExtension.WMA: profile = MediaEncodingProfile.CreateWma(AudioEncodingQuality.High); break;
}
if (mediaType != MediaType.OnlyAudio)
{
var videoData = await videoFile.Properties.GetVideoPropertiesAsync();
profile.Video.Height = videoData.Height;
profile.Video.Width = videoData.Width;
profile.Video.Bitrate = videoData.Bitrate;
}
if (mediaType != MediaType.OnlyVideo)
{
var audioData = await audioFile.Properties.GetMusicPropertiesAsync();
profile.Audio.Bitrate = audioData.Bitrate;
if (mediaType == MediaType.AudioVideo) profile.Video.Bitrate -= audioData.Bitrate;
}
if (mediaType == MediaType.OnlyVideo)
{
var audioTracks = profile.GetAudioTracks();
audioTracks.Clear();
profile.SetAudioTracks(audioTracks.AsEnumerable());
}
return profile;
}
private static async Task<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile audioVideoFile, MediaFileExtension extension, MediaType mediaType)
{
return await GetMediaEncodingProfile(audioVideoFile, audioVideoFile, extension, mediaType);
}
#endregion
#region EVENTS
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProgressChanged;
#endregion
}
}

View File

@@ -1,56 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using Windows.Storage;
namespace VDownload.Core.Services
{
public class OutputFile
{
#region CONSTRUCTORS
public OutputFile(string name, MediaFileExtension extension, StorageFolder location)
{
Name = name;
Extension = extension;
Location = location;
}
public OutputFile(string name, MediaFileExtension extension)
{
Name = name;
Extension = extension;
Location = null;
}
#endregion
#region PROPERTIES
public string Name { get; private set; }
public MediaFileExtension Extension { get; private set; }
public StorageFolder Location { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task<StorageFile> Create()
{
string filename = $"{Name}.{Extension.ToString().ToLower()}";
CreationCollisionOption collisionOption = (bool)Config.GetValue("replace_output_file_if_exists") ? CreationCollisionOption.ReplaceExisting : CreationCollisionOption.GenerateUniqueName;
return await(!(Location is null) ? Location.CreateFileAsync(filename, collisionOption) : DownloadsFolder.CreateFileAsync(filename, collisionOption));
}
public string GetPath() => $@"{(Location != null ? Location.Path : $@"{UserDataPaths.GetDefault().Downloads}\VDownload")}\{Name}.{Extension.ToString().ToLower()}";
#endregion
}
}

View File

@@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Interfaces;
namespace VDownload.Core.Services
{
[Serializable]
public class Subscription
{
#region CONSTRUCTORS
public Subscription(IPlaylist playlist)
{
Playlist = playlist;
SavedVideos = Playlist.Videos;
}
#endregion
#region PROPERTIES
public IPlaylist Playlist { get; private set; }
public IVideo[] SavedVideos { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task<IVideo[]> GetNewVideosAsync(CancellationToken cancellationToken = default)
{
await Playlist.GetVideosAsync(cancellationToken);
return GetUnsavedVideos();
}
public async Task<IVideo[]> GetNewVideosAndUpdateAsync(CancellationToken cancellationToken = default)
{
await Playlist.GetVideosAsync(cancellationToken);
IVideo[] newVideos = GetUnsavedVideos();
SavedVideos = Playlist.Videos;
return newVideos;
}
#endregion
#region PRIVATE METHODS
private IVideo[] GetUnsavedVideos()
{
List<IVideo> newVideos = Playlist.Videos.ToList();
foreach (IVideo savedVideo in SavedVideos)
{
newVideos.RemoveAll((v) => v.Source == savedVideo.Source && v.ID == savedVideo.ID);
}
return newVideos.ToArray();
}
#endregion
}
}

View File

@@ -1,79 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Exceptions;
using Windows.Storage;
namespace VDownload.Core.Services
{
public static class SubscriptionsCollectionManagement
{
#region CONSTANTS
private static readonly StorageFolder SubscriptionFolderLocation = ApplicationData.Current.LocalFolder;
private static readonly string SubscriptionsFolderName = "Subscriptions";
private static readonly string SubscriptionFileExtension = "vsub";
#endregion
#region PUBLIC METHODS
public static async Task<(Subscription Subscription, StorageFile SubscriptionFile)[]> GetSubscriptionsAsync()
{
List<(Subscription Subscription, StorageFile SubscriptionFile)> subscriptions = new List<(Subscription Subscription,StorageFile SubscriptionFile)> ();
StorageFolder subscriptionsFolder = await SubscriptionFolderLocation.CreateFolderAsync(SubscriptionsFolderName, CreationCollisionOption.OpenIfExists);
BinaryFormatter formatter = new BinaryFormatter();
foreach (StorageFile file in await subscriptionsFolder.GetFilesAsync())
{
if (file.Name.EndsWith(SubscriptionFileExtension))
{
Stream fileStream = await file.OpenStreamForReadAsync();
Subscription subscription = (Subscription)formatter.Deserialize(fileStream);
subscriptions.Add((subscription, file));
}
}
return subscriptions.ToArray();
}
public static async Task<StorageFile> CreateSubscriptionFileAsync(Subscription subscription)
{
StorageFolder subscriptionsFolder = await SubscriptionFolderLocation.CreateFolderAsync(SubscriptionsFolderName, CreationCollisionOption.OpenIfExists);
try
{
StorageFile subscriptionFile = await subscriptionsFolder.CreateFileAsync($"{(int)subscription.Playlist.Source}-{subscription.Playlist.ID}.{SubscriptionFileExtension}", CreationCollisionOption.FailIfExists);
BinaryFormatter formatter = new BinaryFormatter();
Stream subscriptionFileStream = await subscriptionFile.OpenStreamForWriteAsync();
formatter.Serialize(subscriptionFileStream, subscription);
return subscriptionFile;
}
catch (Exception ex)
{
if ((uint)ex.HResult == 0x800700B7)
{
throw new SubscriptionExistsException($"Subscription with id \"{(int)subscription.Playlist.Source}-{subscription.Playlist.ID}\" already exists");
}
else
{
throw;
}
}
}
public static async Task UpdateSubscriptionFileAsync(Subscription subscription, StorageFile subscriptionFile)
{
BinaryFormatter formatter = new BinaryFormatter();
Stream subscriptionFileStream = await subscriptionFile.OpenStreamForWriteAsync();
formatter.Serialize(subscriptionFileStream, subscription);
}
public static async Task DeleteSubscriptionFileAsync(StorageFile subscriptionFile) => await subscriptionFile.DeleteAsync(StorageDeleteOption.PermanentDelete);
#endregion
}
}

View File

@@ -1,11 +0,0 @@
using Windows.Storage;
namespace VDownload.Core.Sources
{
internal static class AuthorizationData
{
internal static StorageFolder FolderLocation = ApplicationData.Current.LocalCacheFolder;
internal static string FolderName = "AuthData";
internal static string FilesExtension = "auth";
}
}

View File

@@ -1,86 +0,0 @@
using System.Text.RegularExpressions;
using VDownload.Core.Enums;
using VDownload.Core.Interfaces;
namespace VDownload.Core.Services.Sources
{
public static class Source
{
#region CONSTANTS
// VIDEO SOURCES REGULAR EXPRESSIONS
private static readonly (Regex Regex, VideoSource Type)[] VideoSources = new (Regex Regex, VideoSource Type)[]
{
(new Regex(@"^https://www.twitch.tv/videos/(?<id>\d+)"), VideoSource.TwitchVod),
(new Regex(@"^https://www.twitch.tv/\S+/clip/(?<id>[^?]+)"), VideoSource.TwitchClip),
(new Regex(@"^https://clips.twitch.tv/(?<id>[^?]+)"), VideoSource.TwitchClip),
};
// PLAYLIST SOURCES REGULAR EXPRESSIONS
private static readonly (Regex Regex, PlaylistSource Type)[] PlaylistSources = new (Regex Regex, PlaylistSource Type)[]
{
(new Regex(@"^https://www.twitch.tv/(?<id>[^?/]+)"), PlaylistSource.TwitchChannel),
};
#endregion
#region METHODS
// GET VIDEO SOURCE
public static IVideo GetVideo(string url)
{
VideoSource source = VideoSource.Null;
string id = string.Empty;
foreach ((Regex Regex, VideoSource Type) Source in VideoSources)
{
Match sourceMatch = Source.Regex.Match(url);
if (sourceMatch.Success)
{
source = Source.Type;
id = sourceMatch.Groups["id"].Value;
}
}
return GetVideo(source, id);
}
public static IVideo GetVideo(VideoSource source, string id)
{
IVideo videoService = null;
switch (source)
{
case VideoSource.TwitchVod: videoService = new Twitch.Vod(id); break;
case VideoSource.TwitchClip: videoService = new Twitch.Clip(id); break;
}
return videoService;
}
// GET PLAYLIST SOURCE
public static IPlaylist GetPlaylist(string url)
{
PlaylistSource source = PlaylistSource.Null;
string id = string.Empty;
foreach ((Regex Regex, PlaylistSource Type) Source in PlaylistSources)
{
Match sourceMatch = Source.Regex.Match(url);
if (sourceMatch.Success)
{
source = Source.Type;
id = sourceMatch.Groups["id"].Value;
}
}
return GetPlaylist(source, id);
}
public static IPlaylist GetPlaylist(PlaylistSource source, string id)
{
IPlaylist playlistService = null;
switch (source)
{
case PlaylistSource.TwitchChannel: playlistService = new Twitch.Channel(id); break;
}
return playlistService;
}
#endregion
}
}

View File

@@ -1,113 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
using VDownload.Core.Services.Sources.Twitch.Helpers;
namespace VDownload.Core.Services.Sources.Twitch
{
[Serializable]
public class Channel : IPlaylist
{
#region CONSTRUCTORS
public Channel(string id)
{
Source = PlaylistSource.TwitchChannel;
ID = id;
Url = new Uri($"https://twitch.tv/{ID}");
}
#endregion
#region PROPERTIES
public PlaylistSource Source { get; private set; }
public string ID { get; private set; }
private string UniqueID { get; set; }
public Uri Url { get; private set; }
public string Name { get; private set; }
public IVideo[] Videos { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
JToken response = null;
using (WebClient client = await Client.Helix())
{
client.QueryString.Add("login", ID);
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/users"))["data"];
if (((JArray)response).Count > 0) response = response[0];
else throw new MediaNotFoundException($"Twitch Channel (ID: {ID}) was not found");
}
UniqueID = (string)response["id"];
Name = (string)response["display_name"];
}
public async Task GetVideosAsync(CancellationToken cancellationToken = default) => await GetVideosAsync(0, cancellationToken);
public async Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string pagination = "";
List<Vod> videos = new List<Vod>();
bool getAll = numberOfVideos == 0;
int count;
JToken[] videosData;
List<Task> getStreamsTasks = new List<Task>();
do
{
count = numberOfVideos < 100 && !getAll ? numberOfVideos : 100;
JToken response = null;
using (WebClient client = await Client.Helix())
{
client.QueryString.Add("user_id", UniqueID);
client.QueryString.Add("first", count.ToString());
client.QueryString.Add("after", pagination);
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos"));
}
pagination = (string)response["pagination"]["cursor"];
videosData = response["data"].ToArray();
foreach (JToken videoData in videosData)
{
Vod video = new Vod((string)videoData["id"]);
video.GetMetadataAsync(videoData);
getStreamsTasks.Add(video.GetStreamsAsync());
videos.Add(video);
numberOfVideos--;
}
}
while ((getAll || numberOfVideos > 0) && count == videosData.Length);
await Task.WhenAll(getStreamsTasks);
Videos = videos.ToArray();
}
#endregion
}
}

View File

@@ -1,158 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using VDownload.Core.Enums;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
using VDownload.Core.Services.Sources.Twitch.Helpers;
using VDownload.Core.Structs;
using Windows.Storage;
namespace VDownload.Core.Services.Sources.Twitch
{
[Serializable]
public class Clip : IVideo
{
#region CONSTRUCTORS
public Clip(string id)
{
Source = VideoSource.TwitchClip;
ID = id;
Url = new Uri($"https://clips.twitch.tv/{ID}");
}
#endregion
#region PROPERTIES
public VideoSource Source { get; private set; }
public string ID { get; private set; }
public Uri Url { get; private set; }
public string Title { get; private set; }
public string Author { get; private set; }
public DateTime Date { get; private set; }
public TimeSpan Duration { get; private set; }
public long Views { get; private set; }
public Uri Thumbnail { get; private set; }
public BaseStream[] BaseStreams { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
JToken response = null;
using (WebClient client = await Client.Helix())
{
client.QueryString.Add("id", ID);
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/clips")).GetValue("data");
if (((JArray)response).Count > 0) response = response[0];
else throw new MediaNotFoundException($"Twitch Clip (ID: {ID}) was not found");
}
Title = (string)response["title"];
Author = (string)response["broadcaster_name"];
Date = Convert.ToDateTime(response["created_at"]);
Duration = TimeSpan.FromSeconds((double)response["duration"]);
Views = (long)response["view_count"];
Thumbnail = new Uri((string)response["thumbnail_url"]);
}
public async Task GetStreamsAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
JToken[] response;
using (WebClient client = Client.GQL())
{
response = JArray.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "[{\"operationName\":\"VideoAccessToken_Clip\",\"variables\":{\"slug\":\"" + ID + "\"},\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11\"}}}]"))[0]["data"]["clip"]["videoQualities"].ToArray();
}
List<BaseStream> streams = new List<BaseStream>();
foreach (JToken streamData in response)
{
BaseStream stream = new BaseStream()
{
Url = new Uri((string)streamData["sourceURL"]),
Height = int.Parse((string)streamData["quality"]),
FrameRate = (int)streamData["frameRate"],
};
streams.Add(stream);
}
BaseStreams = streams.ToArray();
}
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
JToken videoAccessToken = null;
using (WebClient client = Client.GQL())
{
videoAccessToken = JArray.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "[{\"operationName\":\"VideoAccessToken_Clip\",\"variables\":{\"slug\":\"" + ID + "\"},\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11\"}}}]"))[0]["data"]["clip"]["playbackAccessToken"];
}
// Download
cancellationToken.ThrowIfCancellationRequested();
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.mp4");
using (WebClient client = new WebClient())
{
client.DownloadProgressChanged += (s, a) => { DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(a.ProgressPercentage)); };
client.QueryString.Add("sig", (string)videoAccessToken["signature"]);
client.QueryString.Add("token", HttpUtility.UrlEncode((string)videoAccessToken["value"]));
cancellationToken.ThrowIfCancellationRequested();
using (cancellationToken.Register(client.CancelAsync))
{
await client.DownloadFileTaskAsync(baseStream.Url, rawFile.Path);
}
}
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
// Processing
StorageFile outputFile = rawFile;
if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trim.Start != null || trim.End != null)
{
cancellationToken.ThrowIfCancellationRequested();
outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
MediaProcessor mediaProcessor = new MediaProcessor();
mediaProcessor.ProgressChanged += ProcessingProgressChanged;
await mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trim, cancellationToken);
}
return outputFile;
}
#endregion
#region EVENTS
public event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
#endregion
}
}

View File

@@ -1,107 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Sources;
using VDownload.Core.Structs;
using Windows.Storage;
namespace VDownload.Core.Services.Sources.Twitch.Helpers
{
public static class Authorization
{
#region CONSTANTS
public readonly static string ClientID = "yukkqkwp61wsv3u1pya17crpyaa98y";
public readonly static string GQLApiClientID = "kimne78kx3ncx6brgo4mv6wki5h1ko";
public readonly static Uri RedirectUrl = new Uri("https://www.vd.com");
private readonly static string ResponseType = "token";
private readonly static string[] Scopes = new[]
{
"user:read:subscriptions",
};
public readonly static Uri AuthorizationUrl = new Uri($"https://id.twitch.tv/oauth2/authorize?client_id={ClientID}&redirect_uri={RedirectUrl.OriginalString}&response_type={ResponseType}&scope={string.Join(" ", Scopes)}");
#endregion
#region METHODS
public static async Task<string> ReadAccessTokenAsync()
{
try
{
StorageFolder authDataFolder = await AuthorizationData.FolderLocation.GetFolderAsync(AuthorizationData.FolderName);
StorageFile authDataFile = await authDataFolder.GetFileAsync($"Twitch.{AuthorizationData.FilesExtension}");
return await FileIO.ReadTextAsync(authDataFile);
}
catch (FileNotFoundException)
{
return null;
}
}
public static async Task SaveAccessTokenAsync(string accessToken)
{
StorageFolder authDataFolder = await AuthorizationData.FolderLocation.CreateFolderAsync(AuthorizationData.FolderName, CreationCollisionOption.OpenIfExists);
StorageFile authDataFile = await authDataFolder.CreateFileAsync($"Twitch.{AuthorizationData.FilesExtension}", CreationCollisionOption.ReplaceExisting);
await FileIO.WriteTextAsync(authDataFile, accessToken);
}
public static async Task DeleteAccessTokenAsync()
{
try
{
StorageFolder authDataFolder = await AuthorizationData.FolderLocation.GetFolderAsync(AuthorizationData.FolderName);
StorageFile authDataFile = await authDataFolder.GetFileAsync($"Twitch.{AuthorizationData.FilesExtension}");
await authDataFile.DeleteAsync();
}
catch (FileNotFoundException) { }
}
public static async Task<TwitchAccessTokenValidationData> ValidateAccessTokenAsync(string accessToken)
{
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
client.Headers.Add("Authorization", $"Bearer {accessToken}");
try
{
JObject response = JObject.Parse(await client.DownloadStringTaskAsync("https://id.twitch.tv/oauth2/validate"));
string login = response["login"].ToString();
DateTime? expirationDate = DateTime.Now.AddSeconds(long.Parse(response["expires_in"].ToString()));
return new TwitchAccessTokenValidationData(accessToken, true, login, expirationDate);
}
catch (WebException ex)
{
if (ex.Response is null)
{
throw;
}
else
{
JObject exInfo = JObject.Parse(new StreamReader(ex.Response.GetResponseStream()).ReadToEnd());
if ((int)exInfo["status"] == 401) return new TwitchAccessTokenValidationData(accessToken, false, null, null);
else throw;
}
}
}
public static async Task RevokeAccessTokenAsync(string accessToken)
{
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
await client.UploadStringTaskAsync(new Uri("https://id.twitch.tv/oauth2/revoke"), $"client_id={ClientID}&token={accessToken}");
}
#endregion
}
}

View File

@@ -1,32 +0,0 @@
using System.Net;
using System.Threading.Tasks;
using VDownload.Core.Exceptions;
namespace VDownload.Core.Services.Sources.Twitch.Helpers
{
internal static class Client
{
internal static async Task<WebClient> Helix()
{
string accessToken = await Authorization.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
var twitchAccessTokenValidation = await Authorization.ValidateAccessTokenAsync(accessToken);
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
WebClient client = new WebClient();
client.Headers.Add("Authorization", $"Bearer {accessToken}");
client.Headers.Add("Client-Id", Authorization.ClientID);
return client;
}
internal static WebClient GQL()
{
WebClient client = new WebClient();
client.Headers.Add("Client-Id", Authorization.GQLApiClientID);
return client;
}
}
}

View File

@@ -1,285 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
using VDownload.Core.Services.Sources.Twitch.Helpers;
using VDownload.Core.Structs;
using Windows.Storage;
namespace VDownload.Core.Services.Sources.Twitch
{
[Serializable]
public class Vod : IVideo
{
#region CONSTRUCTORS
public Vod(string id)
{
Source = VideoSource.TwitchVod;
ID = id;
Url = new Uri($"https://www.twitch.tv/videos/{ID}");
}
#endregion
#region PROPERTIES
public VideoSource Source { get; private set; }
public string ID { get; private set; }
public Uri Url { get; private set; }
public string Title { get; private set; }
public string Author { get; private set; }
public DateTime Date { get; private set; }
public TimeSpan Duration { get; private set; }
public long Views { get; private set; }
public Uri Thumbnail { get; private set; }
public BaseStream[] BaseStreams { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
JToken response = null;
using (WebClient client = await Client.Helix())
{
client.QueryString.Add("id", ID);
cancellationToken.ThrowIfCancellationRequested();
try
{
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")).GetValue("data")[0];
}
catch (WebException ex)
{
if (ex.Response != null && new StreamReader(ex.Response.GetResponseStream()).ReadToEnd().Contains("Not Found")) throw new MediaNotFoundException($"Twitch VOD (ID: {ID}) was not found");
else if (ex.Response != null && new StreamReader(ex.Response.GetResponseStream()).ReadToEnd() == string.Empty && ex.Message.Contains("400")) throw new MediaNotFoundException($"Twitch VOD (ID: {ID}) was not found");
else throw;
}
}
GetMetadataAsync(response);
}
internal void GetMetadataAsync(JToken response)
{
Title = ((string)response["title"]).Replace("\n", "");
Author = (string)response["user_name"];
Date = Convert.ToDateTime(response["created_at"]);
Duration = ParseDuration((string)response["duration"]);
Views = (long)response["view_count"];
Thumbnail = (string)response["thumbnail_url"] == string.Empty ? null : new Uri(((string)response["thumbnail_url"]).Replace("%{width}", "1920").Replace("%{height}", "1080"));
}
public async Task GetStreamsAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string[] response = null;
using (WebClient client = Client.GQL())
{
cancellationToken.ThrowIfCancellationRequested();
JToken videoAccessToken = JObject.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "{\"operationName\":\"PlaybackAccessToken_Template\",\"query\":\"query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \\\"web\\\", playerBackend: \\\"mediaplayer\\\", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \\\"web\\\", playerBackend: \\\"mediaplayer\\\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}\",\"variables\":{\"isLive\":false,\"login\":\"\",\"isVod\":true,\"vodID\":\"" + ID + "\",\"playerType\":\"embed\"}}"))["data"]["videoPlaybackAccessToken"];
cancellationToken.ThrowIfCancellationRequested();
response = (await client.DownloadStringTaskAsync($"http://usher.twitch.tv/vod/{ID}?nauth={videoAccessToken["value"]}&nauthsig={videoAccessToken["signature"]}&allow_source=true&player=twitchweb")).Split("\n");
}
List<BaseStream> streams = new List<BaseStream>();
Regex streamDataL2Regex = new Regex(@"^#EXT-X-STREAM-INF:BANDWIDTH=\d+,CODECS=""\S+,\S+"",RESOLUTION=\d+x(?<height>\d+),VIDEO=""\w+""(,FRAME-RATE=(?<frame_rate>\d+.\d+))?");
for (int i = 2; i < response.Length; i += 3)
{
Match line2 = streamDataL2Regex.Match(response[i + 1]);
BaseStream stream = new BaseStream()
{
Url = new Uri(response[i + 2]),
Height = int.Parse(line2.Groups["height"].Value),
FrameRate = line2.Groups["frame_rate"].Value != string.Empty ? (int)Math.Round(double.Parse(line2.Groups["frame_rate"].Value)) : 0,
};
streams.Add(stream);
}
BaseStreams = streams.ToArray();
}
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
DownloadingProgressChanged.Invoke(this, new EventArgs.ProgressChangedEventArgs(0));
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = await ExtractChunksFromM3U8Async(baseStream.Url, cancellationToken);
TimeSpan duration = Duration;
// Passive trim
if ((bool)Config.GetValue("twitch_vod_passive_trim") && trim.Start != TimeSpan.Zero && trim.End != duration) (trim, duration) = PassiveVideoTrim(chunksList, trim, Duration);
// Download
cancellationToken.ThrowIfCancellationRequested();
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.ts");
double chunksDownloaded = 0;
Task<byte[]> downloadTask;
Task writeTask;
cancellationToken.ThrowIfCancellationRequested();
downloadTask = DownloadChunkAsync(chunksList[0].ChunkUrl);
await downloadTask;
for (int i = 1; i < chunksList.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
writeTask = WriteChunkToFileAsync(rawFile, downloadTask.Result);
downloadTask = DownloadChunkAsync(chunksList[i].ChunkUrl);
await Task.WhenAll(writeTask, downloadTask);
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(++chunksDownloaded * 100 / chunksList.Count));
}
cancellationToken.ThrowIfCancellationRequested();
await WriteChunkToFileAsync(rawFile, downloadTask.Result);
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
// Processing
cancellationToken.ThrowIfCancellationRequested();
StorageFile outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
MediaProcessor mediaProcessor = new MediaProcessor();
mediaProcessor.ProgressChanged += ProcessingProgressChanged;
await mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trim, cancellationToken);
// Return output file
return outputFile;
}
#endregion
#region PRIVATE METHODS
private static async Task<List<(Uri ChunkUrl, TimeSpan ChunkDuration)>> ExtractChunksFromM3U8Async(Uri streamUrl, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string response = null;
using (WebClient client = Client.GQL())
{
response = await client.DownloadStringTaskAsync(streamUrl);
}
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunks = new List<(Uri ChunkUrl, TimeSpan ChunkDuration)>();
Regex chunkDataRegex = new Regex(@"#EXTINF:(?<duration>\d+.\d+),\n(?<filename>\S+.ts)");
string chunkLocationPath = streamUrl.AbsoluteUri.Replace(Path.GetFileName(streamUrl.AbsoluteUri), "");
foreach (Match chunk in chunkDataRegex.Matches(response))
{
Uri chunkUrl = new Uri($"{chunkLocationPath}{chunk.Groups["filename"].Value}");
TimeSpan chunkDuration = TimeSpan.FromSeconds(double.Parse(chunk.Groups["duration"].Value));
chunks.Add((chunkUrl, chunkDuration));
}
return chunks;
}
private static (TrimData Trim, TimeSpan NewDuration) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TrimData trim, TimeSpan duration)
{
TimeSpan newDuration = duration;
while (chunksList[0].ChunkDuration <= trim.Start)
{
trim.Start = trim.Start.Subtract(chunksList[0].ChunkDuration);
trim.End = trim.End.Subtract(chunksList[0].ChunkDuration);
newDuration = newDuration.Subtract(chunksList[0].ChunkDuration);
chunksList.RemoveAt(0);
}
while (chunksList.Last().ChunkDuration <= newDuration.Subtract(trim.End))
{
newDuration = newDuration.Subtract(chunksList.Last().ChunkDuration);
chunksList.RemoveAt(chunksList.Count - 1);
}
return (trim, newDuration);
}
private static async Task<byte[]> DownloadChunkAsync(Uri chunkUrl, CancellationToken cancellationToken = default)
{
int retriesCount = 0;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using (WebClient client = new WebClient())
{
return await client.DownloadDataTaskAsync(chunkUrl);
}
}
catch (WebException wex)
{
if ((bool)Config.GetValue("twitch_vod_downloading_chunk_retry_after_error") && retriesCount < (int)Config.GetValue("twitch_vod_downloading_chunk_max_retries"))
{
retriesCount++;
await Task.Delay((int)Config.GetValue("twitch_vod_downloading_chunk_retries_delay"));
}
else throw wex;
}
}
}
private static Task WriteChunkToFileAsync(StorageFile file, byte[] chunk)
{
return Task.Factory.StartNew(() =>
{
using (var stream = new FileStream(file.Path, FileMode.Append))
{
stream.Write(chunk, 0, chunk.Length);
stream.Close();
}
});
}
private static TimeSpan ParseDuration(string duration)
{
char[] separators = { 'h', 'm', 's' };
string[] durationParts = duration.Split(separators, StringSplitOptions.RemoveEmptyEntries).Reverse().ToArray();
TimeSpan timeSpan = new TimeSpan(
durationParts.Count() > 2 ? int.Parse(durationParts[2]) : 0,
durationParts.Count() > 1 ? int.Parse(durationParts[1]) : 0,
int.Parse(durationParts[0]));
return timeSpan;
}
#endregion
#region EVENTS
public event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
#endregion
}
}

View File

@@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.Structs
{
[Serializable]
public struct BaseStream
{
#region PROPERTIES
public Uri Url { get; set; }
public int Height { get; set; }
public int FrameRate { get; set; }
#endregion
#region METHODS
public override string ToString() => $"{Height}p{(FrameRate > 0 ? FrameRate.ToString() : "N/A")}";
#endregion
}
}

View File

@@ -1,32 +0,0 @@
using System;
namespace VDownload.Core.Structs
{
public struct TwitchAccessTokenValidationData
{
#region CONSTRUCTORS
public TwitchAccessTokenValidationData(string accessToken, bool isValid, string login, DateTime? expirationDate)
{
AccessToken = accessToken;
IsValid = isValid;
Login = login;
ExpirationDate = expirationDate;
}
public static TwitchAccessTokenValidationData Null = new TwitchAccessTokenValidationData(string.Empty, false, string.Empty, null);
#endregion
#region PROPERTIES
public string AccessToken { get; private set; }
public bool IsValid { get; private set; }
public string Login { get; private set; }
public DateTime? ExpirationDate { get; private set; }
#endregion
}
}

View File

@@ -0,0 +1,41 @@
using Windows.ApplicationModel.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
namespace VDownload.Core.Strings
{
public class StringResource
{
#region FIELDS
protected ResourceLoader _resourceLoader;
#endregion
#region CONSTRUCTORS
internal StringResource(ResourceLoader resourceLoader)
{
_resourceLoader = resourceLoader;
}
#endregion
#region PUBLIC METHODS
public string Get(string key)
{
return _resourceLoader.GetString(key);
}
#endregion
}
}

View File

@@ -0,0 +1,57 @@
using Microsoft.UI.Xaml;
using Windows.ApplicationModel.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
namespace VDownload.Core.Strings
{
public static class StringResourcesManager
{
#region PROPERTIES
public static StringResource BaseView { get; } = BuildResource("BaseViewResources");
public static StringResource HomeView { get; } = BuildResource("HomeViewResources");
public static StringResource HomeVideoView { get; } = BuildResource("HomeVideoViewResources");
public static StringResource HomeVideoCollectionView { get; } = BuildResource("HomeVideoCollectionViewResources");
public static StringResource HomeDownloadsView { get; } = BuildResource("HomeDownloadsViewResources");
public static StringResource AuthenticationView { get; } = BuildResource("AuthenticationViewResources");
public static StringResource Notifications { get; } = BuildResource("NotificationsResources");
public static StringResource Search { get; } = BuildResource("SearchResources");
public static StringResource Common { get; } = BuildResource("CommonResources");
public static StringResource DialogButtons { get; } = BuildResource("DialogButtonsResources");
public static StringResource SettingsView { get; } = BuildResource("SettingsViewResources");
public static StringResource FilenameTemplate { get; } = BuildResource("FilenameTemplateResources");
public static StringResource AboutView { get; } = BuildResource("AboutViewResources");
public static StringResource SubscriptionsView { get; } = BuildResource("SubscriptionsViewResources");
#endregion
#region PRIVATE METHODS
private static StringResource BuildResource(string resourceName)
{
File.AppendAllText("C:\\Users\\mateusz\\Desktop\\test.txt", $"teststring {resourceName}\n");
ResourceLoader loader;
try
{
loader = new ResourceLoader($"VDownload.Core.Strings/{resourceName}");
File.AppendAllText("C:\\Users\\mateusz\\Desktop\\test.txt", $"afterteststring {resourceName}\n");
}
catch (Exception e)
{
File.AppendAllText("C:\\Users\\mateusz\\Desktop\\test.txt", $"teststringerror {e.Message}\n");
throw;
}
return new StringResource(loader);
}
#endregion
}
}

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Developers.Text" xml:space="preserve">
<value>Developers</value>
</data>
<data name="Donation.Text" xml:space="preserve">
<value>Donation</value>
</data>
<data name="More.Text" xml:space="preserve">
<value>More</value>
</data>
<data name="Repository.Text" xml:space="preserve">
<value>Repository</value>
</data>
<data name="SelfbuiltVersion" xml:space="preserve">
<value>Self-built version</value>
</data>
<data name="Translation.Text" xml:space="preserve">
<value>Translation (English (US))</value>
</data>
</root>

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AuthenticationButtonSignIn.Text" xml:space="preserve">
<value>Sign in</value>
</data>
<data name="AuthenticationButtonSignOut.Text" xml:space="preserve">
<value>Sign out</value>
</data>
<data name="AuthenticationDescriptionLoading.Value" xml:space="preserve">
<value>Loading...</value>
</data>
<data name="Header.Text" xml:space="preserve">
<value>Authentication</value>
</data>
<data name="TwitchAuthenticationDescriptionAuthenticated" xml:space="preserve">
<value>Signed in as {0}. Expiration date: {1}</value>
</data>
<data name="TwitchAuthenticationDescriptionAuthenticationInvalid" xml:space="preserve">
<value>Token expired or is invalid. Please log in again</value>
</data>
<data name="TwitchAuthenticationDescriptionCannotValidate" xml:space="preserve">
<value>Cannot validate token. Check your internet connection and try again later.</value>
</data>
<data name="TwitchAuthenticationDescriptionNotAuthenticated" xml:space="preserve">
<value>You are not authenticated. Please sign in</value>
</data>
<data name="TwitchAuthenticationDescriptionNotAuthenticatedNoInternetConnection" xml:space="preserve">
<value>You are not authenticated and there is no internet connection. Check your internet connection and try again later.</value>
</data>
<data name="TwitchAuthenticationDialogMessage" xml:space="preserve">
<value>An unknown error occured during Twitch login</value>
</data>
<data name="TwitchAuthenticationDialogTitle" xml:space="preserve">
<value>Twitch authentication error</value>
</data>
<data name="TwitchAuthenticationWindowTitle" xml:space="preserve">
<value>Sign in to Twitch</value>
</data>
</root>

View File

@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AboutNavigationViewItem" xml:space="preserve">
<value>About</value>
</data>
<data name="AuthenticationNavigationViewItem" xml:space="preserve">
<value>Authentication</value>
</data>
<data name="HomeNavigationViewItem" xml:space="preserve">
<value>Home</value>
</data>
<data name="SubscriptionsNavigationViewItem" xml:space="preserve">
<value>Subscriptions</value>
</data>
</root>

View File

@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="MediaTypeOnlyAudio.Text" xml:space="preserve">
<value>Only audio</value>
</data>
<data name="MediaTypeOnlyVideo.Text" xml:space="preserve">
<value>Only video</value>
</data>
<data name="MediaTypeOriginal.Text" xml:space="preserve">
<value>Original</value>
</data>
<data name="ProcessingSpeedFast.Text" xml:space="preserve">
<value>Fast</value>
</data>
<data name="ProcessingSpeedFaster.Text" xml:space="preserve">
<value>Faster</value>
</data>
<data name="ProcessingSpeedMedium.Text" xml:space="preserve">
<value>Medium</value>
</data>
<data name="ProcessingSpeedSlow.Text" xml:space="preserve">
<value>Slow</value>
</data>
<data name="ProcessingSpeedSlower.Text" xml:space="preserve">
<value>Slower</value>
</data>
<data name="ProcessingSpeedSuperFast.Text" xml:space="preserve">
<value>Super fast</value>
</data>
<data name="ProcessingSpeedUltraFast.Text" xml:space="preserve">
<value>Ultra fast</value>
</data>
<data name="ProcessingSpeedVeryFast.Text" xml:space="preserve">
<value>Very fast</value>
</data>
<data name="ProcessingSpeedVerySlow.Text" xml:space="preserve">
<value>Very slow</value>
</data>
<data name="StartAtMeteredConnectionDialogMessage" xml:space="preserve">
<value>You are trying to download a video using a metered connection. Do you want to continue?</value>
</data>
<data name="StartAtMeteredConnectionDialogTitle" xml:space="preserve">
<value>Warning</value>
</data>
<data name="SubscriptionVideoListName" xml:space="preserve">
<value>Subscriptions</value>
</data>
</root>

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Close" xml:space="preserve">
<value>Close</value>
</data>
<data name="No" xml:space="preserve">
<value>No</value>
</data>
<data name="OK" xml:space="preserve">
<value>OK</value>
</data>
<data name="Yes" xml:space="preserve">
<value>Yes</value>
</data>
</root>

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="author" xml:space="preserve">
<value>{author} = author</value>
</data>
<data name="date" xml:space="preserve">
<value>{date:&lt;format&gt;} = publish date in specified format</value>
</data>
<data name="duration" xml:space="preserve">
<value>{duration:&lt;format&gt;} = duration in specified format</value>
</data>
<data name="id" xml:space="preserve">
<value>{id} = id (from url)</value>
</data>
<data name="source" xml:space="preserve">
<value>{source} = source</value>
</data>
<data name="title" xml:space="preserve">
<value>{title} = title</value>
</data>
<data name="views" xml:space="preserve">
<value>{views} = views</value>
</data>
</root>

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DialogErrorMessageNoInternetConnection" xml:space="preserve">
<value>No internet connection. Try again later.</value>
</data>
<data name="DialogErrorTitle" xml:space="preserve">
<value>Error</value>
</data>
<data name="ErrorDownloadingTimeout" xml:space="preserve">
<value>Downloading timeout. Check your internet connection and try again later.</value>
</data>
<data name="ErrorFFmpeg" xml:space="preserve">
<value>FFmpeg exited with error.</value>
</data>
<data name="ErrorFFmpegPath" xml:space="preserve">
<value>No FFmpeg executables found. Check path in settings and try again.</value>
</data>
<data name="StatusCancelled.Text" xml:space="preserve">
<value>Cancelled</value>
</data>
<data name="StatusDone.Text" xml:space="preserve">
<value>Done</value>
</data>
<data name="StatusDownloading.Text" xml:space="preserve">
<value>Downloading</value>
</data>
<data name="StatusError.Text" xml:space="preserve">
<value>Error</value>
</data>
<data name="StatusFinalizing.Text" xml:space="preserve">
<value>Finalizing</value>
</data>
<data name="StatusIdle.Text" xml:space="preserve">
<value>Idle</value>
</data>
<data name="StatusInitializing.Text" xml:space="preserve">
<value>Initializing</value>
</data>
<data name="StatusProcessing.Text" xml:space="preserve">
<value>Processing</value>
</data>
<data name="StatusQueued.Text" xml:space="preserve">
<value>Queued</value>
</data>
</root>

View File

@@ -0,0 +1,198 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ApplyDirectoryButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Selected directory will be applied to all videos in playlist</value>
</data>
<data name="CreateAndStartButton.Content" xml:space="preserve">
<value>Create download tasks and start</value>
</data>
<data name="CreateButton.Content" xml:space="preserve">
<value>Create download tasks</value>
</data>
<data name="DirectorySettingsCard.Header" xml:space="preserve">
<value>Directory</value>
</data>
<data name="DirectorySettingsCardButton.Content" xml:space="preserve">
<value>Browse</value>
</data>
<data name="FilenameSettingsCard.Header" xml:space="preserve">
<value>Filename</value>
</data>
<data name="FileOptionsHeader.Text" xml:space="preserve">
<value>File options</value>
</data>
<data name="FileTypeSettingsCard.Description" xml:space="preserve">
<value>If original video is not in selected type, it will be converted</value>
</data>
<data name="FileTypeSettingsCard.Header" xml:space="preserve">
<value>File type</value>
</data>
<data name="FilterAuthorTextBlock.Text" xml:space="preserve">
<value>Author</value>
</data>
<data name="FilterAuthorTextBox.PlaceholderText" xml:space="preserve">
<value>Enter regular expression</value>
</data>
<data name="FilterDateTextBlock.Text" xml:space="preserve">
<value>Date</value>
</data>
<data name="FilterDurationTextBlock.Text" xml:space="preserve">
<value>Duration</value>
</data>
<data name="FilterRemovedButton.Content" xml:space="preserve">
<value>Restore</value>
</data>
<data name="FilterRemovedTextBlock.Text" xml:space="preserve">
<value>Removed</value>
</data>
<data name="FilterTitleTextBlock.Text" xml:space="preserve">
<value>Title</value>
</data>
<data name="FilterTitleTextBox.PlaceholderText" xml:space="preserve">
<value>Enter regular expression</value>
</data>
<data name="FilterViewsTextBlock.Text" xml:space="preserve">
<value>Views</value>
</data>
<data name="FilterWindow.Title" xml:space="preserve">
<value>Filter</value>
</data>
<data name="HiddenTextBlock.Text" xml:space="preserve">
<value>Hidden: </value>
</data>
<data name="MediaOptionsHeader.Text" xml:space="preserve">
<value>Media options</value>
</data>
<data name="MediaTypeSettingsCard.Header" xml:space="preserve">
<value>Media type</value>
</data>
<data name="QualitySettingsCard.Header" xml:space="preserve">
<value>Quality</value>
</data>
<data name="TrimEndSettingsCard.Header" xml:space="preserve">
<value>End at</value>
</data>
<data name="TrimSettingsGroup.Header" xml:space="preserve">
<value>Trim</value>
</data>
<data name="TrimStartSettingsCard.Header" xml:space="preserve">
<value>Start at</value>
</data>
</root>

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CreateAndStartButton.Content" xml:space="preserve">
<value>Create download task and start</value>
</data>
<data name="CreateButton.Content" xml:space="preserve">
<value>Create download task</value>
</data>
<data name="DirectorySettingsCard.Header" xml:space="preserve">
<value>Directory</value>
</data>
<data name="DirectorySettingsCardButton.Content" xml:space="preserve">
<value>Browse</value>
</data>
<data name="FilenameSettingsCard.Header" xml:space="preserve">
<value>Filename</value>
</data>
<data name="FileOptionsHeader.Text" xml:space="preserve">
<value>File options</value>
</data>
<data name="FileTypeSettingsCard.Description" xml:space="preserve">
<value>If original video is not in selected type, it will be converted</value>
</data>
<data name="FileTypeSettingsCard.Header" xml:space="preserve">
<value>File type</value>
</data>
<data name="MediaOptionsHeader.Text" xml:space="preserve">
<value>Media options</value>
</data>
<data name="MediaTypeSettingsCard.Header" xml:space="preserve">
<value>Media type</value>
</data>
<data name="QualitySettingsCard.Header" xml:space="preserve">
<value>Quality</value>
</data>
<data name="TrimEndSettingsCard.Header" xml:space="preserve">
<value>End at</value>
</data>
<data name="TrimSettingsGroup.Header" xml:space="preserve">
<value>Trim</value>
</data>
<data name="TrimStartSettingsCard.Header" xml:space="preserve">
<value>Start at</value>
</data>
</root>

View File

@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ErrorInfoBar.Title" xml:space="preserve">
<value>Error</value>
</data>
<data name="ErrorInfoBarNoInternetConnection" xml:space="preserve">
<value>No internet connection</value>
</data>
<data name="OptionBarCancelAll.Label" xml:space="preserve">
<value>Cancel all</value>
</data>
<data name="OptionBarCancelAll.Width" xml:space="preserve">
<value>80</value>
</data>
<data name="OptionBarDownloadAll.Label" xml:space="preserve">
<value>Download all</value>
</data>
<data name="OptionBarDownloadAll.Width" xml:space="preserve">
<value>100</value>
</data>
<data name="OptionBarLoadSubscription.Label" xml:space="preserve">
<value>Load from subscriptions</value>
</data>
<data name="OptionBarLoadSubscription.Width" xml:space="preserve">
<value>150</value>
</data>
<data name="OptionBarMessageLoading" xml:space="preserve">
<value>Loading...</value>
</data>
<data name="OptionBarMessageVideosFound" xml:space="preserve">
<value>New videos:</value>
</data>
<data name="OptionBarMessageVideosNotFound" xml:space="preserve">
<value>No new videos</value>
</data>
<data name="OptionBarPlaylistSearch.Label" xml:space="preserve">
<value>Playlist search</value>
</data>
<data name="OptionBarPlaylistSearch.Width" xml:space="preserve">
<value>100</value>
</data>
<data name="OptionBarPlaylistSearchContentNumberBox.ToolTipService.ToolTip" xml:space="preserve">
<value>Number of videos to get from playlist
0 = Get all</value>
</data>
<data name="OptionBarPlaylistSearchContentTextBox.PlaceholderText" xml:space="preserve">
<value>Playlist URL</value>
</data>
<data name="OptionBarSearchButton.Content" xml:space="preserve">
<value>Search</value>
</data>
<data name="OptionBarVideoSearch.Label" xml:space="preserve">
<value>Video search</value>
</data>
<data name="OptionBarVideoSearch.Width" xml:space="preserve">
<value>100</value>
</data>
<data name="OptionBarVideoSearchContentTextBox.PlaceholderText" xml:space="preserve">
<value>Video URL</value>
</data>
</root>

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Author" xml:space="preserve">
<value>Author</value>
</data>
<data name="Message" xml:space="preserve">
<value>Message</value>
</data>
<data name="OnSuccessfulTitle" xml:space="preserve">
<value>Task ended successfully</value>
</data>
<data name="OnUnsuccessfulTitle" xml:space="preserve">
<value>Task ended unsuccessfully</value>
</data>
<data name="Title" xml:space="preserve">
<value>Title</value>
</data>
</root>

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="EmptyPlaylist" xml:space="preserve">
<value>Playlist is empty</value>
</data>
<data name="EmptyUrl" xml:space="preserve">
<value>Invalid url. Url cannot be empty.</value>
</data>
<data name="SearchTimeout" xml:space="preserve">
<value>Search timeout. Check your internet connection.</value>
</data>
<data name="SourceNotSupported" xml:space="preserve">
<value>Invalid url. Url does not match any of supported source.</value>
</data>
<data name="TwitchChannelNotFound" xml:space="preserve">
<value>Invalid url. Twitch channel not found.</value>
</data>
<data name="TwitchClipNotFound" xml:space="preserve">
<value>Invalid url. Twitch clip not found.</value>
</data>
<data name="TwitchNotAuthenticated" xml:space="preserve">
<value>Twitch authentication error. Not authenticated to Twitch.</value>
</data>
<data name="TwitchTokenValidationUnsuccessful" xml:space="preserve">
<value>Twitch authentication error. Authentication token is invalid.</value>
</data>
<data name="TwitchVodNotFound" xml:space="preserve">
<value>Invalid url. Twitch VOD not found.</value>
</data>
<data name="UnknownMediaType" xml:space="preserve">
<value>Invalid url. Url does not match any of supported source's media types.</value>
</data>
</root>

View File

@@ -117,100 +117,124 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="Base_CancelButtonText" xml:space="preserve"> <data name="Header.Text" xml:space="preserve">
<value>Cancel</value> <value>Settings</value>
</data> </data>
<data name="Base_CloseButtonText" xml:space="preserve"> <data name="NotificationsHeader.Text" xml:space="preserve">
<value>OK</value> <value>Notifications</value>
</data> </data>
<data name="Home_DownloadAll_MeteredConnection_Content" xml:space="preserve"> <data name="NotificationsOnSuccessful.Header" xml:space="preserve">
<value>You are on the metered connection now. You can delay tasks until the network changes. Do you want to start the tasks?</value> <value>Show notifications when task ended successfully</value>
</data> </data>
<data name="Home_DownloadAll_MeteredConnection_StartWithDelayButtonText" xml:space="preserve"> <data name="NotificationsOnUnsuccessful.Header" xml:space="preserve">
<value>Yes (With delay)</value> <value>Show notifications when task ended unsuccessfully</value>
</data> </data>
<data name="Home_DownloadAll_MeteredConnection_StartWithoutDelayButtonText" xml:space="preserve"> <data name="ProcessingFFmpegLocation.Header" xml:space="preserve">
<value>Yes (Without delay)</value> <value>FFmpeg location</value>
</data> </data>
<data name="Home_DownloadAll_MeteredConnection_Title" xml:space="preserve"> <data name="ProcessingFFmpegLocationButton.Content" xml:space="preserve">
<value>Metered connection detected</value> <value>Browse</value>
</data> </data>
<data name="Home_DownloadTaskControl_Start_MeteredConnection_Content" xml:space="preserve"> <data name="ProcessingHeader.Text" xml:space="preserve">
<value>You are on the metered connection now. You can delay task until the network changes. Do you want to start the task?</value> <value>Processing</value>
</data> </data>
<data name="Home_DownloadTaskControl_Start_MeteredConnection_StartWithDelayButtonText" xml:space="preserve"> <data name="ProcessingSpeed.Header" xml:space="preserve">
<value>Yes (With delay)</value> <value>Speed</value>
</data> </data>
<data name="Home_DownloadTaskControl_Start_MeteredConnection_StartWithoutDelayButtonText1" xml:space="preserve"> <data name="ProcessingUseHardwareAcceleration.Header" xml:space="preserve">
<value>Yes (Without delay)</value> <value>Use hardware acceleration</value>
</data> </data>
<data name="Home_DownloadTaskControl_Start_MeteredConnection_Title" xml:space="preserve"> <data name="ProcessingUseMultithreading.Header" xml:space="preserve">
<value>Metered connection detected</value> <value>Use multithreading</value>
</data> </data>
<data name="Home_OptionsBar_PlaylistSearchControl_Base_Title" xml:space="preserve"> <data name="RestoreToDefaultButton.Content" xml:space="preserve">
<value>Playlist search error</value> <value>Restore settings to default</value>
</data> </data>
<data name="Home_OptionsBar_PlaylistSearchControl_InternetNotAvailable_Content" xml:space="preserve"> <data name="SearchingHeader.Text" xml:space="preserve">
<value>Unable to connect to servers. Check your internet connection.</value> <value>Searching</value>
</data> </data>
<data name="Home_OptionsBar_PlaylistSearchControl_TwitchAccessTokenNotFound_Content" xml:space="preserve"> <data name="SearchingPlaylistCount.Description" xml:space="preserve">
<value>To get information about Twitch playlists (Channels), you have to link your Twitch account with VDownload. Go to Sources page to sign in.</value> <value>0 = Get all</value>
</data> </data>
<data name="Home_OptionsBar_PlaylistSearchControl_TwitchAccessTokenNotValid_Content" xml:space="preserve"> <data name="SearchingPlaylistCount.Header" xml:space="preserve">
<value>There is a problem with linked Twitch account. Check Twitch login status in Sources page.</value> <value>Default number of videos fetched from playlist</value>
</data> </data>
<data name="Home_OptionsBar_SubscriptionsLoadControl_Base_Title" xml:space="preserve"> <data name="TasksAudioExtension.Header" xml:space="preserve">
<value>Subscriptions loading error</value> <value>Audio extension</value>
</data> </data>
<data name="Home_OptionsBar_SubscriptionsLoadControl_InternetNotAvailable_Content" xml:space="preserve"> <data name="TasksDefaultMediaOptions.Header" xml:space="preserve">
<value>Unable to connect to servers. Check your internet connection.</value> <value>Default media options</value>
</data> </data>
<data name="Home_OptionsBar_SubscriptionsLoadControl_MediaNotFound_Content" xml:space="preserve"> <data name="TasksDefaultOutputDirectory.Header" xml:space="preserve">
<value>Subscribed playlist not found. Name:</value> <value>Default output directory</value>
</data> </data>
<data name="Home_OptionsBar_SubscriptionsLoadControl_TwitchAccessTokenNotFound_Content" xml:space="preserve"> <data name="TasksDefaultOutputDirectoryButton.Content" xml:space="preserve">
<value>To get information about Twitch videos and playlists, you have to link your Twitch account with VDownload. Go to Sources page to sign in.</value> <value>Browse</value>
</data> </data>
<data name="Home_OptionsBar_SubscriptionsLoadControl_TwitchAccessTokenNotValid_Content" xml:space="preserve"> <data name="TasksFilenameTemplate.Header" xml:space="preserve">
<value>There is a problem with linked Twitch account. Check Twitch login status in Sources page.</value> <value>Filename template</value>
</data> </data>
<data name="Home_OptionsBar_VideoSearchControl_Base_Title" xml:space="preserve"> <data name="TasksHeader.Text" xml:space="preserve">
<value>Video search error</value> <value>Tasks</value>
</data> </data>
<data name="Home_OptionsBar_VideoSearchControl_InternetNotAvailable_Content" xml:space="preserve"> <data name="TasksMediaType.Header" xml:space="preserve">
<value>Unable to connect to servers. Check your internet connection.</value> <value>Media type</value>
</data> </data>
<data name="Home_OptionsBar_VideoSearchControl_TwitchAccessTokenNotFound_Content" xml:space="preserve"> <data name="TasksMeteredConnectionWarning.Header" xml:space="preserve">
<value>To get information about Twitch videos (VODs and Clips), you have to link your Twitch account with VDownload. Go to Sources page to sign in.</value> <value>Show warning before downloading on a metered connection</value>
</data> </data>
<data name="Home_OptionsBar_VideoSearchControl_TwitchAccessTokenNotValid_Content" xml:space="preserve"> <data name="TasksReplaceOutputFile.Description" xml:space="preserve">
<value>There is a problem with linked Twitch account. Check Twitch login status in Sources page.</value> <value>If file with same name as output file, already exists, it will be replaced. Otherwise new file with unique name will be created.</value>
</data> </data>
<data name="Sources_TwitchLogin_Base_Title" xml:space="preserve"> <data name="TasksReplaceOutputFile.Header" xml:space="preserve">
<value>Login to Twitch failed</value> <value>Replace output file if exists</value>
</data> </data>
<data name="Sources_TwitchLogin_InternetNotAvailable_Content" xml:space="preserve"> <data name="TasksRunningTasks.Header" xml:space="preserve">
<value>Unable to connect to servers. Check your internet connection.</value> <value>Maximum number of tasks running in parallel</value>
</data> </data>
<data name="Sources_TwitchLogin_Unknown_Content" xml:space="preserve"> <data name="TasksSaveLastOutputDirectory.Description" xml:space="preserve">
<value>Unknown error. </value> <value>Otherwise, default directory defined below (after expanding) will be used</value>
</data> </data>
<data name="Subscription_Adding_Base_Title" xml:space="preserve"> <data name="TasksSaveLastOutputDirectory.Header" xml:space="preserve">
<value>Playlist adding error</value> <value>Save last output directory</value>
</data> </data>
<data name="Subscription_Adding_InternetNotAvailable_Content" xml:space="preserve"> <data name="TasksVideoExtension.Header" xml:space="preserve">
<value>Unable to connect to servers. Check your internet connection.</value> <value>Video extension</value>
</data> </data>
<data name="Subscription_Adding_PlaylistNotFound_Content" xml:space="preserve"> <data name="TempDeleteOnFail.Header" xml:space="preserve">
<value>Playlist not found. Check the URL.</value> <value>Delete task's temporary files when an error occurs</value>
</data> </data>
<data name="Subscription_Adding_SubscriptionExists_Content" xml:space="preserve"> <data name="TempDirectory.Header" xml:space="preserve">
<value>This playlist has been already subscribed</value> <value>Temporary files location</value>
</data> </data>
<data name="Subscription_Adding_TwitchAccessTokenNotFound_Content" xml:space="preserve"> <data name="TempDirectoryButton.Content" xml:space="preserve">
<value>To be able to subscribe Twitch playlists (Channels), you have to link your Twitch account with VDownload. Go to Sources page to sign in.</value> <value>Browse</value>
</data> </data>
<data name="Subscription_Adding_TwitchAccessTokenNotValid_Content" xml:space="preserve"> <data name="TempHeader.Text" xml:space="preserve">
<value>There is a problem with linked Twitch account. Check Twitch login status in Sources page.</value> <value>Temporary files</value>
</data>
<data name="TwitchHeader.Text" xml:space="preserve">
<value>Twitch</value>
</data>
<data name="TwitchVodChunkDownloadingErrorRetry.Header" xml:space="preserve">
<value>Retry when downloading single VOD chunk fails</value>
</data>
<data name="TwitchVodChunkDownloadingErrorRetryCount.Header" xml:space="preserve">
<value>Number of retries</value>
</data>
<data name="TwitchVodChunkDownloadingErrorRetryDelay.Header" xml:space="preserve">
<value>Time between fail and retry (in miliseconds)</value>
</data>
<data name="TwitchVodParallelDownloads.Description" xml:space="preserve">
<value>WARNING: Too many chunks downloaded at once may cause performance problems</value>
</data>
<data name="TwitchVodParallelDownloads.Header" xml:space="preserve">
<value>Maximum number of VOD chunks downloaded in parallel</value>
</data>
<data name="TwitchVodPassiveTrimming.Description" xml:space="preserve">
<value>Only chunks from trim start to trim end range will be downloaded</value>
</data>
<data name="TwitchVodPassiveTrimming.Header" xml:space="preserve">
<value>VOD passive trimming</value>
</data> </data>
</root> </root>

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DuplicateError" xml:space="preserve">
<value>Playlist has been already added</value>
</data>
<data name="ErrorInfoBar.Title" xml:space="preserve">
<value>Error</value>
</data>
<data name="Header.Text" xml:space="preserve">
<value>Subscriptions</value>
</data>
<data name="NoInternetConnectionError" xml:space="preserve">
<value>No internet connection</value>
</data>
<data name="PlaylistSearchButton.Content" xml:space="preserve">
<value>Add</value>
</data>
<data name="PlaylistUrlTextBox.PlaceholderText" xml:space="preserve">
<value>Playlist URL</value>
</data>
<data name="RemovePlaylistButton.Content" xml:space="preserve">
<value>Remove</value>
</data>
</root>

View File

@@ -0,0 +1,62 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>VDownload.Core.Strings</RootNamespace>
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<UseRidGraph>true</UseRidGraph>
<EnableCoreMrtTooling Condition=" '$(BuildingInsideVisualStudio)' != 'true' ">false</EnableCoreMrtTooling>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240227000" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
</ItemGroup>
<ItemGroup>
<PRIResource Update="Strings\en-US\AboutViewResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\AuthenticationViewResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\BaseViewResources.resw">
<Generator></Generator>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\CommonResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\DialogButtonsResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\FilenameTemplateResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\HomeDownloadsViewResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\HomeVideoCollectionViewResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\HomeVideoViewResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\HomeViewResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\NotificationsResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\SearchResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\SettingsViewResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
<PRIResource Update="Strings\en-US\SubscriptionsViewResources.resw">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PRIResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,292 @@
using CommunityToolkit.Mvvm.ComponentModel;
using VDownload.Models;
using VDownload.Services.Utility.FFmpeg;
using VDownload.Services.Data.Configuration;
using VDownload.Services.Data.Settings;
using Windows.System;
using System.Threading;
using System.Threading.Tasks;
using System;
using System.Linq;
using Windows.Storage;
using System.IO;
using VDownload.Services.UI.Notifications;
using System.Collections.Generic;
using System.Net.Http;
using Instances.Exceptions;
using FFMpegCore.Exceptions;
using VDownload.Core.Strings;
namespace VDownload.Core.Tasks
{
public partial class DownloadTask : ObservableObject
{
#region ENUMS
private enum TaskResult
{
Success,
Cancellation,
Error
}
#endregion
#region SERVICES
protected readonly IConfigurationService _configurationService;
protected readonly ISettingsService _settingsService;
protected readonly IFFmpegService _ffmpegService;
protected readonly INotificationsService _notificationsService;
#endregion
#region FIELDS
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private CancellationTokenSource? _cancellationTokenSource;
private Task _downloadTask;
#endregion
#region PROPERTIES
[ObservableProperty]
protected Guid _id;
[ObservableProperty]
protected Video _video;
[ObservableProperty]
protected VideoDownloadOptions _downloadOptions;
[ObservableProperty]
protected DownloadTaskStatus _status;
[ObservableProperty]
protected DateTime _createDate;
[ObservableProperty]
protected double _progress;
[ObservableProperty]
protected string? _error;
#endregion
#region CONSTRUCTORS
internal DownloadTask(Video video, VideoDownloadOptions downloadOptions, IConfigurationService configurationService, ISettingsService settingsService, IFFmpegService ffmpegService, INotificationsService notificationsService)
{
_configurationService = configurationService;
_settingsService = settingsService;
_ffmpegService = ffmpegService;
_notificationsService = notificationsService;
_video = video;
_downloadOptions = downloadOptions;
_id = Guid.NewGuid();
_status = DownloadTaskStatus.Idle;
_createDate = DateTime.Now;
_progress = 0;
}
#endregion
#region PUBLIC METHODS
public void Enqueue()
{
DownloadTaskStatus[] statuses =
[
DownloadTaskStatus.Idle,
DownloadTaskStatus.EndedUnsuccessfully,
DownloadTaskStatus.EndedSuccessfully,
DownloadTaskStatus.EndedCancelled,
];
if (statuses.Contains(Status))
{
Status = DownloadTaskStatus.Queued;
}
}
internal void Start()
{
_cancellationTokenSource = new CancellationTokenSource();
UpdateStatusWithDispatcher(DownloadTaskStatus.Initializing);
_downloadTask = Download();
}
public async Task Cancel()
{
if (_cancellationTokenSource is not null)
{
_cancellationTokenSource.Cancel();
await _downloadTask;
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
}
}
#endregion
#region PRIVATE METHODS
private async Task Download()
{
CancellationToken token = _cancellationTokenSource.Token;
await _settingsService.Load();
string tempDirectory = $"{_settingsService.Data.Common.Temp.Directory}\\{_configurationService.Common.Path.Temp.TasksDirectory}\\{Guid.NewGuid()}";
if (Directory.Exists(tempDirectory))
{
Directory.Delete(tempDirectory, true);
}
Directory.CreateDirectory(tempDirectory);
List<string> content = new List<string>()
{
$"{StringResourcesManager.Notifications.Get("Title")}: {Video.Title}",
$"{StringResourcesManager.Notifications.Get("Author")}: {Video.Author}"
};
string errorMessage = null;
TaskResult? endingStatus = null;
try
{
IProgress<double> onProgressDownloading = new Progress<double>((value) =>
{
UpdateStatusWithDispatcher(DownloadTaskStatus.Downloading);
UpdateProgressWithDispatcher(value);
});
VideoStreamDownloadResult downloadResult = await DownloadOptions.SelectedStream.Download(tempDirectory, onProgressDownloading, token, Video.Duration, DownloadOptions.TrimStart, DownloadOptions.TrimEnd);
Action<double> onProgressProcessing = (value) =>
{
UpdateStatusWithDispatcher(DownloadTaskStatus.Processing);
UpdateProgressWithDispatcher(value);
};
string outputPath = $"{tempDirectory}\\processed.{DownloadOptions.Extension}";
FFmpegBuilder ffmpegBuilder = _ffmpegService.CreateBuilder();
if (downloadResult.NewTrimStart != TimeSpan.Zero)
{
ffmpegBuilder.TrimStart(downloadResult.NewTrimStart);
}
if (downloadResult.NewTrimEnd != downloadResult.NewDuration)
{
ffmpegBuilder.TrimEnd(downloadResult.NewTrimEnd);
}
ffmpegBuilder.SetMediaType(DownloadOptions.MediaType)
.JoinCancellationToken(token)
.JoinProgressReporter(onProgressProcessing, downloadResult.NewDuration);
await ffmpegBuilder.RunAsync(downloadResult.File, outputPath);
StorageFile outputFile = await StorageFile.GetFileFromPathAsync(outputPath);
UpdateStatusWithDispatcher(DownloadTaskStatus.Finalizing);
await Finalizing(outputFile);
UpdateStatusWithDispatcher(DownloadTaskStatus.EndedSuccessfully);
endingStatus = TaskResult.Success;
}
catch (OperationCanceledException)
{
UpdateStatusWithDispatcher(DownloadTaskStatus.EndedCancelled);
endingStatus = TaskResult.Cancellation;
}
catch (Exception ex)
{
string message;
if (ex is TaskCanceledException || ex is HttpRequestException)
{
message = StringResourcesManager.HomeDownloadsView.Get("ErrorDownloadingTimeout");
}
else if (ex is InstanceFileNotFoundException)
{
message = StringResourcesManager.HomeDownloadsView.Get("ErrorFFmpegPath");
}
else if (ex is FFMpegException)
{
message = StringResourcesManager.HomeDownloadsView.Get("ErrorFFmpeg");
}
else
{
message = ex.Message;
}
UpdateErrorWithDispatcher(message);
UpdateStatusWithDispatcher(DownloadTaskStatus.EndedUnsuccessfully);
endingStatus = TaskResult.Error;
errorMessage = message;
}
finally
{
switch (endingStatus)
{
case TaskResult.Error:
if (_settingsService.Data.Common.Notifications.OnUnsuccessful)
{
string title = StringResourcesManager.Notifications.Get("OnUnsuccessfulTitle");
content.Add($"{StringResourcesManager.Notifications.Get("Message")}: {errorMessage}");
_notificationsService.SendNotification(title, content);
}
break;
case TaskResult.Success:
if (_settingsService.Data.Common.Notifications.OnSuccessful)
{
string title = StringResourcesManager.Notifications.Get("OnSuccessfulTitle");
_notificationsService.SendNotification(title, content);
}
break;
}
if (Status != DownloadTaskStatus.EndedUnsuccessfully || _settingsService.Data.Common.Temp.DeleteOnError)
{
Directory.Delete(tempDirectory, true);
}
}
}
protected async Task Finalizing(StorageFile outputFile)
{
StorageFolder destination = await StorageFolder.GetFolderFromPathAsync(DownloadOptions.Directory);
string filename = $"{DownloadOptions.Filename}.{DownloadOptions.Extension}";
NameCollisionOption nameCollisionOption = _settingsService.Data.Common.Tasks.ReplaceOutputFileIfExists
? NameCollisionOption.ReplaceExisting
: NameCollisionOption.GenerateUniqueName;
await outputFile.CopyAsync(destination, filename, nameCollisionOption);
}
protected void UpdateStatusWithDispatcher(DownloadTaskStatus status) => _dispatcherQueue.TryEnqueue(() => Status = status);
protected void UpdateProgressWithDispatcher(double progress) => _dispatcherQueue.TryEnqueue(() => Progress = progress);
protected void UpdateErrorWithDispatcher(string message) => _dispatcherQueue.TryEnqueue(() => Error = message);
#endregion
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Models;
using VDownload.Services.Data.Configuration;
using VDownload.Services.Data.Settings;
using VDownload.Services.UI.Notifications;
using VDownload.Services.Utility.FFmpeg;
namespace VDownload.Core.Tasks
{
public interface IDownloadTaskFactoryService
{
DownloadTask Create(Video video, VideoDownloadOptions downloadOptions);
}
public class DownloadTaskFactoryService : IDownloadTaskFactoryService
{
#region SERVICES
protected readonly IConfigurationService _configurationService;
protected readonly ISettingsService _settingsService;
protected readonly IFFmpegService _ffmpegService;
protected readonly INotificationsService _notificationsService;
#endregion
#region CONSTRUCTORS
public DownloadTaskFactoryService(IConfigurationService configurationService, ISettingsService settingsService, IFFmpegService ffmpegService, INotificationsService notificationsService)
{
_configurationService = configurationService;
_settingsService = settingsService;
_ffmpegService = ffmpegService;
_notificationsService = notificationsService;
}
#endregion
#region PUBLIC METHODS
public DownloadTask Create(Video video, VideoDownloadOptions downloadOptions)
{
return new DownloadTask(video, downloadOptions, _configurationService, _settingsService, _ffmpegService, _notificationsService);
}
#endregion
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Models;
using VDownload.Services.Data.Settings;
namespace VDownload.Core.Tasks
{
public interface IDownloadTaskManager
{
#region PROPERTIES
ReadOnlyObservableCollection<DownloadTask> Tasks { get; }
#endregion
#region EVENTS
event NotifyCollectionChangedEventHandler TaskCollectionChanged;
#endregion
#region METHODS
DownloadTask AddTask(Video video, VideoDownloadOptions downloadOptions);
void RemoveTask(DownloadTask task);
#endregion
}
public class DownloadTaskManager : IDownloadTaskManager
{
#region SERVICES
protected readonly IDownloadTaskFactoryService _downloadTaskFactoryService;
protected readonly ISettingsService _settingsService;
#endregion
#region FIELDS
private readonly Thread _taskMonitorThread;
private readonly ObservableCollection<DownloadTask> _tasksMain;
#endregion
#region PROPERTIES
public ReadOnlyObservableCollection<DownloadTask> Tasks { get; protected set; }
#endregion
#region EVENTS
public event NotifyCollectionChangedEventHandler TaskCollectionChanged;
#endregion
#region CONSTRUCTORS
public DownloadTaskManager(IDownloadTaskFactoryService downloadTaskFactoryService, ISettingsService settingsService)
{
_downloadTaskFactoryService = downloadTaskFactoryService;
_settingsService = settingsService;
_tasksMain = new ObservableCollection<DownloadTask>();
_tasksMain.CollectionChanged += Tasks_CollectionChanged;
Tasks = new ReadOnlyObservableCollection<DownloadTask>(_tasksMain);
_taskMonitorThread = new Thread(TaskMonitor)
{
IsBackground = true
};
_taskMonitorThread.Start();
}
#endregion
#region PUBLIC METHODS
public DownloadTask AddTask(Video video, VideoDownloadOptions downloadOptions)
{
DownloadTask task = _downloadTaskFactoryService.Create(video, downloadOptions);
_tasksMain.Add(task);
return task;
}
public void RemoveTask(DownloadTask task)
{
_tasksMain.Remove(task);
}
#endregion
#region PRIVATE METHODS
private async void TaskMonitor()
{
await _settingsService.Load();
DownloadTaskStatus[] pendingStatuses =
[
DownloadTaskStatus.Initializing,
DownloadTaskStatus.Downloading,
DownloadTaskStatus.Processing,
DownloadTaskStatus.Finalizing
];
while (true)
{
try
{
IEnumerable<DownloadTask> pendingTasks = Tasks.Where(x => pendingStatuses.Contains(x.Status));
int freeSlots = _settingsService.Data.Common.Tasks.MaxNumberOfRunningTasks - pendingTasks.Count();
if (freeSlots > 0)
{
IEnumerable<DownloadTask> queuedTasks = Tasks.Where(x => x.Status == DownloadTaskStatus.Queued).OrderBy(x => x.CreateDate).Take(freeSlots);
foreach (DownloadTask queuedTask in queuedTasks)
{
queuedTask.Start();
}
}
}
catch (InvalidOperationException)
{
Console.WriteLine("TaskMonitor: Collection locked - skipping");
}
}
}
#endregion
#region EVENT HANDLERS
private void Tasks_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
TaskCollectionChanged?.Invoke(this, e);
}
#endregion
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.Tasks
{
public class DownloadTaskNotInitializedException : Exception
{
#region CONSTRUCTORS
public DownloadTaskNotInitializedException() : base() { }
public DownloadTaskNotInitializedException(string message) : base(message) { }
public DownloadTaskNotInitializedException(string message, Exception inner) : base(message, inner) { }
#endregion
}
}

View File

@@ -1,14 +1,21 @@
namespace VDownload.Core.Enums using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.Tasks
{ {
public enum DownloadTaskStatus public enum DownloadTaskStatus
{ {
Idle, Idle,
Scheduled,
Queued,
Downloading,
Processing,
Finalizing,
EndedSuccessfully, EndedSuccessfully,
EndedUnsuccessfully, EndedUnsuccessfully,
EndedCancelled,
Queued,
Initializing,
Finalizing,
Processing,
Downloading,
} }
} }

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>VDownload.Core.Tasks</RootNamespace>
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<UseRidGraph>true</UseRidGraph>
<EnableCoreMrtTooling Condition=" '$(BuildingInsideVisualStudio)' != 'true' ">false</EnableCoreMrtTooling>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\VDownload.Models\VDownload.Models.csproj" />
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Configuration\VDownload.Services.Data.Configuration.csproj" />
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Settings\VDownload.Services.Data.Settings.csproj" />
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.UI\VDownload.Services.UI.Notifications\VDownload.Services.UI.Notifications.csproj" />
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.Utility\VDownload.Services.Utility.FFmpeg\VDownload.Services.Utility.FFmpeg.csproj" />
<ProjectReference Include="..\VDownload.Core.Strings\VDownload.Core.Strings.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,86 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Strings;
using VDownload.Core.ViewModels.About.Helpers;
using VDownload.Services.Data.Configuration;
using VDownload.Services.Data.Configuration.Models;
using Windows.System.UserProfile;
namespace VDownload.Core.ViewModels.About
{
public partial class AboutViewModel : ObservableObject
{
#region SERVICES
protected readonly IConfigurationService _configurationService;
#endregion
#region PROPERTIES
[ObservableProperty]
protected string _version;
[ObservableProperty]
protected ObservableCollection<PersonViewModel> _developers;
[ObservableProperty]
protected ObservableCollection<PersonViewModel> _translators;
[ObservableProperty]
protected Uri _repositoryUrl;
[ObservableProperty]
protected Uri _donationUrl;
#endregion
#region CONSTRUCTORS
public AboutViewModel(IConfigurationService configurationService)
{
_configurationService = configurationService;
string version = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
if (version == "0.0.0")
{
version = StringResourcesManager.AboutView.Get("SelfbuiltVersion");
}
_version = version;
_developers = new ObservableCollection<PersonViewModel>(_configurationService.Common.About.Developers.Select(x => new PersonViewModel(x.Name, x.Url)));
_repositoryUrl = new Uri(_configurationService.Common.About.RepositoryUrl);
_donationUrl = new Uri(_configurationService.Common.About.DonationUrl);
}
#endregion
#region COMMANDS
[RelayCommand]
public void Navigation()
{
string languageCode = "en-US";
Language language = _configurationService.Common.About.Translation.FirstOrDefault(x => x.Code == languageCode);
Translators = new ObservableCollection<PersonViewModel>(language.Translators.Select(x => new PersonViewModel(x.Name, x.Url)));
}
#endregion
}
}

View File

@@ -0,0 +1,34 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.ViewModels.About.Helpers
{
public partial class PersonViewModel : ObservableObject
{
#region PROPERTIES
[ObservableProperty]
protected string _name;
[ObservableProperty]
protected Uri _url;
#endregion
#region CONSTRUCTORS
public PersonViewModel(string name, string url)
{
_name = name;
_url = new Uri(url);
}
#endregion
}
}

View File

@@ -0,0 +1,178 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using VDownload.Core.Strings;
using VDownload.Services.Data.Configuration;
using VDownload.Services.UI.Dialogs;
using VDownload.Services.UI.WebView;
using VDownload.Sources.Twitch.Authentication;
namespace VDownload.Core.ViewModels.Authentication
{
public partial class AuthenticationViewModel : ObservableObject
{
#region ENUMS
public enum AuthenticationButton
{
SignIn,
SignOut,
Loading
}
#endregion
#region SERVICES
protected readonly IDialogsService _dialogsService;
protected readonly IWebViewService _webViewService;
protected readonly IConfigurationService _configurationService;
protected readonly ITwitchAuthenticationService _twitchAuthenticationService;
#endregion
#region PROPERTIES
[ObservableProperty]
protected AuthenticationButton _twitchButtonState = AuthenticationButton.Loading;
[ObservableProperty]
protected bool _twitchButtonEnable = true;
[ObservableProperty]
protected string _twitchDescription;
#endregion
#region CONSTRUCTORS
public AuthenticationViewModel(IDialogsService dialogsService, IWebViewService webViewService, IConfigurationService configurationService, ITwitchAuthenticationService twitchAuthenticationService)
{
_dialogsService = dialogsService;
_webViewService = webViewService;
_configurationService = configurationService;
_twitchAuthenticationService = twitchAuthenticationService;
}
#endregion
#region PUBLIC METHODS
[RelayCommand]
public async Task Navigation()
{
List<Task> refreshTasks = new List<Task>
{
TwitchAuthenticationRefresh()
};
await Task.WhenAll(refreshTasks);
}
[RelayCommand]
public async Task TwitchAuthentication()
{
AuthenticationButton state = TwitchButtonState;
TwitchButtonState = AuthenticationButton.Loading;
if (state == AuthenticationButton.SignOut)
{
await _twitchAuthenticationService.DeleteToken();
}
else
{
Sources.Twitch.Configuration.Models.Authentication auth = _configurationService.Twitch.Authentication;
string authUrl = string.Format(auth.Url, auth.ClientId, auth.RedirectUrl, auth.ResponseType, string.Join(' ', auth.Scopes));
string url = await _webViewService.Show(new Uri(authUrl), (url) => url.StartsWith(auth.RedirectUrl), StringResourcesManager.AuthenticationView.Get("TwitchAuthenticationWindowTitle"));
Regex regex = new Regex(auth.RedirectUrlRegex);
Match match = regex.Match(url);
if (match.Success)
{
string token = match.Groups[1].Value;
await _twitchAuthenticationService.SetToken(Encoding.UTF8.GetBytes(token));
}
else
{
string title = StringResourcesManager.AuthenticationView.Get("TwitchAuthenticationDialogTitle");
string message = StringResourcesManager.AuthenticationView.Get("TwitchAuthenticationDialogMessage");
await _dialogsService.ShowOk(title, message);
}
}
await TwitchAuthenticationRefresh();
}
#endregion
#region PRIVATE METHODS
private async Task TwitchAuthenticationRefresh()
{
TwitchButtonState = AuthenticationButton.Loading;
TwitchButtonEnable = true;
byte[]? token = await _twitchAuthenticationService.GetToken();
if (token is null)
{
if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable)
{
TwitchButtonEnable = false;
TwitchDescription = StringResourcesManager.AuthenticationView.Get("TwitchAuthenticationDescriptionNotAuthenticatedNoInternetConnection");
}
else
{
TwitchDescription = StringResourcesManager.AuthenticationView.Get("TwitchAuthenticationDescriptionNotAuthenticated");
}
TwitchButtonState = AuthenticationButton.SignIn;
}
else
{
TwitchValidationResult validationResult;
try
{
validationResult = await _twitchAuthenticationService.ValidateToken(token);
}
catch (Exception ex) when (ex is TaskCanceledException || ex is HttpRequestException)
{
TwitchDescription = StringResourcesManager.AuthenticationView.Get("TwitchAuthenticationDescriptionCannotValidate");
TwitchButtonState = AuthenticationButton.SignIn;
TwitchButtonEnable = false;
return;
}
if (validationResult.Success)
{
TwitchDescription = string.Format(StringResourcesManager.AuthenticationView.Get("TwitchAuthenticationDescriptionAuthenticated"), validationResult.TokenData.Login, validationResult.TokenData.ExpirationDate);
TwitchButtonState = AuthenticationButton.SignOut;
}
else
{
await _twitchAuthenticationService.DeleteToken();
TwitchDescription = StringResourcesManager.AuthenticationView.Get("TwitchAuthenticationDescriptionAuthenticationInvalid");
TwitchButtonState = AuthenticationButton.SignIn;
}
}
}
#endregion
}
}

View File

@@ -0,0 +1,122 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.ViewModels.Authentication;
using VDownload.Core.ViewModels.Home;
using VDownload.Core.ViewModels.Settings;
using VDownload.Services.UI.DictionaryResources;
using SimpleToolkit.UI.Models;
using VDownload.Core.ViewModels.About;
using VDownload.Core.ViewModels.Subscriptions;
using VDownload.Core.Strings;
namespace VDownload.Core.ViewModels
{
public partial class BaseViewModel : ObservableObject
{
#region SERVICES
protected readonly IDictionaryResourcesService _dictionaryResourcesService;
#endregion
#region FIELDS
protected readonly Type _settingsViewModel = typeof(SettingsViewModel);
#endregion
#region PROPERTIES
[ObservableProperty]
private Type _currentViewModel;
[ObservableProperty]
private NavigationViewItem _selectedItem;
public ReadOnlyObservableCollection<NavigationViewItem> Items { get; protected set; }
public ReadOnlyObservableCollection<NavigationViewItem> FooterItems { get; protected set; }
#endregion
#region CONSTRUCTORS
public BaseViewModel(IDictionaryResourcesService dictionaryResourcesService)
{
_dictionaryResourcesService = dictionaryResourcesService;
Items = new ReadOnlyObservableCollection<NavigationViewItem>
(
new ObservableCollection<NavigationViewItem>
{
new NavigationViewItem()
{
Name = StringResourcesManager.BaseView.Get("HomeNavigationViewItem"),
IconSource = _dictionaryResourcesService.Get<string>("ImageBaseViewHome"),
ViewModel = typeof(HomeViewModel),
},
new NavigationViewItem()
{
Name = StringResourcesManager.BaseView.Get("SubscriptionsNavigationViewItem"),
IconSource = _dictionaryResourcesService.Get<string>("ImageBaseViewSubscriptions"),
ViewModel = typeof(SubscriptionsViewModel),
},
}
);
FooterItems = new ReadOnlyObservableCollection<NavigationViewItem>
(
new ObservableCollection<NavigationViewItem>
{
new NavigationViewItem()
{
Name = StringResourcesManager.BaseView.Get("AboutNavigationViewItem"),
IconSource = _dictionaryResourcesService.Get<string>("ImageBaseViewAbout"),
ViewModel = typeof(AboutViewModel),
},
new NavigationViewItem()
{
Name = StringResourcesManager.BaseView.Get("AuthenticationNavigationViewItem"),
IconSource = _dictionaryResourcesService.Get<string>("ImageBaseViewAuthentication"),
ViewModel = typeof(AuthenticationViewModel),
}
}
);
SelectedItem = Items.First();
CurrentViewModel = SelectedItem.ViewModel;
}
#endregion
#region PUBLIC METHODS
[RelayCommand]
public void Navigate(Microsoft.UI.Xaml.Controls.NavigationViewItemInvokedEventArgs e)
{
if (e.IsSettingsInvoked)
{
CurrentViewModel = _settingsViewModel;
}
else
{
NavigationViewItem item = e.InvokedItemContainer.DataContext as NavigationViewItem;
CurrentViewModel = item.ViewModel;
}
}
#endregion
}
}

View File

@@ -0,0 +1,163 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Models;
using VDownload.Services.Data.Application;
using VDownload.Services.Data.Settings;
using VDownload.Services.UI.StoragePicker;
using VDownload.Services.Utility.Filename;
namespace VDownload.Core.ViewModels.Home.Helpers
{
public partial class VideoViewModel : ObservableObject
{
#region SERVICES
protected readonly ISettingsService _settingsService;
protected readonly IStoragePickerService _storagePickerService;
protected readonly IFilenameService _filenameService;
protected readonly IApplicationDataService _applicationDataService;
#endregion
#region PROPERTIES
[ObservableProperty]
protected Uri _thumbnailUrl;
[ObservableProperty]
protected string _title;
[ObservableProperty]
protected string _author;
[ObservableProperty]
protected DateTime _publishDate;
[ObservableProperty]
protected TimeSpan _duration;
[ObservableProperty]
protected long _views;
[ObservableProperty]
protected ObservableCollection<VideoStream> _streams;
[ObservableProperty]
protected VideoStream _selectedStream;
[ObservableProperty]
protected MediaType _mediaType;
[ObservableProperty]
protected TimeSpan _trimStart;
[ObservableProperty]
protected TimeSpan _trimEnd;
[ObservableProperty]
protected string _directoryPath;
[ObservableProperty]
protected string _filename;
[ObservableProperty]
protected VideoExtension _videoExtension;
[ObservableProperty]
protected AudioExtension _audioExtension;
public Video Video { get; protected set; }
#endregion
#region CONSTRUCTORS
public VideoViewModel(Video video, ISettingsService settingsService, IStoragePickerService storagePickerService, IFilenameService filenameService, IApplicationDataService applicationDataService)
{
_settingsService = settingsService;
_storagePickerService = storagePickerService;
_filenameService = filenameService;
_applicationDataService = applicationDataService;
Video = video;
ThumbnailUrl = video.ThumbnailUrl;
Title = video.Title;
Author = video.Author;
PublishDate = video.PublishDate;
Duration = video.Duration;
Views = video.Views;
Streams = [.. video.Streams];
SelectedStream = Streams[0];
MediaType = _settingsService.Data.Common.Tasks.DefaultMediaType;
TrimStart = TimeSpan.Zero;
TrimEnd = Duration;
Filename = _filenameService.CreateFilename(_settingsService.Data.Common.Tasks.FilenameTemplate, video);
VideoExtension = _settingsService.Data.Common.Tasks.DefaultVideoExtension;
AudioExtension = _settingsService.Data.Common.Tasks.DefaultAudioExtension;
if (_settingsService.Data.Common.Tasks.SaveLastOutputDirectory && !string.IsNullOrWhiteSpace(_applicationDataService.Data.Common.LastOutputDirectory))
{
DirectoryPath = _applicationDataService.Data.Common.LastOutputDirectory;
}
else
{
DirectoryPath = _settingsService.Data.Common.Tasks.DefaultOutputDirectory;
}
}
#endregion
#region PUBLIC METHODS
public VideoDownloadOptions BuildDownloadOptions()
{
return new VideoDownloadOptions(Duration)
{
MediaType = this.MediaType,
SelectedStream = this.SelectedStream,
TrimStart = this.TrimStart,
TrimEnd = this.TrimEnd,
Directory = this.DirectoryPath,
Filename = _filenameService.SanitizeFilename(this.Filename),
Extension = (this.MediaType == MediaType.OnlyAudio ? this.AudioExtension.ToString() : this.VideoExtension.ToString()).ToLower(),
};
}
#endregion
#region COMMANDS
[RelayCommand]
public async Task Browse()
{
string? newDirectory = await _storagePickerService.OpenDirectory();
if (newDirectory is not null)
{
this.DirectoryPath = newDirectory;
_applicationDataService.Data.Common.LastOutputDirectory = newDirectory;
await _applicationDataService.Save();
}
}
#endregion
}
}

View File

@@ -0,0 +1,125 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI.Helpers;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Strings;
using VDownload.Core.Tasks;
using VDownload.Services.Data.Settings;
using VDownload.Services.UI.Dialogs;
namespace VDownload.Core.ViewModels.Home
{
public partial class HomeDownloadsViewModel : ObservableObject
{
#region SERVICES
protected readonly IDownloadTaskManager _tasksManager;
protected readonly IDialogsService _dialogsService;
protected readonly ISettingsService _settingsService;
#endregion
#region PROPERTIES
public ReadOnlyObservableCollection<DownloadTask> Tasks => _tasksManager.Tasks;
[ObservableProperty]
private bool _taskListIsEmpty;
#endregion
#region CONSTRUCTORS
public HomeDownloadsViewModel(IDownloadTaskManager tasksManager, IDialogsService dialogsService, ISettingsService settingsService)
{
_tasksManager = tasksManager;
_tasksManager.TaskCollectionChanged += Tasks_CollectionChanged;
_dialogsService = dialogsService;
_settingsService = settingsService;
_taskListIsEmpty = _tasksManager.Tasks.Count == 0;
}
#endregion
#region PUBLIC METHODS
[RelayCommand]
public async Task StartCancelTask(DownloadTask task)
{
DownloadTaskStatus[] idleStatuses =
[
DownloadTaskStatus.Idle,
DownloadTaskStatus.EndedUnsuccessfully,
DownloadTaskStatus.EndedSuccessfully,
DownloadTaskStatus.EndedCancelled
];
if (idleStatuses.Contains(task.Status))
{
if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable)
{
string title = StringResourcesManager.HomeDownloadsView.Get("DialogErrorTitle");
string message = StringResourcesManager.HomeDownloadsView.Get("DialogErrorMessageNoInternetConnection");
await _dialogsService.ShowOk(title, message);
return;
}
bool continueEnqueue = true;
if
(
_settingsService.Data.Common.Tasks.ShowMeteredConnectionWarnings
&&
NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection
)
{
string title = StringResourcesManager.Common.Get("StartAtMeteredConnectionDialogTitle");
string message = StringResourcesManager.Common.Get("StartAtMeteredConnectionDialogMessage");
DialogResultYesNo result = await _dialogsService.ShowYesNo(title, message);
continueEnqueue = result == DialogResultYesNo.Yes;
}
if (continueEnqueue)
{
task.Enqueue();
}
}
else
{
await task.Cancel();
}
}
[RelayCommand]
public async Task RemoveTask(DownloadTask task)
{
await task.Cancel();
_tasksManager.RemoveTask(task);
}
#endregion
#region PRIVATE METHODS
private void Tasks_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
TaskListIsEmpty = Tasks.Count == 0;
}
#endregion
}
}

View File

@@ -0,0 +1,374 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Tasks;
using VDownload.Core.ViewModels.Home.Helpers;
using VDownload.Models;
using VDownload.Services.Data.Settings;
using VDownload.Services.UI.StoragePicker;
using VDownload.Sources.Twitch.Configuration.Models;
using SimpleToolkit.MVVM;
using System.Text.RegularExpressions;
using VDownload.Services.Utility.Filename;
using VDownload.Services.UI.Dialogs;
using CommunityToolkit.WinUI.Helpers;
using VDownload.Services.Data.Application;
using VDownload.Core.Strings;
namespace VDownload.Core.ViewModels.Home
{
public partial class HomeVideoCollectionViewModel : ObservableObject
{
#region SERVICES
protected readonly IDownloadTaskManager _tasksManager;
protected readonly ISettingsService _settingsService;
protected readonly IStoragePickerService _storagePickerService;
protected readonly IFilenameService _filenameService;
protected readonly IDialogsService _dialogsService;
protected readonly IApplicationDataService _applicationDataService;
#endregion
#region FIELDS
protected VideoCollection _collection;
protected List<VideoViewModel> _removedVideos;
#endregion
#region PROPERTIES
[ObservableProperty]
protected string _name;
[ObservableProperty]
protected ObservableDictionary<VideoViewModel, bool> _videos;
[ObservableProperty]
protected int _removedCount;
[ObservableProperty]
protected int _hiddenCount;
[ObservableProperty]
protected bool _isSomethingHidden;
public string TitleFilter
{
get => _titleFilter;
set
{
SetProperty(ref _titleFilter, value, nameof(TitleFilter));
UpdateFilter();
}
}
protected string _titleFilter;
public string AuthorFilter
{
get => _authorFilter;
set
{
SetProperty(ref _authorFilter, value, nameof(AuthorFilter));
UpdateFilter();
}
}
protected string _authorFilter;
public long MinViewsFilter
{
get => _minViewsFilter;
set
{
SetProperty(ref _minViewsFilter, value, nameof(MinViewsFilter));
UpdateFilter();
}
}
protected long _minViewsFilter;
public long MaxViewsFilter
{
get => _maxViewsFilter;
set
{
SetProperty(ref _maxViewsFilter, value, nameof(MaxViewsFilter));
UpdateFilter();
}
}
protected long _maxViewsFilter;
[ObservableProperty]
protected long _minViews;
[ObservableProperty]
protected long _maxViews;
public DateTimeOffset MinDateFilter
{
get => _minDateFilter;
set
{
SetProperty(ref _minDateFilter, value, nameof(MinDateFilter));
UpdateFilter();
}
}
protected DateTimeOffset _minDateFilter;
public DateTimeOffset MaxDateFilter
{
get => _maxDateFilter;
set
{
SetProperty(ref _maxDateFilter, value, nameof(MaxDateFilter));
UpdateFilter();
}
}
protected DateTimeOffset _maxDateFilter;
[ObservableProperty]
protected DateTimeOffset _minDate;
[ObservableProperty]
protected DateTimeOffset _maxDate;
public TimeSpan MinDurationFilter
{
get => _minDurationFilter;
set
{
SetProperty(ref _minDurationFilter, value, nameof(MinDurationFilter));
UpdateFilter();
}
}
protected TimeSpan _minDurationFilter;
public TimeSpan MaxDurationFilter
{
get => _maxDurationFilter;
set
{
SetProperty(ref _maxDurationFilter, value, nameof(MaxDurationFilter));
UpdateFilter();
}
}
protected TimeSpan _maxDurationFilter;
[ObservableProperty]
protected TimeSpan _minDuration;
[ObservableProperty]
protected TimeSpan _maxDuration;
#endregion
#region CONSTRUCTORS
public HomeVideoCollectionViewModel(IDownloadTaskManager tasksManager, ISettingsService settingsService, IStoragePickerService storagePickerService, IFilenameService filenameService, IDialogsService dialogsService, IApplicationDataService applicationDataService)
{
_tasksManager = tasksManager;
_settingsService = settingsService;
_storagePickerService = storagePickerService;
_filenameService = filenameService;
_dialogsService = dialogsService;
_applicationDataService = applicationDataService;
_removedVideos = new List<VideoViewModel>();
_videos = new ObservableDictionary<VideoViewModel, bool>();
}
#endregion
#region PUBLIC METHODS
public void LoadCollection(VideoCollection collection)
{
_collection = collection;
ParallelQuery<Video> playlistQuery = _collection.AsParallel();
_removedVideos.Clear();
_titleFilter = string.Empty;
_authorFilter = string.Empty;
IEnumerable<long> views = playlistQuery.Select(x => x.Views);
MinViews = views.Min();
MaxViews = views.Max();
_minViewsFilter = MinViews;
_maxViewsFilter = MaxViews;
IEnumerable<DateTimeOffset> date = _collection.Select(x => new DateTimeOffset(x.PublishDate));
MinDate = date.Min();
MaxDate = date.Max();
_minDateFilter = MinDate;
_maxDateFilter = MaxDate;
IEnumerable<TimeSpan> duration = playlistQuery.Select(x => x.Duration);
MinDuration = duration.Min();
MaxDuration = duration.Max();
_minDurationFilter = MinDuration;
_maxDurationFilter = MaxDuration;
Name = _collection.Name;
Videos.Clear();
foreach (Video video in _collection)
{
Videos.Add(new VideoViewModel(video, _settingsService, _storagePickerService, _filenameService, _applicationDataService), true);
}
UpdateFilter();
}
#endregion
#region COMMANDS
[RelayCommand]
public async Task SelectDirectory()
{
string? newDirectory = await _storagePickerService.OpenDirectory();
if (newDirectory is not null)
{
foreach (VideoViewModel video in Videos.Keys)
{
video.DirectoryPath = newDirectory;
}
_applicationDataService.Data.Common.LastOutputDirectory = newDirectory;
await _applicationDataService.Save();
}
}
[RelayCommand]
public void RemoveVideo(VideoViewModel video)
{
Videos[video] = false;
_removedVideos.Add(video);
UpdateFilter();
}
[RelayCommand]
public void RestoreRemovedVideos()
{
foreach(VideoViewModel video in _removedVideos)
{
Videos[video] = true;
}
_removedVideos.Clear();
UpdateFilter();
}
[RelayCommand]
public async Task CreateTasksAndDownload() => await CreateTasks(true);
[RelayCommand]
public async Task CreateTasks() => await CreateTasks(false);
#endregion
#region PRIVATE METHODS
protected async Task CreateTasks(bool download)
{
if
(
download
&&
_settingsService.Data.Common.Tasks.ShowMeteredConnectionWarnings
&&
NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection
)
{
string title = StringResourcesManager.Common.Get("StartAtMeteredConnectionDialogTitle");
string message = StringResourcesManager.Common.Get("StartAtMeteredConnectionDialogMessage");
DialogResultYesNo result = await _dialogsService.ShowYesNo(title, message);
download = result == DialogResultYesNo.Yes;
}
IEnumerable<VideoViewModel> videos = Videos.Cast<ObservableKeyValuePair<VideoViewModel, bool>>()
.Where(x => x.Value)
.Select(x => x.Key);
foreach (VideoViewModel video in videos)
{
DownloadTask task = _tasksManager.AddTask(video.Video, video.BuildDownloadOptions());
if (download)
{
task.Enqueue();
}
}
CloseRequested?.Invoke(this, EventArgs.Empty);
}
protected void UpdateFilter()
{
Regex titleRegex = new Regex(TitleFilter);
Regex authorRegex = new Regex(AuthorFilter);
foreach (ObservableKeyValuePair<VideoViewModel, bool> item in Videos)
{
VideoViewModel video = item.Key;
bool hide =
(
_removedVideos.Contains(video)
||
!titleRegex.IsMatch(video.Title)
||
!authorRegex.IsMatch(video.Author)
||
MinViewsFilter > video.Views
||
MaxViewsFilter < video.Views
||
MinDateFilter.Date > video.PublishDate.Date
||
MaxDateFilter.Date < video.PublishDate.Date
||
MinDurationFilter > video.Duration
||
MaxDurationFilter < video.Duration
);
item.Value = !hide;
}
RemovedCount = _removedVideos.Count;
HiddenCount = Videos.Values.Where(x => !x).Count();
IsSomethingHidden = HiddenCount > 0;
}
#endregion
#region EVENTS
public event EventHandler CloseRequested;
#endregion
}
}

View File

@@ -0,0 +1,223 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI.Helpers;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Strings;
using VDownload.Core.Tasks;
using VDownload.Models;
using VDownload.Services.Data.Application;
using VDownload.Services.Data.Settings;
using VDownload.Services.UI.Dialogs;
using VDownload.Services.UI.StoragePicker;
using VDownload.Services.Utility.Filename;
namespace VDownload.Core.ViewModels.Home
{
public partial class HomeVideoViewModel : ObservableObject
{
#region SERVICES
protected readonly IDownloadTaskManager _tasksManager;
protected readonly ISettingsService _settingsService;
protected readonly IStoragePickerService _storagePickerService;
protected readonly IFilenameService _filenameService;
protected readonly IDialogsService _dialogsService;
protected readonly IApplicationDataService _applicationDataService;
#endregion
#region FIELDS
protected Video _video;
#endregion
#region PROPERTIES
[ObservableProperty]
protected Uri _thumbnailUrl;
[ObservableProperty]
protected string _title;
[ObservableProperty]
protected string _author;
[ObservableProperty]
protected DateTime _publishDate;
[ObservableProperty]
protected TimeSpan _duration;
[ObservableProperty]
protected long _views;
[ObservableProperty]
protected ObservableCollection<VideoStream> _streams;
[ObservableProperty]
protected VideoStream _selectedStream;
[ObservableProperty]
protected MediaType _mediaType;
[ObservableProperty]
protected TimeSpan _trimStart;
[ObservableProperty]
protected TimeSpan _trimEnd;
[ObservableProperty]
protected string _directoryPath;
[ObservableProperty]
protected string _filename;
[ObservableProperty]
protected VideoExtension _videoExtension;
[ObservableProperty]
protected AudioExtension _audioExtension;
#endregion
#region CONSTRUCTORS
public HomeVideoViewModel(IDownloadTaskManager tasksManager, ISettingsService settingsService, IStoragePickerService storagePickerService, IFilenameService filenameService, IDialogsService dialogsService, IApplicationDataService applicationDataService)
{
_tasksManager = tasksManager;
_settingsService = settingsService;
_storagePickerService = storagePickerService;
_filenameService = filenameService;
_dialogsService = dialogsService;
_applicationDataService = applicationDataService;
}
#endregion
#region PUBLIC METHODS
public async Task LoadVideo(Video video)
{
await _settingsService.Load();
_video = video;
ThumbnailUrl = video.ThumbnailUrl;
Title = video.Title;
Author = video.Author;
PublishDate = video.PublishDate;
Duration = video.Duration;
Views = video.Views;
Streams = [.. video.Streams];
SelectedStream = Streams[0];
MediaType = _settingsService.Data.Common.Tasks.DefaultMediaType;
TrimStart = TimeSpan.Zero;
TrimEnd = Duration;
Filename = _filenameService.CreateFilename(_settingsService.Data.Common.Tasks.FilenameTemplate, video);
VideoExtension = _settingsService.Data.Common.Tasks.DefaultVideoExtension;
AudioExtension = _settingsService.Data.Common.Tasks.DefaultAudioExtension;
if (_settingsService.Data.Common.Tasks.SaveLastOutputDirectory && !string.IsNullOrWhiteSpace(_applicationDataService.Data.Common.LastOutputDirectory))
{
DirectoryPath = _applicationDataService.Data.Common.LastOutputDirectory;
}
else
{
DirectoryPath = _settingsService.Data.Common.Tasks.DefaultOutputDirectory;
}
}
[RelayCommand]
public async Task Browse()
{
string? newDirectory = await _storagePickerService.OpenDirectory();
if (newDirectory is not null)
{
this.DirectoryPath = newDirectory;
_applicationDataService.Data.Common.LastOutputDirectory = newDirectory;
await _applicationDataService.Save();
}
}
[RelayCommand]
public async Task CreateTask() => await CreateTask(false);
[RelayCommand]
public async Task CreateTaskAndDownload() => await CreateTask(true);
#endregion
#region PRIVATE METHODS
protected async Task CreateTask(bool download)
{
if
(
download
&&
_settingsService.Data.Common.Tasks.ShowMeteredConnectionWarnings
&&
NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection
)
{
string title = StringResourcesManager.Common.Get("StartAtMeteredConnectionDialogTitle");
string message = StringResourcesManager.Common.Get("StartAtMeteredConnectionDialogMessage");
DialogResultYesNo result = await _dialogsService.ShowYesNo(title, message);
download = result == DialogResultYesNo.Yes;
}
DownloadTask task = _tasksManager.AddTask(_video, BuildDownloadOptions());
CloseRequested?.Invoke(this, EventArgs.Empty);
if (download)
{
task.Enqueue();
}
}
protected VideoDownloadOptions BuildDownloadOptions()
{
return new VideoDownloadOptions(Duration)
{
MediaType = this.MediaType,
SelectedStream = this.SelectedStream,
TrimStart = this.TrimStart,
TrimEnd = this.TrimEnd,
Directory = this.DirectoryPath,
Filename = _filenameService.SanitizeFilename(this.Filename),
Extension = (this.MediaType == MediaType.OnlyAudio ? this.AudioExtension.ToString() : this.VideoExtension.ToString()).ToLower(),
};
}
#endregion
#region EVENTS
public event EventHandler CloseRequested;
#endregion
}
}

View File

@@ -0,0 +1,429 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Strings;
using VDownload.Core.Tasks;
using VDownload.Models;
using VDownload.Services.Data.Configuration;
using VDownload.Services.Data.Settings;
using VDownload.Services.Data.Subscriptions;
using VDownload.Services.UI.Dialogs;
using VDownload.Sources;
using VDownload.Sources.Common;
using VDownload.Sources.Twitch.Configuration.Models;
namespace VDownload.Core.ViewModels.Home
{
public partial class HomeViewModel : ObservableObject
{
#region ENUMS
public enum OptionBarContentType
{
None,
VideoSearch,
PlaylistSearch
}
public enum MainContentType
{
Downloads,
Video
}
#endregion
#region SERVICES
protected readonly IConfigurationService _configurationService;
protected readonly ISettingsService _settingsService;
protected readonly ISearchService _searchService;
protected readonly ISubscriptionsDataService _subscriptionsDataService;
protected readonly IDialogsService _dialogsService;
protected readonly IDownloadTaskManager _downloadTaskManager;
protected readonly HomeVideoViewModel _videoViewModel;
protected readonly HomeVideoCollectionViewModel _videoCollectionViewModel;
#endregion
#region FIELDS
protected readonly Type _downloadsView = typeof(HomeDownloadsViewModel);
protected readonly Type _videoView = typeof(HomeVideoViewModel);
protected readonly Type _videoCollectionView = typeof(HomeVideoCollectionViewModel);
#endregion
#region PROPERTIES
[ObservableProperty]
private Type _mainContent;
[ObservableProperty]
private string _optionBarError;
[ObservableProperty]
private bool _optionBarIsErrorOpened;
[ObservableProperty]
private OptionBarContentType _optionBarContent;
[ObservableProperty]
private string _optionBarMessage;
[ObservableProperty]
private bool _optionBarLoading;
[ObservableProperty]
private bool _optionBarLoadSubscriptionButtonChecked;
[ObservableProperty]
private bool _optionBarVideoSearchButtonChecked;
[ObservableProperty]
private bool _optionBarPlaylistSearchButtonChecked;
[ObservableProperty]
private bool _optionBarSearchNotPending;
[ObservableProperty]
private string _optionBarVideoSearchTBValue;
[ObservableProperty]
private string _optionBarPlaylistSearchTBValue;
[ObservableProperty]
private int _optionBarPlaylistSearchNBValue;
#endregion
#region CONSTRUCTORS
public HomeViewModel(IConfigurationService configurationService, ISettingsService settingsService, ISearchService searchService, ISubscriptionsDataService subscriptionsDataService, IDialogsService dialogsService, IDownloadTaskManager downloadTaskManager, HomeVideoViewModel videoViewModel, HomeVideoCollectionViewModel videoCollectionViewModel)
{
_configurationService = configurationService;
_settingsService = settingsService;
_searchService = searchService;
_subscriptionsDataService = subscriptionsDataService;
_dialogsService = dialogsService;
_downloadTaskManager = downloadTaskManager;
_videoViewModel = videoViewModel;
_videoViewModel.CloseRequested += BackToDownload_EventHandler;
_videoCollectionViewModel = videoCollectionViewModel;
_videoCollectionViewModel.CloseRequested += BackToDownload_EventHandler;
}
#endregion
#region COMMANDS
[RelayCommand]
public async Task Navigation()
{
await _settingsService.Load();
MainContent = _downloadsView;
OptionBarContent = OptionBarContentType.None;
OptionBarLoading = false;
OptionBarMessage = null;
OptionBarError = null;
OptionBarIsErrorOpened = true;
OptionBarLoadSubscriptionButtonChecked = false;
OptionBarVideoSearchButtonChecked = false;
OptionBarPlaylistSearchButtonChecked = false;
OptionBarSearchNotPending = true;
OptionBarVideoSearchTBValue = string.Empty;
OptionBarPlaylistSearchNBValue = _settingsService.Data.Common.Searching.MaxNumberOfVideosToGetFromPlaylist;
OptionBarPlaylistSearchTBValue = string.Empty;
}
[RelayCommand]
public async Task LoadFromSubscription()
{
SearchButtonClicked();
if (OptionBarLoadSubscriptionButtonChecked)
{
if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable)
{
ShowError(ErrorNoInternetConnection());
OptionBarLoadSubscriptionButtonChecked = false;
return;
}
OptionBarContent = OptionBarContentType.None;
OptionBarVideoSearchButtonChecked = false;
OptionBarPlaylistSearchButtonChecked = false;
StartSearch();
SubscriptionsVideoList subList = new SubscriptionsVideoList { Name = StringResourcesManager.Common.Get("SubscriptionVideoListName") };
List<Task> tasks = new List<Task>();
try
{
foreach (Subscription sub in _subscriptionsDataService.Data.ToArray())
{
tasks.Add(Task.Run(async () =>
{
Playlist playlist;
try
{
playlist = await _searchService.SearchPlaylist(sub.Url.OriginalString, 0);
}
catch (MediaSearchException)
{
return;
}
IEnumerable<Video> newIds = playlist.Where(x => !sub.VideoIds.Contains(x.Id));
subList.AddRange(newIds);
foreach (Video video in newIds)
{
sub.VideoIds.Add(video.Id);
}
}));
}
await Task.WhenAll(tasks);
await _subscriptionsDataService.Save();
}
catch (Exception ex) when (ex is TaskCanceledException || ex is HttpRequestException)
{
ShowError(ErrorSearchTimeout());
return;
}
if (subList.Count > 0)
{
OptionBarMessage = $"{StringResourcesManager.HomeView.Get("OptionBarMessageVideosFound")} {subList.Count}";
_videoCollectionViewModel.LoadCollection(subList);
MainContent = _videoCollectionView;
}
else
{
OptionBarMessage = StringResourcesManager.HomeView.Get("OptionBarMessageVideosNotFound");
}
OptionBarSearchNotPending = true;
OptionBarLoading = false;
}
}
[RelayCommand]
public void VideoSearchShow()
{
SearchButtonClicked();
if (OptionBarContent != OptionBarContentType.VideoSearch)
{
OptionBarContent = OptionBarContentType.VideoSearch;
OptionBarPlaylistSearchButtonChecked = false;
OptionBarLoadSubscriptionButtonChecked = false;
}
else
{
OptionBarContent = OptionBarContentType.None;
}
}
[RelayCommand]
public void PlaylistSearchShow()
{
SearchButtonClicked();
if (OptionBarContent != OptionBarContentType.PlaylistSearch)
{
OptionBarContent = OptionBarContentType.PlaylistSearch;
OptionBarVideoSearchButtonChecked = false;
OptionBarLoadSubscriptionButtonChecked = false;
}
else
{
OptionBarContent = OptionBarContentType.None;
}
}
[RelayCommand]
public async Task VideoSearchStart()
{
if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable)
{
ShowError(ErrorNoInternetConnection());
return;
}
StartSearch();
Video video;
try
{
video = await _searchService.SearchVideo(OptionBarVideoSearchTBValue);
}
catch (MediaSearchException ex)
{
ShowError(StringResourcesManager.Search.Get(ex.StringCode));
return;
}
catch (Exception ex) when (ex is TaskCanceledException || ex is HttpRequestException)
{
ShowError(ErrorSearchTimeout());
return;
}
await _videoViewModel.LoadVideo(video);
MainContent = _videoView;
OptionBarSearchNotPending = true;
OptionBarLoading = false;
OptionBarMessage = null;
}
[RelayCommand]
public async Task PlaylistSearchStart()
{
if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable)
{
ShowError(ErrorNoInternetConnection());
return;
}
StartSearch();
Playlist playlist;
try
{
playlist = await _searchService.SearchPlaylist(OptionBarPlaylistSearchTBValue, OptionBarPlaylistSearchNBValue);
}
catch (MediaSearchException ex)
{
ShowError(StringResourcesManager.Search.Get(ex.StringCode));
return;
}
catch (Exception ex) when (ex is TaskCanceledException || ex is HttpRequestException)
{
ShowError(ErrorSearchTimeout());
return;
}
_videoCollectionViewModel.LoadCollection(playlist);
MainContent = _videoCollectionView;
OptionBarSearchNotPending = true;
OptionBarLoading = false;
OptionBarMessage = null;
}
[RelayCommand]
public async Task Download()
{
if (_downloadTaskManager.Tasks.Count > 0)
{
if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable)
{
ShowError(ErrorNoInternetConnection());
return;
}
if
(
_settingsService.Data.Common.Tasks.ShowMeteredConnectionWarnings
&&
NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection
)
{
string title = StringResourcesManager.Common.Get("StartAtMeteredConnectionDialogTitle");
string message = StringResourcesManager.Common.Get("StartAtMeteredConnectionDialogMessage");
DialogResultYesNo result = await _dialogsService.ShowYesNo(title, message);
if (result == DialogResultYesNo.No)
{
return;
}
}
foreach (DownloadTask task in _downloadTaskManager.Tasks)
{
task.Enqueue();
}
}
}
[RelayCommand]
public async Task Cancel()
{
List<Task> tasks = new List<Task>();
foreach (DownloadTask task in _downloadTaskManager.Tasks)
{
tasks.Add(task.Cancel());
}
await Task.WhenAll(tasks);
}
[RelayCommand]
public void CloseError()
{
OptionBarError = null;
OptionBarIsErrorOpened = true;
}
#endregion
#region PRIVATE METHODS
protected void ShowError(string message)
{
OptionBarError = message;
OptionBarSearchNotPending = true;
OptionBarLoading = false;
OptionBarMessage = null;
}
protected void SearchButtonClicked()
{
OptionBarSearchNotPending = true;
OptionBarLoading = false;
OptionBarMessage = null;
MainContent = _downloadsView;
}
protected void StartSearch()
{
OptionBarSearchNotPending = false;
OptionBarLoading = true;
OptionBarMessage = StringResourcesManager.HomeView.Get("OptionBarMessageLoading");
}
protected async void BackToDownload_EventHandler(object sender, EventArgs e) => await Navigation();
protected string ErrorNoInternetConnection() => StringResourcesManager.HomeView.Get("ErrorInfoBarNoInternetConnection");
protected string ErrorSearchTimeout() => StringResourcesManager.Search.Get("SearchTimeout");
#endregion
}
}

View File

@@ -0,0 +1,248 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Strings;
using VDownload.Models;
using VDownload.Services.Data.Configuration;
using VDownload.Services.Data.Settings;
using VDownload.Services.UI.StoragePicker;
namespace VDownload.Core.ViewModels.Settings
{
public partial class SettingsViewModel : ObservableObject
{
#region SERVICES
protected readonly ISettingsService _settingsService;
protected readonly IConfigurationService _configurationService;
protected readonly IStoragePickerService _storagePickerService;
#endregion
#region PROPERTIES
public int SearchingPlaylistCount
{
get => _settingsService.Data.Common.Searching.MaxNumberOfVideosToGetFromPlaylist;
set => SetProperty(_settingsService.Data.Common.Searching.MaxNumberOfVideosToGetFromPlaylist, value, _settingsService.Data.Common.Searching, (u, n) => u.MaxNumberOfVideosToGetFromPlaylist = n);
}
public int TasksRunningTasks
{
get => _settingsService.Data.Common.Tasks.MaxNumberOfRunningTasks;
set => SetProperty(_settingsService.Data.Common.Tasks.MaxNumberOfRunningTasks, value, _settingsService.Data.Common.Tasks, (u, n) => u.MaxNumberOfRunningTasks = n);
}
public MediaType TasksMediaType
{
get => _settingsService.Data.Common.Tasks.DefaultMediaType;
set => SetProperty(_settingsService.Data.Common.Tasks.DefaultMediaType, value, _settingsService.Data.Common.Tasks, (u, n) => u.DefaultMediaType = n);
}
public VideoExtension TasksVideoExtension
{
get => _settingsService.Data.Common.Tasks.DefaultVideoExtension;
set => SetProperty(_settingsService.Data.Common.Tasks.DefaultVideoExtension, value, _settingsService.Data.Common.Tasks, (u, n) => u.DefaultVideoExtension = n);
}
public AudioExtension TasksAudioExtension
{
get => _settingsService.Data.Common.Tasks.DefaultAudioExtension;
set => SetProperty(_settingsService.Data.Common.Tasks.DefaultAudioExtension, value, _settingsService.Data.Common.Tasks, (u, n) => u.DefaultAudioExtension = n);
}
public string TasksFilenameTemplate
{
get => _settingsService.Data.Common.Tasks.FilenameTemplate;
set => SetProperty(_settingsService.Data.Common.Tasks.FilenameTemplate, value, _settingsService.Data.Common.Tasks, (u, n) => u.FilenameTemplate = n);
}
public bool TasksMeteredConnectionWarning
{
get => _settingsService.Data.Common.Tasks.ShowMeteredConnectionWarnings;
set => SetProperty(_settingsService.Data.Common.Tasks.ShowMeteredConnectionWarnings, value, _settingsService.Data.Common.Tasks, (u, n) => u.ShowMeteredConnectionWarnings = n);
}
public bool TasksSaveLastOutputDirectory
{
get => _settingsService.Data.Common.Tasks.SaveLastOutputDirectory;
set => SetProperty(_settingsService.Data.Common.Tasks.SaveLastOutputDirectory, value, _settingsService.Data.Common.Tasks, (u, n) => u.SaveLastOutputDirectory = n);
}
public string TasksDefaultOutputDirectory
{
get => _settingsService.Data.Common.Tasks.DefaultOutputDirectory;
set => SetProperty(_settingsService.Data.Common.Tasks.DefaultOutputDirectory, value, _settingsService.Data.Common.Tasks, (u, n) => u.DefaultOutputDirectory = n);
}
public bool TasksReplaceOutputFile
{
get => _settingsService.Data.Common.Tasks.ReplaceOutputFileIfExists;
set => SetProperty(_settingsService.Data.Common.Tasks.ReplaceOutputFileIfExists, value, _settingsService.Data.Common.Tasks, (u, n) => u.ReplaceOutputFileIfExists = n);
}
public string ProcessingFFmpegLocation
{
get => _settingsService.Data.Common.Processing.FFmpegLocation;
set => SetProperty(_settingsService.Data.Common.Processing.FFmpegLocation, value, _settingsService.Data.Common.Processing, (u, n) => u.FFmpegLocation = n);
}
public bool ProcessingUseHardwareAcceleration
{
get => _settingsService.Data.Common.Processing.UseHardwareAcceleration;
set => SetProperty(_settingsService.Data.Common.Processing.UseHardwareAcceleration, value, _settingsService.Data.Common.Processing, (u, n) => u.UseHardwareAcceleration = n);
}
public bool ProcessingUseMultithreading
{
get => _settingsService.Data.Common.Processing.UseMultithreading;
set => SetProperty(_settingsService.Data.Common.Processing.UseMultithreading, value, _settingsService.Data.Common.Processing, (u, n) => u.UseMultithreading = n);
}
public ProcessingSpeed ProcessingSpeed
{
get => _settingsService.Data.Common.Processing.Speed;
set => SetProperty(_settingsService.Data.Common.Processing.Speed, value, _settingsService.Data.Common.Processing, (u, n) => u.Speed = n);
}
public bool NotificationsOnSuccessful
{
get => _settingsService.Data.Common.Notifications.OnSuccessful;
set => SetProperty(_settingsService.Data.Common.Notifications.OnSuccessful, value, _settingsService.Data.Common.Notifications, (u, n) => u.OnSuccessful = n);
}
public bool NotificationsOnUnsuccessful
{
get => _settingsService.Data.Common.Notifications.OnUnsuccessful;
set => SetProperty(_settingsService.Data.Common.Notifications.OnUnsuccessful, value, _settingsService.Data.Common.Notifications, (u, n) => u.OnUnsuccessful = n);
}
public string TempDirectory
{
get => _settingsService.Data.Common.Temp.Directory;
set => SetProperty(_settingsService.Data.Common.Temp.Directory, value, _settingsService.Data.Common.Temp, (u, n) => u.Directory = n);
}
public bool TempDeleteOnFail
{
get => _settingsService.Data.Common.Temp.DeleteOnError;
set => SetProperty(_settingsService.Data.Common.Temp.DeleteOnError, value, _settingsService.Data.Common.Temp, (u, n) => u.DeleteOnError = n);
}
public bool TwitchVodPassiveTrimming
{
get => _settingsService.Data.Twitch.Vod.PassiveTrimming;
set => SetProperty(_settingsService.Data.Twitch.Vod.PassiveTrimming, value, _settingsService.Data.Twitch.Vod, (u, n) => u.PassiveTrimming = n);
}
public int TwitchVodParallelDownloads
{
get => _settingsService.Data.Twitch.Vod.MaxNumberOfParallelDownloads;
set => SetProperty(_settingsService.Data.Twitch.Vod.MaxNumberOfParallelDownloads, value, _settingsService.Data.Twitch.Vod, (u, n) => u.MaxNumberOfParallelDownloads = n);
}
public bool TwitchVodChunkDownloadingErrorRetry
{
get => _settingsService.Data.Twitch.Vod.ChunkDownloadingError.Retry;
set => SetProperty(_settingsService.Data.Twitch.Vod.ChunkDownloadingError.Retry, value, _settingsService.Data.Twitch.Vod.ChunkDownloadingError, (u, n) => u.Retry = n);
}
public int TwitchVodChunkDownloadingErrorRetryCount
{
get => _settingsService.Data.Twitch.Vod.ChunkDownloadingError.RetriesCount;
set => SetProperty(_settingsService.Data.Twitch.Vod.ChunkDownloadingError.RetriesCount, value, _settingsService.Data.Twitch.Vod.ChunkDownloadingError, (u, n) => u.RetriesCount = n);
}
public int TwitchVodChunkDownloadingErrorRetryDelay
{
get => _settingsService.Data.Twitch.Vod.ChunkDownloadingError.RetryDelay;
set => SetProperty(_settingsService.Data.Twitch.Vod.ChunkDownloadingError.RetryDelay, value, _settingsService.Data.Twitch.Vod.ChunkDownloadingError, (u, n) => u.RetryDelay = n);
}
[ObservableProperty]
protected string _tasksFilenameTemplateTooltip;
#endregion
#region CONSTRUCTORS
public SettingsViewModel(ISettingsService settingsService, IConfigurationService configurationService, IStoragePickerService storagePickerService)
{
_settingsService = settingsService;
_configurationService = configurationService;
_storagePickerService = storagePickerService;
base.PropertyChanged += PropertyChangedEventHandler;
_tasksFilenameTemplateTooltip = string.Join('\n', _configurationService.Common.FilenameTemplates.Select(x => StringResourcesManager.FilenameTemplate.Get(x.Name)));
}
#endregion
#region COMMANDS
[RelayCommand]
public async Task BrowseTasksDefaultOutputDirectory()
{
string? newDirectory = await _storagePickerService.OpenDirectory();
if (newDirectory is not null)
{
this.TasksDefaultOutputDirectory = newDirectory;
}
}
[RelayCommand]
public async Task BrowseTempDirectory()
{
string? newDirectory = await _storagePickerService.OpenDirectory();
if (newDirectory is not null)
{
this.TempDirectory = newDirectory;
}
}
[RelayCommand]
public async Task BrowseProcessingFFmpegLocation()
{
string? newDirectory = await _storagePickerService.OpenDirectory();
if (newDirectory is not null)
{
this.ProcessingFFmpegLocation = newDirectory;
}
}
[RelayCommand]
public async Task RestoreToDefault()
{
await _settingsService.Restore();
foreach (PropertyInfo property in this.GetType().GetProperties())
{
base.OnPropertyChanged(property.Name);
}
}
#endregion
#region PRIVATE METHODS
private async void PropertyChangedEventHandler(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
await _settingsService.Save();
}
#endregion
}
}

View File

@@ -0,0 +1,39 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Models;
namespace VDownload.Core.ViewModels.Subscriptions.Helpers
{
public partial class PlaylistViewModel : ObservableObject
{
#region PROPERTIES
[ObservableProperty]
protected Guid _guid;
[ObservableProperty]
protected string _name;
[ObservableProperty]
protected Source _source;
#endregion
#region CONSTRUCTORS
public PlaylistViewModel(string name, Source source, Guid guid)
{
_name = name;
_source = source;
_guid = guid;
}
#endregion
}
}

View File

@@ -0,0 +1,161 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI.Helpers;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Strings;
using VDownload.Core.ViewModels.Subscriptions.Helpers;
using VDownload.Models;
using VDownload.Services.Data.Subscriptions;
using VDownload.Sources;
using VDownload.Sources.Common;
namespace VDownload.Core.ViewModels.Subscriptions
{
public partial class SubscriptionsViewModel : ObservableObject
{
#region SERVICES
protected readonly ISearchService _searchService;
protected readonly ISubscriptionsDataService _subscriptionsDataService;
#endregion
#region PROPERTIES
[ObservableProperty]
protected ObservableCollection<PlaylistViewModel> _playlists;
[ObservableProperty]
protected string _url;
[ObservableProperty]
protected bool _loading;
[ObservableProperty]
protected string _error;
[ObservableProperty]
protected bool _isErrorOpened;
#endregion
#region CONSTRUCTORS
public SubscriptionsViewModel(ISearchService searchService, ISubscriptionsDataService subscriptionsDataService)
{
_searchService = searchService;
_subscriptionsDataService = subscriptionsDataService;
_playlists = new ObservableCollection<PlaylistViewModel>();
_loading = false;
_isErrorOpened = true;
_error = null;
}
#endregion
#region COMMANDS
[RelayCommand]
public void Navigation()
{
Playlists.Clear();
foreach (Subscription sub in _subscriptionsDataService.Data)
{
Playlists.Add(new PlaylistViewModel(sub.Name, sub.Source, sub.Guid));
}
}
[RelayCommand]
public async Task RemovePlaylist(PlaylistViewModel playlist)
{
Playlists.Remove(playlist);
Subscription sub = _subscriptionsDataService.Data.FirstOrDefault(x => x.Guid == playlist.Guid);
_subscriptionsDataService.Data.Remove(sub);
await _subscriptionsDataService.Save();
}
[RelayCommand]
public async Task Add()
{
if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable)
{
ShowError(StringResourcesManager.SubscriptionsView.Get("NoInternetConnectionError"));
return;
}
Loading = true;
Playlist playlist;
try
{
playlist = await _searchService.SearchPlaylist(Url, 0);
}
catch (MediaSearchException ex)
{
ShowError(StringResourcesManager.Search.Get(ex.StringCode));
return;
}
catch (Exception ex) when (ex is TaskCanceledException || ex is HttpRequestException)
{
ShowError(StringResourcesManager.Search.Get("SearchTimeout"));
return;
}
if (_subscriptionsDataService.Data.Any(x => x.Source == playlist.Source && x.Name == playlist.Name))
{
ShowError(StringResourcesManager.SubscriptionsView.Get("DuplicateError"));
return;
}
Subscription subscription = new Subscription
{
Name = playlist.Name,
Source = playlist.Source,
Url = playlist.Url,
};
playlist.ForEach(x => subscription.VideoIds.Add(x.Id));
_subscriptionsDataService.Data.Add(subscription);
await _subscriptionsDataService.Save();
Playlists.Add(new PlaylistViewModel(subscription.Name, subscription.Source, subscription.Guid));
Loading = false;
}
[RelayCommand]
public void CloseError()
{
Error = null;
IsErrorOpened = true;
}
#endregion
#region PRIVATE METHODS
protected void ShowError(string message)
{
Error = message;
Loading = false;
}
#endregion
}
}

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>VDownload.Core.ViewModels</RootNamespace>
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<UseRidGraph>true</UseRidGraph>
<EnableCoreMrtTooling Condition=" '$(BuildingInsideVisualStudio)' != 'true' ">false</EnableCoreMrtTooling>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.0.240109" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240227000" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="SimpleToolkit.MVVM" Version="1.7.4" />
<PackageReference Include="SimpleToolkit.UI.Models" Version="1.7.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Application\VDownload.Services.Data.Application.csproj" />
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Settings\VDownload.Services.Data.Settings.csproj" />
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.UI\VDownload.Services.UI.Dialogs\VDownload.Services.UI.Dialogs.csproj" />
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.UI\VDownload.Services.UI.DictionaryResources\VDownload.Services.UI.DictionaryResources.csproj" />
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.UI\VDownload.Services.UI.StoragePicker\VDownload.Services.UI.StoragePicker.csproj" />
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.UI\VDownload.Services.UI.WebView\VDownload.Services.UI.WebView.csproj" />
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.Utility\VDownload.Services.Utility.Filename\VDownload.Services.Utility.Filename.csproj" />
<ProjectReference Include="..\..\VDownload.Sources\VDownload.Sources.Twitch\VDownload.Sources.Twitch.Authentication\VDownload.Sources.Twitch.Authentication.csproj" />
<ProjectReference Include="..\..\VDownload.Sources\VDownload.Sources\VDownload.Sources.csproj" />
<ProjectReference Include="..\VDownload.Core.Tasks\VDownload.Core.Tasks.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="VDownload.Core.Views.About.AboutView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VDownload.Core.Views.About"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
mc:Ignorable="d"
Background="Transparent">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="Loaded">
<ic:InvokeCommandAction Command="{Binding NavigationCommand}"/>
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Grid>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center" Spacing="20">
<StackPanel HorizontalAlignment="Center">
<Image Source="{StaticResource ImageLogo}"
Width="150"
Height="150"
HorizontalAlignment="Center"/>
<TextBlock Text="VDownload"
HorizontalAlignment="Center"
FontWeight="SemiBold"
FontSize="30"/>
<TextBlock Text="{Binding Version}"
HorizontalAlignment="Center"
FontSize="16"/>
</StackPanel>
<StackPanel HorizontalAlignment="Center"
Spacing="2">
<TextBlock x:Uid="/VDownload.Core.Strings/AboutViewResources/Developers"
HorizontalAlignment="Center"
FontSize="17"
FontWeight="SemiBold"/>
<ItemsRepeater HorizontalAlignment="Center"
ItemsSource="{Binding Developers}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<HyperlinkButton HorizontalAlignment="Center"
NavigateUri="{Binding Url}">
<TextBlock FontSize="12"
Text="{Binding Name}"/>
</HyperlinkButton>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
<StackPanel HorizontalAlignment="Center"
Spacing="2">
<TextBlock x:Uid="/VDownload.Core.Strings/AboutViewResources/Translation"
HorizontalAlignment="Center"
FontSize="17"
FontWeight="SemiBold"/>
<ItemsRepeater HorizontalAlignment="Center"
ItemsSource="{Binding Translators}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<HyperlinkButton HorizontalAlignment="Center"
NavigateUri="{Binding Url}">
<TextBlock FontSize="12"
Text="{Binding Name}"/>
</HyperlinkButton>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
<StackPanel HorizontalAlignment="Center"
Spacing="2">
<TextBlock x:Uid="/VDownload.Core.Strings/AboutViewResources/More"
HorizontalAlignment="Center"
FontSize="17"
FontWeight="SemiBold"/>
<StackPanel Orientation="Horizontal">
<HyperlinkButton HorizontalAlignment="Center"
NavigateUri="{Binding RepositoryUrl}">
<TextBlock x:Uid="/VDownload.Core.Strings/AboutViewResources/Repository"
FontSize="12"/>
</HyperlinkButton>
<HyperlinkButton HorizontalAlignment="Center"
NavigateUri="{Binding DonationUrl}">
<TextBlock x:Uid="/VDownload.Core.Strings/AboutViewResources/Donation"
FontSize="12"/>
</HyperlinkButton>
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>
</Page>

View File

@@ -0,0 +1,32 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using VDownload.Core.ViewModels.About;
using VDownload.Core.ViewModels.Home;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace VDownload.Core.Views.About
{
public sealed partial class AboutView : Page
{
#region CONSTRUCTORS
public AboutView(AboutViewModel viewModel)
{
this.InitializeComponent();
this.DataContext = viewModel;
}
#endregion
}
}

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="VDownload.Core.Views.Authentication.AuthenticationView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VDownload.Core.Views.Authentication"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ctuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:ctc="using:CommunityToolkit.WinUI.Controls"
xmlns:ct="using:CommunityToolkit.WinUI"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
mc:Ignorable="d"
Background="{ThemeResource ViewBackgroundColor}">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="Loaded">
<ic:InvokeCommandAction Command="{Binding NavigationCommand}"/>
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Grid Padding="20"
RowSpacing="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock x:Uid="/VDownload.Core.Strings/AuthenticationViewResources/Header"
Grid.Row="0"
FontSize="28"
FontWeight="SemiBold"/>
<StackPanel Grid.Row="1"
Spacing="10">
<ctc:SettingsCard Header="Twitch">
<i:Interaction.Behaviors>
<ic:DataTriggerBehavior Binding="{Binding TwitchButtonState, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="NotEqual"
Value="Loading">
<ic:ChangePropertyAction PropertyName="Description"
Value="{Binding TwitchDescription}"/>
<ic:ChangePropertyAction PropertyName="Content">
<ic:ChangePropertyAction.Value>
<Button Command="{Binding TwitchAuthenticationCommand}"
IsEnabled="{Binding TwitchButtonEnable}">
<Button.Content>
<ctuc:SwitchPresenter Value="{Binding TwitchButtonState, Converter={StaticResource ObjectToStringConverter}}">
<ctuc:Case Value="SignIn">
<TextBlock x:Uid="/VDownload.Core.Strings/AuthenticationViewResources/AuthenticationButtonSignIn"/>
</ctuc:Case>
<ctuc:Case Value="SignOut">
<TextBlock x:Uid="/VDownload.Core.Strings/AuthenticationViewResources/AuthenticationButtonSignOut"/>
</ctuc:Case>
</ctuc:SwitchPresenter>
</Button.Content>
</Button>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding TwitchButtonState, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Loading">
<ic:ChangePropertyAction x:Uid="/VDownload.Core.Strings/AuthenticationViewResources/AuthenticationDescriptionLoading"
PropertyName="Description"/>
<ic:ChangePropertyAction PropertyName="Content">
<ic:ChangePropertyAction.Value>
<ProgressRing Width="20"
Height="20"/>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{StaticResource ImageSourcesTwitch}"/>
</ctc:SettingsCard.HeaderIcon>
</ctc:SettingsCard>
</StackPanel>
</Grid>
</Page>

View File

@@ -0,0 +1,18 @@
using Microsoft.UI.Xaml.Controls;
using VDownload.Core.ViewModels.Authentication;
namespace VDownload.Core.Views.Authentication
{
public sealed partial class AuthenticationView : Page
{
#region CONSTRUCTORS
public AuthenticationView(AuthenticationViewModel viewModel)
{
this.InitializeComponent();
this.DataContext = viewModel;
}
#endregion
}
}

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="VDownload.Core.Views.BaseWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
xmlns:cb="using:SimpleToolkit.UI.WinUI.Behaviors"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop Kind="Base"/>
</Window.SystemBackdrop>
<Grid x:Name="Root"
Loaded="Root_Loaded">
<Border x:Name="AppTitleBar"
IsHitTestVisible="True"
VerticalAlignment="Top"
Height="40"
Canvas.ZIndex="1"
Margin="55,4,0,0">
<StackPanel Orientation="Horizontal">
<Image HorizontalAlignment="Left"
VerticalAlignment="Center"
Source="{StaticResource ImageLogo}"
Width="16"
Height="16"/>
<TextBlock Text="VDownload"
VerticalAlignment="Center"
Margin="12, 0, 0, 0"
Style="{StaticResource CaptionTextBlockStyle}"/>
</StackPanel>
</Border>
<NavigationView IsTitleBarAutoPaddingEnabled="True"
IsBackButtonVisible="Collapsed"
PaneDisplayMode="LeftCompact"
Canvas.ZIndex="0"
MenuItemsSource="{Binding Items}"
FooterMenuItemsSource="{Binding FooterItems}"
SelectedItem="{Binding SelectedItem}"
Background="Transparent">
<i:Interaction.Behaviors>
<cb:EventToCommandBehavior Command="{Binding NavigateCommand}"
Event="ItemInvoked"
PassArguments="True"/>
</i:Interaction.Behaviors>
<NavigationView.Resources>
<SolidColorBrush x:Key="NavigationViewContentBackground"
Color="Transparent"/>
</NavigationView.Resources>
<NavigationView.MenuItemTemplate>
<DataTemplate>
<NavigationViewItem Content="{Binding Name}" Tag="{Binding}">
<NavigationViewItem.Icon>
<ImageIcon Source="{Binding IconSource}"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
</DataTemplate>
</NavigationView.MenuItemTemplate>
<Frame Margin="0,48,0,0"
CornerRadius="10"
Content="{Binding CurrentViewModel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource ViewModelToViewConverter}}">
</Frame>
</NavigationView>
</Grid>
</Window>

View File

@@ -0,0 +1,70 @@
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using VDownload.Core.ViewModels;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace VDownload.Core.Views
{
public sealed partial class BaseWindow : Window
{
#region PROPERTIES
public XamlRoot XamlRoot => this.Root.XamlRoot;
#endregion
#region EVENTS
public event EventHandler RootLoaded;
#endregion
#region CONSTRUCTORS
public BaseWindow(BaseViewModel viewModel)
{
this.InitializeComponent();
this.Activated += BaseWindow_Activated;
this.ExtendsContentIntoTitleBar = true;
this.SetTitleBar(this.AppTitleBar);
this.Root.DataContext = viewModel;
}
#endregion
#region PRIVATE METHODS
private void Root_Loaded(object sender, RoutedEventArgs e) => RootLoaded?.Invoke(this, EventArgs.Empty);
private void BaseWindow_Activated(object sender, WindowActivatedEventArgs args)
{
IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowId windowId = Win32Interop.GetWindowIdFromWindow(windowHandle);
AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
appWindow.SetIcon(@"Assets\Logo\Logo.ico");
}
#endregion
}
}

View File

@@ -0,0 +1,397 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="VDownload.Core.Views.Home.HomeDownloadsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VDownload.Core.Views.Home"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
xmlns:m="using:VDownload.Models"
xmlns:ct="using:CommunityToolkit.WinUI"
xmlns:ctc="using:CommunityToolkit.WinUI.Controls"
xmlns:ctuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:ctb="using:CommunityToolkit.WinUI.Behaviors"
mc:Ignorable="d"
x:Name="Root"
Background="Transparent">
<ctuc:SwitchPresenter Value="{Binding TaskListIsEmpty, Converter={StaticResource ObjectToStringConverter}}">
<ctuc:Case Value="True">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image Source="{StaticResource ImageHomeDownloadsViewNoTasks}"
Width="100"/>
<TextBlock Text="Click Video/Playlist search button to add new tasks"
Foreground="{StaticResource GreyText}"/>
</StackPanel>
</ctuc:Case>
<ctuc:Case Value="False">
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="{ThemeResource ViewBackgroundColor}"
CornerRadius="10"
Height="150"
Margin="0,0,0,10">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0"
Source="{Binding Video.ThumbnailUrl, TargetNullValue={StaticResource ImageOtherThumbnail}}"
VerticalAlignment="Stretch"/>
<Grid Grid.Column="1"
Margin="10"
RowSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Grid.Row="0"
ColumnSpacing="10"
HorizontalAlignment="Left">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
FontWeight="SemiBold"
FontSize="18"
Text="{Binding Video.Title}"
TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
FontWeight="Light"
FontSize="12"
Text="{Binding Video.Author}"/>
</Grid>
<Grid Grid.Row="1"
RowSpacing="10"
ColumnSpacing="10">
<Grid.Resources>
<x:Double x:Key="TextSize">12</x:Double>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Grid.Row="0"
Grid.Column="0"
Source="{ThemeResource ImageHomeDownloadsViewQuality}"/>
<StackPanel Grid.Row="0"
Grid.Column="1"
VerticalAlignment="Center"
Orientation="Horizontal">
<ctuc:SwitchPresenter Value="{Binding DownloadOptions.MediaType, Converter={StaticResource ObjectToStringConverter}}">
<ctuc:Case Value="Original">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOriginal"
FontSize="{StaticResource TextSize}"/>
</ctuc:Case>
<ctuc:Case Value="OnlyVideo">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOnlyVideo"
FontSize="{StaticResource TextSize}"/>
</ctuc:Case>
<ctuc:Case Value="OnlyAudio">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOnlyAudio"
FontSize="{StaticResource TextSize}"/>
</ctuc:Case>
</ctuc:SwitchPresenter>
<TextBlock FontSize="{StaticResource TextSize}">
<Run Text=" ("/><Run Text="{Binding DownloadOptions.SelectedStream.Name}"/>)
</TextBlock>
</StackPanel>
<Image Grid.Row="1"
Grid.Column="0"
Source="{ThemeResource ImageHomeDownloadsViewTime}"/>
<StackPanel Grid.Row="1"
Grid.Column="1"
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="{Binding DownloadOptions.DurationAfterTrim}"
FontSize="{StaticResource TextSize}"/>
<TextBlock Visibility="{Binding DownloadOptions.IsTrimmed, Converter={StaticResource BoolToVisibilityConverter}}"
FontSize="{StaticResource TextSize}">
<Run Text=" "/>(<Run Text="{Binding DownloadOptions.TrimStart}"/> - <Run Text="{Binding DownloadOptions.TrimEnd}"/>)
</TextBlock>
</StackPanel>
<Image Grid.Row="2"
Grid.Column="0"
Source="{ThemeResource ImageHomeDownloadsViewFile}"/>
<TextBlock Grid.Row="2"
Grid.Column="1"
FontSize="{StaticResource TextSize}"
VerticalAlignment="Center">
<Run Text="{Binding DownloadOptions.Directory}"/>\<Run Text="{Binding DownloadOptions.Filename}"/>.<Run Text="{Binding DownloadOptions.Extension}"/>
</TextBlock>
<Image Grid.Row="3"
Grid.Column="0">
<i:Interaction.Behaviors>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Idle">
<ic:ChangePropertyAction PropertyName="Source"
Value="{ThemeResource ImageHomeDownloadsViewIdle}"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Queued">
<ic:ChangePropertyAction PropertyName="Source"
Value="{ThemeResource ImageHomeDownloadsViewQueued}"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Initializing">
<ic:ChangePropertyAction PropertyName="Source"
Value="{ThemeResource ImageHomeDownloadsViewInitializing}"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Downloading">
<ic:ChangePropertyAction PropertyName="Source"
Value="{ThemeResource ImageHomeDownloadsViewDownloading}"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Processing">
<ic:ChangePropertyAction PropertyName="Source"
Value="{ThemeResource ImageHomeDownloadsViewProcessing}"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Finalizing">
<ic:ChangePropertyAction PropertyName="Source"
Value="{ThemeResource ImageHomeDownloadsViewFinalizing}"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="EndedUnsuccessfully">
<ic:ChangePropertyAction PropertyName="Source"
Value="{ThemeResource ImageHomeDownloadsViewError}"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="EndedSuccessfully">
<ic:ChangePropertyAction PropertyName="Source"
Value="{ThemeResource ImageHomeDownloadsViewDone}"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="EndedCancelled">
<ic:ChangePropertyAction PropertyName="Source"
Value="{ThemeResource ImageHomeDownloadsViewCancelled}"/>
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
</Image>
<Border Grid.Row="3"
Grid.Column="1"
VerticalAlignment="Center">
<i:Interaction.Behaviors>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Idle">
<ic:ChangePropertyAction PropertyName="Child">
<ic:ChangePropertyAction.Value>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeDownloadsViewResources/StatusIdle"
FontSize="{StaticResource TextSize}"/>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Queued">
<ic:ChangePropertyAction PropertyName="Child">
<ic:ChangePropertyAction.Value>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeDownloadsViewResources/StatusQueued"
FontSize="{StaticResource TextSize}"/>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Initializing">
<ic:ChangePropertyAction PropertyName="Child">
<ic:ChangePropertyAction.Value>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeDownloadsViewResources/StatusInitializing"
FontSize="{StaticResource TextSize}"/>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Downloading">
<ic:ChangePropertyAction PropertyName="Child">
<ic:ChangePropertyAction.Value>
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="/VDownload.Core.Strings/HomeDownloadsViewResources/StatusDownloading"
FontSize="{StaticResource TextSize}"/>
<TextBlock Text="{Binding Progress, Converter={StaticResource StringFormatConverter}, ConverterParameter='{} ({0:0.##}%)'}"
FontSize="{StaticResource TextSize}"/>
</StackPanel>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Processing">
<ic:ChangePropertyAction PropertyName="Child">
<ic:ChangePropertyAction.Value>
<TextBlock FontSize="{StaticResource TextSize}">
<Run x:Uid="/VDownload.Core.Strings/HomeDownloadsViewResources/StatusProcessing"/><Run Text=" ("/><Run Text="{Binding Progress, Converter={StaticResource StringFormatConverter}, ConverterParameter='{}{0:0.##}'}"/>%)
</TextBlock>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Finalizing">
<ic:ChangePropertyAction PropertyName="Child">
<ic:ChangePropertyAction.Value>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeDownloadsViewResources/StatusFinalizing"
FontSize="{StaticResource TextSize}"/>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="EndedUnsuccessfully">
<ic:ChangePropertyAction PropertyName="Child">
<ic:ChangePropertyAction.Value>
<TextBlock FontSize="{StaticResource TextSize}">
<Run x:Uid="/VDownload.Core.Strings/HomeDownloadsViewResources/StatusError"/><Run Text=" ("/><Run Text="{Binding Error}"/>)
</TextBlock>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="EndedSuccessfully">
<ic:ChangePropertyAction PropertyName="Child">
<ic:ChangePropertyAction.Value>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeDownloadsViewResources/StatusDone"
FontSize="{StaticResource TextSize}"/>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="EndedCancelled">
<ic:ChangePropertyAction PropertyName="Child">
<ic:ChangePropertyAction.Value>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeDownloadsViewResources/StatusCancelled"
FontSize="{StaticResource TextSize}"/>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
</Border>
</Grid>
</Grid>
<Grid Grid.Column="2"
Margin="0,0,5,0">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<AppBarButton Grid.Row="0"
Width="40"
Height="48">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="Click">
<ctb:NavigateToUriAction NavigateUri="{Binding Video.Url}"/>
</ic:EventTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Video.Source, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="Twitch">
<ic:ChangePropertyAction PropertyName="Icon">
<ic:ChangePropertyAction.Value>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{StaticResource ImageSourcesTwitch}"/>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
</AppBarButton>
<AppBarButton Grid.Row="1"
Width="40"
Height="48"
Command="{Binding ElementName=Root, Path=DataContext.StartCancelTaskCommand}"
CommandParameter="{Binding}">
<i:Interaction.Behaviors>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToIntConverter}}"
ComparisonCondition="LessThan"
Value="4">
<ic:ChangePropertyAction PropertyName="Icon">
<ic:ChangePropertyAction.Value>
<SymbolIcon Symbol="Download"/>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToIntConverter}}"
ComparisonCondition="GreaterThanOrEqual"
Value="4">
<ic:ChangePropertyAction PropertyName="Icon">
<ic:ChangePropertyAction.Value>
<SymbolIcon Symbol="Cancel"/>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
</AppBarButton>
<AppBarButton Grid.Row="2"
Icon="Delete"
Width="40"
Height="48"
Command="{Binding ElementName=Root, Path=DataContext.RemoveTaskCommand}"
CommandParameter="{Binding}"/>
</Grid>
</Grid>
<ProgressBar Grid.Row="1"
Value="{Binding Progress}"
Maximum="100">
<i:Interaction.Behaviors>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToIntConverter}}"
ComparisonCondition="LessThan"
Value="5">
<ic:ChangePropertyAction PropertyName="Visibility" Value="Collapsed"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToIntConverter}}"
ComparisonCondition="GreaterThanOrEqual"
Value="5">
<ic:ChangePropertyAction PropertyName="Visibility" Value="Visible"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToIntConverter}}"
ComparisonCondition="LessThan"
Value="7">
<ic:ChangePropertyAction PropertyName="IsIndeterminate" Value="True"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Status, Converter={StaticResource ObjectToIntConverter}}"
ComparisonCondition="GreaterThanOrEqual"
Value="7">
<ic:ChangePropertyAction PropertyName="IsIndeterminate" Value="False"/>
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
</ProgressBar>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</ctuc:Case>
</ctuc:SwitchPresenter>
</Page>

View File

@@ -0,0 +1,31 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using VDownload.Core.ViewModels.Home;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace VDownload.Core.Views.Home
{
public sealed partial class HomeDownloadsView : Page
{
#region CONSTRUCTORS
public HomeDownloadsView(HomeDownloadsViewModel viewModel)
{
this.InitializeComponent();
this.DataContext = viewModel;
}
#endregion
}
}

View File

@@ -0,0 +1,424 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="VDownload.Core.Views.Home.HomeVideoCollectionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VDownload.Core.Views.Home"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
xmlns:m="using:VDownload.Models"
xmlns:ct="using:CommunityToolkit.WinUI"
xmlns:ctc="using:CommunityToolkit.WinUI.Controls"
xmlns:ctuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:c="using:SimpleToolkit.UI.WinUI.Controls"
mc:Ignorable="d"
Background="{ThemeResource ViewBackgroundColor}"
x:Name="Root">
<Grid Padding="15"
RowSpacing="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding Name}"
FontWeight="Bold"
FontSize="20"
TextWrapping="WrapWholeWords"/>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Margin="-5"
Spacing="10">
<TextBlock VerticalAlignment="Center"
Visibility="{Binding IsSomethingHidden, Converter={StaticResource BoolToVisibilityConverter}}">
<Run x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/HiddenTextBlock"/><Run Text="{Binding HiddenCount}"/>
</TextBlock>
<AppBarToggleButton x:Name="FilterButton"
Icon="Filter"
Width="40"
Height="48">
<AppBarToggleButton.Resources>
<TeachingTip x:Name="FilterWindow"
x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilterWindow"
Target="{Binding ElementName=FilterButton}"
PreferredPlacement="BottomLeft"
IsOpen="{Binding ElementName=FilterButton, Path=IsChecked, Mode=TwoWay}">
<Grid VerticalAlignment="Top"
Margin="0,10,0,0"
ColumnSpacing="10"
RowSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilterTitleTextBlock"
Grid.Row="0"
Grid.Column="0"
VerticalAlignment="Center"/>
<TextBox x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilterTitleTextBox"
Grid.Row="0"
Grid.Column="1"
Text="{Binding TitleFilter, Mode=TwoWay}"/>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilterAuthorTextBlock"
Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center"/>
<TextBox x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilterAuthorTextBox"
Grid.Row="1"
Grid.Column="1"
Text="{Binding AuthorFilter, Mode=TwoWay}"/>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilterViewsTextBlock"
Grid.Row="2"
Grid.Column="0"
VerticalAlignment="Center"/>
<Grid Grid.Row="2"
Grid.Column="1"
ColumnSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<NumberBox Grid.Column="0"
SpinButtonPlacementMode="Compact"
Minimum="{Binding MinViews}"
Value="{Binding MinViewsFilter, Mode=TwoWay}"
Maximum="{Binding MaxViewsFilter}"
SmallChange="1"
LargeChange="10"/>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Text="-"/>
<NumberBox Grid.Column="2"
SpinButtonPlacementMode="Compact"
Minimum="{Binding MinViewsFilter}"
Value="{Binding MaxViewsFilter, Mode=TwoWay}"
Maximum="{Binding MaxViews}"
SmallChange="1"
LargeChange="10"/>
</Grid>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilterDateTextBlock"
Grid.Row="3"
Grid.Column="0"
VerticalAlignment="Center"/>
<Grid Grid.Row="3"
Grid.Column="1"
ColumnSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CalendarDatePicker Grid.Column="0"
MinDate="{Binding MinDate}"
Date="{Binding MinDateFilter, Mode=TwoWay}"
MaxDate="{Binding MaxDateFilter}"/>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Text="-"/>
<CalendarDatePicker Grid.Column="2"
MinDate="{Binding MinDateFilter}"
Date="{Binding MaxDateFilter, Mode=TwoWay}"
MaxDate="{Binding MaxDate}"/>
</Grid>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilterDurationTextBlock"
Grid.Row="4"
Grid.Column="0"
VerticalAlignment="Center"/>
<Grid Grid.Row="4"
Grid.Column="1"
RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<c:TimeSpanControl Grid.Row="0"
Minimum="{Binding MinDuration}"
Value="{Binding MinDurationFilter, Mode=TwoWay}"
Maximum="{Binding MaxDurationFilter}"/>
<c:TimeSpanControl Grid.Row="1"
Minimum="{Binding MinDurationFilter}"
Value="{Binding MaxDurationFilter, Mode=TwoWay}"
Maximum="{Binding MaxDuration}"/>
</Grid>
<TextBlock x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilterRemovedTextBlock"
Grid.Row="5"
Grid.Column="0"
VerticalAlignment="Center"/>
<Grid Grid.Row="5"
Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding RemovedCount}"/>
<Button x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilterRemovedButton"
Grid.Column="1"
Command="{Binding RestoreRemovedVideosCommand}"/>
</Grid>
</Grid>
</TeachingTip>
</AppBarToggleButton.Resources>
</AppBarToggleButton>
<AppBarButton x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/ApplyDirectoryButton"
Icon="Folder"
Width="40"
Height="48"
Command="{Binding SelectDirectoryCommand}"/>
</StackPanel>
</Grid>
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding Videos}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander HorizontalAlignment="Stretch"
Margin="0,0,0,10"
CornerRadius="10"
HorizontalContentAlignment="Stretch"
Visibility="{Binding Value, Converter={StaticResource BoolToVisibilityConverter}}">
<Expander.Header>
<Grid Padding="-16,0,-16,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0"
Source="{Binding Key.ThumbnailUrl, TargetNullValue={StaticResource ImageOtherThumbnail}}"
Height="100"/>
<Grid Grid.Column="1"
Margin="10"
RowSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
FontSize="16"
Text="{Binding Key.Title}"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"/>
<Grid Grid.Row="1"
RowSpacing="10"
ColumnSpacing="10">
<Grid.Resources>
<x:Double x:Key="IconSize">17</x:Double>
<x:Double x:Key="FontSize">13</x:Double>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Image Grid.Column="0"
Grid.Row="0"
VerticalAlignment="Center"
Source="{ThemeResource ImageHomeVideoCollectionViewAuthor}"
Width="{StaticResource IconSize}"/>
<TextBlock Grid.Column="1"
Grid.Row="0"
FontSize="{StaticResource FontSize}"
VerticalAlignment="Center"
Text="{Binding Key.Author}"/>
<Image Grid.Column="0"
Grid.Row="1"
VerticalAlignment="Center"
Source="{ThemeResource ImageHomeVideoCollectionViewDate}"
Width="{StaticResource IconSize}"/>
<TextBlock Grid.Column="1"
Grid.Row="1"
FontSize="{StaticResource FontSize}"
VerticalAlignment="Center"
Text="{Binding Key.PublishDate}"/>
<Image Grid.Column="2"
Grid.Row="0"
VerticalAlignment="Center"
Source="{ThemeResource ImageHomeVideoCollectionViewTime}"
Width="{StaticResource IconSize}"/>
<TextBlock Grid.Column="3"
Grid.Row="0"
FontSize="{StaticResource FontSize}"
VerticalAlignment="Center"
Text="{Binding Key.Duration}"/>
<Image Grid.Column="2"
Grid.Row="1"
VerticalAlignment="Center"
Source="{ThemeResource ImageHomeVideoCollectionViewViews}"
Width="{StaticResource IconSize}"/>
<TextBlock Grid.Column="3"
Grid.Row="1"
FontSize="{StaticResource FontSize}"
VerticalAlignment="Center"
Text="{Binding Key.Views}"/>
</Grid>
</Grid>
<AppBarButton Grid.Column="2"
Width="40"
Height="48"
Icon="Delete"
VerticalAlignment="Center"
Command="{Binding ElementName=Root, Path=DataContext.RemoveVideoCommand}"
CommandParameter="{Binding Key}"/>
</Grid>
</Expander.Header>
<Expander.Content>
<StackPanel Spacing="20">
<StackPanel Spacing="5">
<TextBlock x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/MediaOptionsHeader"
FontWeight="Bold"
FontSize="15"/>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/QualitySettingsCard">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoCollectionViewMedia}"/>
</ctc:SettingsCard.HeaderIcon>
<ComboBox ItemsSource="{Binding Key.Streams}"
SelectedItem="{Binding Key.SelectedStream, Mode=TwoWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/MediaTypeSettingsCard">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoCollectionViewMedia}"/>
</ctc:SettingsCard.HeaderIcon>
<ComboBox ItemsSource="{ct:EnumValues Type=m:MediaType}"
SelectedItem="{Binding Key.MediaType, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<ctuc:SwitchPresenter Value="{Binding Converter={StaticResource ObjectToStringConverter}}">
<ctuc:Case Value="Original">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOriginal"/>
</ctuc:Case>
<ctuc:Case Value="OnlyVideo">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOnlyVideo"/>
</ctuc:Case>
<ctuc:Case Value="OnlyAudio">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOnlyAudio"/>
</ctuc:Case>
</ctuc:SwitchPresenter>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ctc:SettingsCard>
<ctc:SettingsExpander x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/TrimSettingsGroup">
<ctc:SettingsExpander.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoCollectionViewTrim}"/>
</ctc:SettingsExpander.HeaderIcon>
<ctc:SettingsExpander.Items>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/TrimStartSettingsCard">
<c:TimeSpanControl Value="{Binding Key.TrimStart, Mode=TwoWay}"
Maximum="{Binding Key.TrimEnd, Mode=OneWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/TrimEndSettingsCard">
<c:TimeSpanControl Minimum="{Binding Key.TrimStart, Mode=OneWay}"
Value="{Binding Key.TrimEnd, Mode=TwoWay}"
Maximum="{Binding Key.Duration, Mode=OneWay}"/>
</ctc:SettingsCard>
</ctc:SettingsExpander.Items>
</ctc:SettingsExpander>
</StackPanel>
<StackPanel Spacing="5">
<TextBlock x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FileOptionsHeader"
FontWeight="Bold"
FontSize="15"/>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/DirectorySettingsCard"
Description="{Binding Key.DirectoryPath}">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoCollectionViewDirectory}"/>
</ctc:SettingsCard.HeaderIcon>
<Button x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/DirectorySettingsCardButton"
Command="{Binding Key.BrowseCommand}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FilenameSettingsCard">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoCollectionViewFilename}"/>
</ctc:SettingsCard.HeaderIcon>
<TextBox Text="{Binding Key.Filename, Mode=TwoWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/FileTypeSettingsCard">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoCollectionViewExtension}"/>
</ctc:SettingsCard.HeaderIcon>
<i:Interaction.Behaviors>
<ic:DataTriggerBehavior Binding="{Binding Key.MediaType, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="OnlyAudio">
<ic:ChangePropertyAction PropertyName="Content">
<ic:ChangePropertyAction.Value>
<ComboBox ItemsSource="{ct:EnumValues Type=m:AudioExtension}"
SelectedItem="{Binding Key.AudioExtension, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding Key.MediaType, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="NotEqual"
Value="OnlyAudio">
<ic:ChangePropertyAction PropertyName="Content">
<ic:ChangePropertyAction.Value>
<ComboBox ItemsSource="{ct:EnumValues Type=m:VideoExtension}"
SelectedItem="{Binding Key.VideoExtension, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
</ctc:SettingsCard>
</StackPanel>
</StackPanel>
</Expander.Content>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<StackPanel Grid.Row="2"
HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="10">
<Button x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/CreateAndStartButton"
Command="{Binding CreateTasksAndDownloadCommand}"/>
<Button x:Uid="/VDownload.Core.Strings/HomeVideoCollectionViewResources/CreateButton"
Style="{StaticResource AccentButtonStyle}"
Command="{Binding CreateTasksCommand}"/>
</StackPanel>
</Grid>
</Page>

View File

@@ -0,0 +1,31 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using VDownload.Core.ViewModels.Home;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace VDownload.Core.Views.Home
{
public sealed partial class HomeVideoCollectionView : Page
{
#region CONSTRUCTORS
public HomeVideoCollectionView(HomeVideoCollectionViewModel viewModel)
{
this.InitializeComponent();
this.DataContext = viewModel;
}
#endregion
}
}

View File

@@ -0,0 +1,220 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="VDownload.Core.Views.Home.HomeVideoView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VDownload.Core.Views.Home"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
xmlns:m="using:VDownload.Models"
xmlns:ct="using:CommunityToolkit.WinUI"
xmlns:ctc="using:CommunityToolkit.WinUI.Controls"
xmlns:ctuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:c="using:SimpleToolkit.UI.WinUI.Controls"
mc:Ignorable="d"
Background="{ThemeResource ViewBackgroundColor}">
<Grid Padding="15"
RowSpacing="20">
<Grid.RowDefinitions>
<RowDefinition Height="150"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0"
ColumnSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
CornerRadius="{ThemeResource ControlCornerRadius}">
<Image Source="{Binding ThumbnailUrl, TargetNullValue={StaticResource ImageOtherThumbnail}}"
VerticalAlignment="Stretch"/>
</Border>
<StackPanel Grid.Column="1"
Spacing="15">
<TextBlock Text="{Binding Title}"
FontWeight="Bold"
FontSize="20"
TextWrapping="WrapWholeWords"/>
<Grid ColumnSpacing="10"
RowSpacing="10">
<Grid.Resources>
<x:Double x:Key="IconSize">18</x:Double>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Image Grid.Column="0"
Grid.Row="0"
Source="{ThemeResource ImageHomeVideoViewAuthor}"
Width="{StaticResource IconSize}"/>
<TextBlock Grid.Column="1"
Grid.Row="0"
Text="{Binding Author}"/>
<Image Grid.Column="0"
Grid.Row="1"
Source="{ThemeResource ImageHomeVideoViewDate}"
Width="{StaticResource IconSize}"/>
<TextBlock Grid.Column="1"
Grid.Row="1"
Text="{Binding PublishDate}"/>
<Image Grid.Column="0"
Grid.Row="2"
Source="{ThemeResource ImageHomeVideoViewTime}"
Width="{StaticResource IconSize}"/>
<TextBlock Grid.Column="1"
Grid.Row="2"
Text="{Binding Duration}"/>
<Image Grid.Column="0"
Grid.Row="3"
Source="{ThemeResource ImageHomeVideoViewViews}"
Width="{StaticResource IconSize}"/>
<TextBlock Grid.Column="1"
Grid.Row="3"
Text="{Binding Views}"/>
</Grid>
</StackPanel>
</Grid>
<ScrollViewer Grid.Row="1">
<StackPanel Spacing="20">
<StackPanel Spacing="5">
<TextBlock x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/MediaOptionsHeader"
FontWeight="Bold"
FontSize="15"/>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/QualitySettingsCard">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoViewQuality}"/>
</ctc:SettingsCard.HeaderIcon>
<ComboBox ItemsSource="{Binding Streams}"
SelectedItem="{Binding SelectedStream, Mode=TwoWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/MediaTypeSettingsCard">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoViewMedia}"/>
</ctc:SettingsCard.HeaderIcon>
<ComboBox ItemsSource="{ct:EnumValues Type=m:MediaType}"
SelectedItem="{Binding MediaType, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<ctuc:SwitchPresenter Value="{Binding Converter={StaticResource ObjectToStringConverter}}">
<ctuc:Case Value="Original">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOriginal"/>
</ctuc:Case>
<ctuc:Case Value="OnlyVideo">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOnlyVideo"/>
</ctuc:Case>
<ctuc:Case Value="OnlyAudio">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOnlyAudio"/>
</ctuc:Case>
</ctuc:SwitchPresenter>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ctc:SettingsCard>
<ctc:SettingsExpander x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/TrimSettingsGroup">
<ctc:SettingsExpander.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoViewTrim}"/>
</ctc:SettingsExpander.HeaderIcon>
<ctc:SettingsExpander.Items>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/TrimStartSettingsCard">
<c:TimeSpanControl Value="{Binding TrimStart, Mode=TwoWay}"
Maximum="{Binding TrimEnd, Mode=OneWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/TrimEndSettingsCard">
<c:TimeSpanControl Minimum="{Binding TrimStart, Mode=OneWay}"
Value="{Binding TrimEnd, Mode=TwoWay}"
Maximum="{Binding Duration, Mode=OneWay}"/>
</ctc:SettingsCard>
</ctc:SettingsExpander.Items>
</ctc:SettingsExpander>
</StackPanel>
<StackPanel Spacing="5">
<TextBlock x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/FileOptionsHeader"
FontWeight="Bold"
FontSize="15"/>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/DirectorySettingsCard"
Description="{Binding DirectoryPath}">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoViewDirectory}"/>
</ctc:SettingsCard.HeaderIcon>
<Button x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/DirectorySettingsCardButton"
Command="{Binding BrowseCommand}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/FilenameSettingsCard">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoViewFilename}"/>
</ctc:SettingsCard.HeaderIcon>
<TextBox Text="{Binding Filename, Mode=TwoWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/FileTypeSettingsCard">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageHomeVideoViewExtension}"/>
</ctc:SettingsCard.HeaderIcon>
<i:Interaction.Behaviors>
<ic:DataTriggerBehavior Binding="{Binding MediaType, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="OnlyAudio">
<ic:ChangePropertyAction PropertyName="Content">
<ic:ChangePropertyAction.Value>
<ComboBox ItemsSource="{ct:EnumValues Type=m:AudioExtension}"
SelectedItem="{Binding AudioExtension, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding MediaType, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="NotEqual"
Value="OnlyAudio">
<ic:ChangePropertyAction PropertyName="Content">
<ic:ChangePropertyAction.Value>
<ComboBox ItemsSource="{ct:EnumValues Type=m:VideoExtension}"
SelectedItem="{Binding VideoExtension, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ic:ChangePropertyAction.Value>
</ic:ChangePropertyAction>
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
</ctc:SettingsCard>
</StackPanel>
</StackPanel>
</ScrollViewer>
<StackPanel Grid.Row="2"
HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="10">
<Button x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/CreateAndStartButton"
Command="{Binding CreateTaskAndDownloadCommand}"/>
<Button x:Uid="/VDownload.Core.Strings/HomeVideoViewResources/CreateButton"
Style="{StaticResource AccentButtonStyle}"
Command="{Binding CreateTaskCommand}"/>
</StackPanel>
</Grid>
</Page>

View File

@@ -0,0 +1,31 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using VDownload.Core.ViewModels.Home;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace VDownload.Core.Views.Home
{
public sealed partial class HomeVideoView : Page
{
#region CONSTRUCTORS
public HomeVideoView(HomeVideoViewModel viewModel)
{
this.InitializeComponent();
this.DataContext = viewModel;
}
#endregion
}
}

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="VDownload.Core.Views.Home.HomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
xmlns:ctuc="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d"
Background="Transparent"
x:Name="Root">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="Loaded">
<ic:InvokeCommandAction Command="{Binding NavigationCommand}"/>
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Grid RowSpacing="10"
Margin="10">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Frame Content="{Binding MainContent, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource ViewModelToViewConverter}}">
</Frame>
<ctuc:SwitchPresenter Grid.Row="1"
Value="{Binding OptionBarError, Converter={StaticResource IsNotNullConverter}}">
<ctuc:Case Value="False">
<Grid Background="{ThemeResource OptionBarBackgroundColor}"
CornerRadius="10">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ctuc:UniformGrid Grid.Column="0"
Rows="1"
Margin="15,0,0,0">
<ctuc:UniformGrid.RowDefinitions>
<RowDefinition/>
</ctuc:UniformGrid.RowDefinitions>
<ctuc:UniformGrid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</ctuc:UniformGrid.ColumnDefinitions>
<ctuc:SwitchPresenter Grid.Row="0"
VerticalAlignment="Stretch"
Margin="0,0,15,0"
Value="{Binding OptionBarContent, Converter={StaticResource ObjectToStringConverter}}">
<i:Interaction.Behaviors>
<ic:DataTriggerBehavior Binding="{Binding OptionBarContent, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="Equal"
Value="None">
<ic:ChangePropertyAction PropertyName="Visibility"
Value="Collapsed"/>
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior Binding="{Binding OptionBarContent, Converter={StaticResource ObjectToStringConverter}}"
ComparisonCondition="NotEqual"
Value="None">
<ic:ChangePropertyAction PropertyName="Visibility"
Value="Visible"/>
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
<ctuc:Case Value="VideoSearch">
<Grid ColumnSpacing="10"
VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Uid="/VDownload.Core.Strings/HomeViewResources/OptionBarVideoSearchContentTextBox"
Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding OptionBarVideoSearchTBValue, Mode=TwoWay}"/>
<Button x:Uid="/VDownload.Core.Strings/HomeViewResources/OptionBarSearchButton"
Grid.Column="1"
IsEnabled="{Binding OptionBarSearchNotPending}"
Command="{Binding VideoSearchStartCommand}"/>
</Grid>
</ctuc:Case>
<ctuc:Case Value="PlaylistSearch">
<Grid ColumnSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Uid="/VDownload.Core.Strings/HomeViewResources/OptionBarPlaylistSearchContentTextBox"
Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding OptionBarPlaylistSearchTBValue, Mode=TwoWay}"/>
<NumberBox x:Uid="/VDownload.Core.Strings/HomeViewResources/OptionBarPlaylistSearchContentNumberBox"
Grid.Column="1"
VerticalAlignment="Center"
SpinButtonPlacementMode="Compact"
SmallChange="1"
LargeChange="10"
Value="{Binding OptionBarPlaylistSearchNBValue, Mode=TwoWay}"
Minimum="0"/>
<Button x:Uid="/VDownload.Core.Strings/HomeViewResources/OptionBarSearchButton"
Grid.Column="2"
IsEnabled="{Binding OptionBarSearchNotPending}"
Command="{Binding PlaylistSearchStartCommand}"/>
</Grid>
</ctuc:Case>
</ctuc:SwitchPresenter>
<StackPanel VerticalAlignment="Center"
Orientation="Horizontal">
<ProgressRing Width="20"
Height="20"
Margin="0,0,10,0"
Visibility="{Binding OptionBarLoading, Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock Text="{Binding OptionBarMessage}"/>
</StackPanel>
</ctuc:UniformGrid>
<StackPanel Grid.Column="2"
Orientation="Horizontal"
Margin="0,0,4,0">
<AppBarToggleButton x:Uid="/VDownload.Core.Strings/HomeViewResources/OptionBarLoadSubscription"
Icon="Favorite"
IsEnabled="{Binding OptionBarSearchNotPending}"
IsChecked="{Binding OptionBarLoadSubscriptionButtonChecked, Mode=TwoWay}"
Command="{Binding LoadFromSubscriptionCommand}"/>
<AppBarToggleButton x:Uid="/VDownload.Core.Strings/HomeViewResources/OptionBarVideoSearch"
Icon="Video"
IsEnabled="{Binding OptionBarSearchNotPending}"
IsChecked="{Binding OptionBarVideoSearchButtonChecked, Mode=TwoWay}"
Command="{Binding VideoSearchShowCommand}"/>
<AppBarToggleButton x:Uid="/VDownload.Core.Strings/HomeViewResources/OptionBarPlaylistSearch"
Icon="List"
IsEnabled="{Binding OptionBarSearchNotPending}"
IsChecked="{Binding OptionBarPlaylistSearchButtonChecked, Mode=TwoWay}"
Command="{Binding PlaylistSearchShowCommand}"/>
<AppBarSeparator/>
<AppBarButton x:Uid="/VDownload.Core.Strings/HomeViewResources/OptionBarCancelAll"
Icon="Cancel"
Command="{Binding CancelCommand}"/>
<AppBarButton x:Uid="/VDownload.Core.Strings/HomeViewResources/OptionBarDownloadAll"
Icon="Download"
Command="{Binding DownloadCommand}"/>
</StackPanel>
</Grid>
</ctuc:Case>
<ctuc:Case Value="True">
<InfoBar x:Uid="/VDownload.Core.Strings/HomeViewResources/ErrorInfoBar"
Severity="Error"
IsOpen="{Binding OptionBarIsErrorOpened, Mode=TwoWay}"
Message="{Binding OptionBarError}"
CloseButtonCommand="{Binding CloseErrorCommand}"/>
</ctuc:Case>
</ctuc:SwitchPresenter>
</Grid>
</Page>

View File

@@ -0,0 +1,18 @@
using Microsoft.UI.Xaml.Controls;
using VDownload.Core.ViewModels.Home;
namespace VDownload.Core.Views.Home
{
public sealed partial class HomeView : Page
{
#region CONSTRUCTORS
public HomeView(HomeViewModel viewModel)
{
this.InitializeComponent();
this.DataContext = viewModel;
}
#endregion
}
}

View File

@@ -0,0 +1,312 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="VDownload.Core.Views.Settings.SettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VDownload.Core.Views.Settings"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ctuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:ctc="using:CommunityToolkit.WinUI.Controls"
xmlns:ct="using:CommunityToolkit.WinUI"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
xmlns:m="using:VDownload.Models"
mc:Ignorable="d"
Background="{ThemeResource ViewBackgroundColor}">
<Grid Padding="20"
RowSpacing="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock x:Uid="/VDownload.Core.Strings/SettingsViewResources/Header"
Grid.Row="0"
FontSize="28"
FontWeight="SemiBold"/>
<ScrollViewer Grid.Row="1">
<StackPanel Spacing="20">
<!-- Searching -->
<StackPanel Spacing="5">
<TextBlock x:Uid="/VDownload.Core.Strings/SettingsViewResources/SearchingHeader"
FontWeight="Bold"
FontSize="15"/>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/SearchingPlaylistCount">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewSearchingPlaylistCount}"/>
</ctc:SettingsCard.HeaderIcon>
<NumberBox Value="{Binding SearchingPlaylistCount, Mode=TwoWay}"
Minimum="0"
SmallChange="1"
LargeChange="10"
SpinButtonPlacementMode="Compact"/>
</ctc:SettingsCard>
</StackPanel>
<!-- Tasks -->
<StackPanel Spacing="5">
<TextBlock x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksHeader"
FontWeight="Bold"
FontSize="15"/>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksRunningTasks">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewTasksRunningTasks}"/>
</ctc:SettingsCard.HeaderIcon>
<NumberBox Value="{Binding TasksRunningTasks, Mode=TwoWay}"
Minimum="1"
SmallChange="1"
LargeChange="10"
SpinButtonPlacementMode="Compact"/>
</ctc:SettingsCard>
<ctc:SettingsExpander x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksDefaultMediaOptions">
<ctc:SettingsExpander.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewTasksDefaultMediaOptions}"/>
</ctc:SettingsExpander.HeaderIcon>
<ctc:SettingsExpander.Items>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksMediaType">
<ComboBox ItemsSource="{ct:EnumValues Type=m:MediaType}"
SelectedItem="{Binding TasksMediaType, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<ctuc:SwitchPresenter Value="{Binding Converter={StaticResource ObjectToStringConverter}}">
<ctuc:Case Value="Original">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOriginal"/>
</ctuc:Case>
<ctuc:Case Value="OnlyVideo">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOnlyVideo"/>
</ctuc:Case>
<ctuc:Case Value="OnlyAudio">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/MediaTypeOnlyAudio"/>
</ctuc:Case>
</ctuc:SwitchPresenter>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksVideoExtension">
<ComboBox ItemsSource="{ct:EnumValues Type=m:VideoExtension}"
SelectedItem="{Binding TasksVideoExtension, Mode=TwoWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksAudioExtension">
<ComboBox ItemsSource="{ct:EnumValues Type=m:AudioExtension}"
SelectedItem="{Binding TasksAudioExtension, Mode=TwoWay}"/>
</ctc:SettingsCard>
</ctc:SettingsExpander.Items>
</ctc:SettingsExpander>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksFilenameTemplate">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewTasksFilenameTemplate}"/>
</ctc:SettingsCard.HeaderIcon>
<TextBox Text="{Binding TasksFilenameTemplate, Mode=TwoWay}"
ToolTipService.ToolTip="{Binding TasksFilenameTemplateTooltip}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksMeteredConnectionWarning">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewTasksMeteredConnectionWarning}"/>
</ctc:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{Binding TasksMeteredConnectionWarning, Mode=TwoWay}"/>
</ctc:SettingsCard>
<ctc:SettingsExpander x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksSaveLastOutputDirectory">
<ctc:SettingsExpander.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewTasksOutputDirectory}"/>
</ctc:SettingsExpander.HeaderIcon>
<ToggleSwitch IsOn="{Binding TasksSaveLastOutputDirectory, Mode=TwoWay}"/>
<ctc:SettingsExpander.Items>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksDefaultOutputDirectory"
IsEnabled="{Binding TasksSaveLastOutputDirectory, Converter={StaticResource BoolNegationConverter}}"
Description="{Binding TasksDefaultOutputDirectory}">
<Button x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksDefaultOutputDirectoryButton"
Command="{Binding BrowseTasksDefaultOutputDirectoryCommand}"/>
</ctc:SettingsCard>
</ctc:SettingsExpander.Items>
</ctc:SettingsExpander>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TasksReplaceOutputFile">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewTasksReplaceOutputFile}"/>
</ctc:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{Binding TasksReplaceOutputFile, Mode=TwoWay}"/>
</ctc:SettingsCard>
</StackPanel>
<!-- Processing -->
<StackPanel Spacing="5">
<TextBlock x:Uid="/VDownload.Core.Strings/SettingsViewResources/ProcessingHeader"
FontWeight="Bold"
FontSize="15"/>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/ProcessingFFmpegLocation"
Description="{Binding ProcessingFFmpegLocation}">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewProcessingFFmpegLocation}"/>
</ctc:SettingsCard.HeaderIcon>
<Button x:Uid="/VDownload.Core.Strings/SettingsViewResources/ProcessingFFmpegLocationButton"
Command="{Binding BrowseProcessingFFmpegLocationCommand}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/ProcessingUseMultithreading">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewProcessingUseMultithreading}"/>
</ctc:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{Binding ProcessingUseMultithreading, Mode=TwoWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/ProcessingUseHardwareAcceleration">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewProcessingUseHardwareAcceleration}"/>
</ctc:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{Binding ProcessingUseHardwareAcceleration, Mode=TwoWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/ProcessingSpeed">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewProcessingSpeed}"/>
</ctc:SettingsCard.HeaderIcon>
<ComboBox ItemsSource="{ct:EnumValues Type=m:ProcessingSpeed}"
SelectedItem="{Binding ProcessingSpeed, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<ctuc:SwitchPresenter Value="{Binding Converter={StaticResource ObjectToStringConverter}}">
<ctuc:Case Value="VerySlow">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/ProcessingSpeedVerySlow"/>
</ctuc:Case>
<ctuc:Case Value="Slower">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/ProcessingSpeedSlower"/>
</ctuc:Case>
<ctuc:Case Value="Slow">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/ProcessingSpeedSlow"/>
</ctuc:Case>
<ctuc:Case Value="Medium">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/ProcessingSpeedMedium"/>
</ctuc:Case>
<ctuc:Case Value="Fast">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/ProcessingSpeedFast"/>
</ctuc:Case>
<ctuc:Case Value="Faster">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/ProcessingSpeedFaster"/>
</ctuc:Case>
<ctuc:Case Value="VeryFast">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/ProcessingSpeedVeryFast"/>
</ctuc:Case>
<ctuc:Case Value="SuperFast">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/ProcessingSpeedSuperFast"/>
</ctuc:Case>
<ctuc:Case Value="UltraFast">
<TextBlock x:Uid="/VDownload.Core.Strings/CommonResources/ProcessingSpeedUltraFast"/>
</ctuc:Case>
</ctuc:SwitchPresenter>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ctc:SettingsCard>
</StackPanel>
<!-- Notifications -->
<StackPanel Spacing="5">
<TextBlock x:Uid="/VDownload.Core.Strings/SettingsViewResources/NotificationsHeader"
FontWeight="Bold"
FontSize="15"/>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/NotificationsOnSuccessful">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewNotificationOnSuccessful}"/>
</ctc:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{Binding NotificationsOnSuccessful, Mode=TwoWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/NotificationsOnUnsuccessful">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewNotificationOnUnsuccessful}"/>
</ctc:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{Binding NotificationsOnUnsuccessful, Mode=TwoWay}"/>
</ctc:SettingsCard>
</StackPanel>
<!-- Temporary files -->
<StackPanel Spacing="5">
<TextBlock x:Uid="/VDownload.Core.Strings/SettingsViewResources/TempHeader"
FontWeight="Bold"
FontSize="15"/>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TempDirectory"
Description="{Binding TempDirectory}">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewTempDirectory}"/>
</ctc:SettingsCard.HeaderIcon>
<Button x:Uid="/VDownload.Core.Strings/SettingsViewResources/TempDirectoryButton"
Command="{Binding BrowseTempDirectoryCommand}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TempDeleteOnFail">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{ThemeResource ImageSettingsViewTempDeleteOnFail}"/>
</ctc:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{Binding TempDeleteOnFail, Mode=TwoWay}"/>
</ctc:SettingsCard>
</StackPanel>
<!-- Twitch -->
<StackPanel Spacing="5">
<TextBlock x:Uid="/VDownload.Core.Strings/SettingsViewResources/TwitchHeader"
FontWeight="Bold"
FontSize="15"/>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TwitchVodPassiveTrimming">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{StaticResource ImageSourcesTwitch}"/>
</ctc:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{Binding TwitchVodPassiveTrimming, Mode=TwoWay}"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TwitchVodParallelDownloads">
<ctc:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{StaticResource ImageSourcesTwitch}"/>
</ctc:SettingsCard.HeaderIcon>
<NumberBox Value="{Binding TwitchVodParallelDownloads, Mode=TwoWay}"
Minimum="1"
SmallChange="1"
LargeChange="10"
SpinButtonPlacementMode="Compact"/>
</ctc:SettingsCard>
<ctc:SettingsExpander x:Uid="/VDownload.Core.Strings/SettingsViewResources/TwitchVodChunkDownloadingErrorRetry">
<ctc:SettingsExpander.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False"
UriSource="{StaticResource ImageSourcesTwitch}"/>
</ctc:SettingsExpander.HeaderIcon>
<ToggleSwitch IsOn="{Binding TwitchVodChunkDownloadingErrorRetry, Mode=TwoWay}"/>
<ctc:SettingsExpander.Items>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TwitchVodChunkDownloadingErrorRetryCount"
IsEnabled="{Binding TwitchVodChunkDownloadingErrorRetry}">
<NumberBox Value="{Binding TwitchVodChunkDownloadingErrorRetryCount, Mode=TwoWay}"
Minimum="1"
SmallChange="1"
LargeChange="10"
SpinButtonPlacementMode="Compact"/>
</ctc:SettingsCard>
<ctc:SettingsCard x:Uid="/VDownload.Core.Strings/SettingsViewResources/TwitchVodChunkDownloadingErrorRetryDelay"
IsEnabled="{Binding TwitchVodChunkDownloadingErrorRetry}">
<NumberBox Value="{Binding TwitchVodChunkDownloadingErrorRetryDelay, Mode=TwoWay}"
Minimum="0"
SmallChange="1"
LargeChange="10"
SpinButtonPlacementMode="Compact"/>
</ctc:SettingsCard>
</ctc:SettingsExpander.Items>
</ctc:SettingsExpander>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Uid="/VDownload.Core.Strings/SettingsViewResources/RestoreToDefaultButton"
Command="{Binding RestoreToDefaultCommand}"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,18 @@
using Microsoft.UI.Xaml.Controls;
using VDownload.Core.ViewModels.Settings;
namespace VDownload.Core.Views.Settings
{
public sealed partial class SettingsView : Page
{
#region CONSTRUCTORS
public SettingsView(SettingsViewModel viewModel)
{
this.InitializeComponent();
this.DataContext = viewModel;
}
#endregion
}
}

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="VDownload.Core.Views.Subscriptions.SubscriptionsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VDownload.Core.Views.Subscriptions"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ctuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:ctc="using:CommunityToolkit.WinUI.Controls"
xmlns:ct="using:CommunityToolkit.WinUI"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
mc:Ignorable="d"
Background="{ThemeResource ViewBackgroundColor}"
x:Name="Root">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="Loaded">
<ic:InvokeCommandAction Command="{Binding NavigationCommand}"/>
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Grid Padding="20"
RowSpacing="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock x:Uid="/VDownload.Core.Strings/SubscriptionsViewResources/Header"
Grid.Row="0"
FontSize="28"
FontWeight="SemiBold"/>
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding Playlists}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ctuc:StaggeredPanel ColumnSpacing="10"
DesiredColumnWidth="120"
RowSpacing="10"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid RowSpacing="10"
Padding="10"
Background="{ThemeResource PanelBackgroundColor}"
CornerRadius="10">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Image Grid.Row="0">
<i:Interaction.Behaviors>
<ic:DataTriggerBehavior Binding="{Binding Source, Converter={StaticResource ObjectToStringConverter}}"
Value="Twitch">
<ic:ChangePropertyAction PropertyName="Source"
Value="{StaticResource ImageSourcesTwitch}"/>
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors>
</Image>
<TextBlock Grid.Row="1"
Text="{Binding Name}"
HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis"/>
<HyperlinkButton x:Uid="/VDownload.Core.Strings/SubscriptionsViewResources/RemovePlaylistButton"
Grid.Row="2"
Content="Remove"
HorizontalAlignment="Center"
Command="{Binding DataContext.RemovePlaylistCommand, ElementName=Root}"
CommandParameter="{Binding}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<ctuc:SwitchPresenter Grid.Row="2"
Value="{Binding Error, Converter={StaticResource IsNotNullConverter}}">
<ctuc:Case Value="False">
<Grid Background="{ThemeResource PanelBackgroundColor}"
Padding="10"
ColumnSpacing="10"
CornerRadius="10">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Uid="/VDownload.Core.Strings/SubscriptionsViewResources/PlaylistUrlTextBox"
Grid.Column="0"
Text="{Binding Url, Mode=TwoWay}"
IsEnabled="{Binding Loading, Converter={StaticResource BoolNegationConverter}}"/>
<ctuc:SwitchPresenter Grid.Column="1"
Value="{Binding Loading, Converter={StaticResource ObjectToStringConverter}}">
<ctuc:Case Value="True">
<ProgressRing IsIndeterminate="True"
Width="20"
Height="20"/>
</ctuc:Case>
<ctuc:Case Value="False">
<Button x:Uid="/VDownload.Core.Strings/SubscriptionsViewResources/PlaylistSearchButton"
Command="{Binding AddCommand}"/>
</ctuc:Case>
</ctuc:SwitchPresenter>
</Grid>
</ctuc:Case>
<ctuc:Case Value="True">
<InfoBar x:Uid="/VDownload.Core.Strings/SubscriptionsViewResources/ErrorInfoBar"
Severity="Error"
IsOpen="{Binding IsErrorOpened, Mode=TwoWay}"
Message="{Binding Error}"
CloseButtonCommand="{Binding CloseErrorCommand}"/>
</ctuc:Case>
</ctuc:SwitchPresenter>
</Grid>
</Page>

View File

@@ -0,0 +1,32 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using VDownload.Core.ViewModels.About;
using VDownload.Core.ViewModels.Subscriptions;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace VDownload.Core.Views.Subscriptions
{
public sealed partial class SubscriptionsView : Page
{
#region CONSTRUCTORS
public SubscriptionsView(SubscriptionsViewModel viewModel)
{
this.InitializeComponent();
this.DataContext = viewModel;
}
#endregion
}
}

View File

@@ -0,0 +1,97 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>VDownload.Core.Views</RootNamespace>
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<UseRidGraph>true</UseRidGraph>
<EnableCoreMrtTooling Condition=" '$(BuildingInsideVisualStudio)' != 'true' ">false</EnableCoreMrtTooling>
</PropertyGroup>
<ItemGroup>
<None Remove="About\AboutView.xaml" />
<None Remove="Home\HomeVideoCollectionView.xaml" />
<None Remove="Subscriptions\SubscriptionsView.xaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.WinUI" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.0.240109" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.0.240109" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.0.240109" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240227000" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageReference Include="SimpleToolkit.UI.WinUI.Behaviors" Version="1.7.4" />
<PackageReference Include="SimpleToolkit.UI.WinUI.Controls" Version="1.7.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\VDownload.Core.Strings\VDownload.Core.Strings.csproj" />
<ProjectReference Include="..\VDownload.Core.ViewModels\VDownload.Core.ViewModels.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="About\AboutView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<None Update="Authentication\AuthenticationView.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
<None Update="BaseWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
<None Update="Home\HomeDownloadsView.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
<Page Update="Subscriptions\SubscriptionsView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Home\HomeVideoCollectionView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<None Update="Home\HomeVideoView.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
<None Update="Settings\SettingsView.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
</ItemGroup>
<ItemGroup>
<Page Update="Home\HomeVideoView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="BaseWindow.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Home\HomeView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Home\HomeDownloadsView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Authentication\AuthenticationView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Settings\SettingsView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More