diff --git a/VideoDownloader.Mobile/ApiClient.cs b/VideoDownloader.Mobile/ApiClient.cs new file mode 100644 index 0000000..4138bc7 --- /dev/null +++ b/VideoDownloader.Mobile/ApiClient.cs @@ -0,0 +1,139 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; + +namespace VideoDownloader.Mobile; + +/// 데스크톱 원격 서버 API 클라이언트(M1). 토큰은 QR(#t=)/수동 입력에서 획득. +public sealed class ApiClient +{ + public static readonly ApiClient Instance = new(); + + private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(8) }; + + public string? BaseUrl { get; private set; } + public string? Token { get; private set; } + public string? LastError { get; private set; } + public bool IsConfigured => BaseUrl is { Length: > 0 } && Token is { Length: > 0 }; + + public event Action? ConnectionChanged; + + /// QR 문자열(http://ip:port/#t=TOKEN) 또는 수동 입력(host:port + token) 구성. + public bool Configure(string? qrOrUrl, string? manualToken = null) + { + try + { + if (!string.IsNullOrWhiteSpace(manualToken)) + { + var host = qrOrUrl!.Trim().TrimEnd('/'); + if (!host.StartsWith("http", StringComparison.OrdinalIgnoreCase)) host = "http://" + host; + BaseUrl = host; + Token = manualToken.Trim(); + } + else + { + var u = new Uri(qrOrUrl!.Trim()); + var frag = u.Fragment.TrimStart('#'); + var token = System.Web.HttpUtility.ParseQueryString(frag)["t"]; + if (string.IsNullOrEmpty(token)) return false; + BaseUrl = $"http://{u.Authority}"; + Token = token; + } + _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token); + Preferences.Set("vd_base", BaseUrl); + Preferences.Set("vd_token", Token); + ConnectionChanged?.Invoke(); + return true; + } + catch { return false; } + } + + public void Restore() + { + var b = Preferences.Get("vd_base", ""); + var t = Preferences.Get("vd_token", ""); + if (b.Length > 0 && t.Length > 0) + { + BaseUrl = b; + Token = t; + _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", t); + } + } + + public void Forget() + { + BaseUrl = null; Token = null; + Preferences.Remove("vd_base"); Preferences.Remove("vd_token"); + ConnectionChanged?.Invoke(); + } + + public async Task> GetQueueAsync() + { + try + { + LastError = null; + var r = await _http.GetAsync($"{BaseUrl}/api/queue"); + if (!r.IsSuccessStatusCode) { LastError = $"HTTP {(int)r.StatusCode}"; return new(); } + return await r.Content.ReadFromJsonAsync>() ?? new(); + } + catch (Exception e) { LastError = e.Message; return new(); } + } + + public async Task AddAsync(string url) + { + try + { + var r = await _http.PostAsJsonAsync($"{BaseUrl}/api/queue", new { url }); + return r.IsSuccessStatusCode; + } + catch { return false; } + } + + public Task ControlAsync(string id, string action) => + PostAsync($"/api/queue/{id}/{action}"); + + public async Task PairAsync() + { + try + { + var r = await _http.PostAsJsonAsync($"{BaseUrl}/api/pair", new { token = Token, device = "Android" }); + return r.IsSuccessStatusCode; + } + catch { return false; } + } + + public async Task> GetLibraryAsync() + { + try + { + var r = await _http.GetAsync($"{BaseUrl}/api/library"); + if (!r.IsSuccessStatusCode) return new(); + return await r.Content.ReadFromJsonAsync>() ?? new(); + } + catch { return new(); } + } + + public string StreamUrl(LibraryItem item) => $"{BaseUrl}/stream/{item.Id}?t={Token}"; + + private Task PostAsync(string path) + { + try { var t = _http.PostAsync($"{BaseUrl}{path}", null); return Continue(t); } + catch { return Task.FromResult(false); } + static async Task Continue(Task t) { try { return (await t).IsSuccessStatusCode; } catch { return false; } } + } + + private async Task GetAsync(string path) where T : class + { + try + { + var r = await _http.GetAsync($"{BaseUrl}{path}"); + return r.IsSuccessStatusCode ? await r.Content.ReadFromJsonAsync() : null; + } + catch { return null; } + } +} + +public sealed record QueueItem(string Id, string Title, string Url, string Status, double Percent); + +public sealed record LibraryItem(string Id, string FileName, long SizeBytes, DateTime Modified); diff --git a/VideoDownloader.Mobile/App.xaml b/VideoDownloader.Mobile/App.xaml new file mode 100644 index 0000000..6100cfa --- /dev/null +++ b/VideoDownloader.Mobile/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/VideoDownloader.Mobile/App.xaml.cs b/VideoDownloader.Mobile/App.xaml.cs new file mode 100644 index 0000000..26b5aa1 --- /dev/null +++ b/VideoDownloader.Mobile/App.xaml.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace VideoDownloader.Mobile; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + } + + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new TabbedRootPage()); + } +} \ No newline at end of file diff --git a/VideoDownloader.Mobile/MauiProgram.cs b/VideoDownloader.Mobile/MauiProgram.cs new file mode 100644 index 0000000..7861346 --- /dev/null +++ b/VideoDownloader.Mobile/MauiProgram.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Logging; + +using CommunityToolkit.Maui; +using ZXing.Net.Maui.Controls; + +namespace VideoDownloader.Mobile; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .UseBarcodeReader() + .UseMauiCommunityToolkitMediaElement() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + +#if DEBUG + builder.Logging.AddDebug(); +#endif + + return builder.Build(); + } +} diff --git a/VideoDownloader.Mobile/Pages.cs b/VideoDownloader.Mobile/Pages.cs new file mode 100644 index 0000000..03d968c --- /dev/null +++ b/VideoDownloader.Mobile/Pages.cs @@ -0,0 +1,213 @@ +using Microsoft.Maui.Controls; +using System.Net.Http; + +namespace VideoDownloader.Mobile; + +/// M1 연결 페이지: QR 스캔(ZXing) + 수동 입력 폴백. +public sealed class ConnectPage : ContentPage +{ + private readonly Entry _manual = new() { Placeholder = "PC 주소 (예: 192.168.0.223:52100)", Keyboard = Keyboard.Url }; + private readonly Entry _manualToken = new() { Placeholder = "토큰 (QR 못 쓸 때)", IsPassword = true }; + private readonly Label _status = new() { Text = "PC 앱에서 '기기 연결'을 누르고 QR 을 스캔하세요", HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 12) }; + private readonly ZXing.Net.Maui.Controls.CameraBarcodeReaderView _scanner; + + public ConnectPage() + { + Title = "연결"; + + _scanner = new ZXing.Net.Maui.Controls.CameraBarcodeReaderView + { + HorizontalOptions = LayoutOptions.Fill, + HeightRequest = 320, + Options = new ZXing.Net.Maui.BarcodeReaderOptions + { + Formats = ZXing.Net.Maui.BarcodeFormats.TwoDimensional, + AutoRotate = true, + }, + }; + _scanner.BarcodesDetected += (s, e) => + { + var text = e.Results?.FirstOrDefault()?.Value; + if (string.IsNullOrEmpty(text)) return; + MainThread.BeginInvokeOnMainThread(() => + { + if (ApiClient.Instance.Configure(text)) + { + _status.Text = "✅ 연결됨"; + _scanner.IsDetecting = false; + } + }); + }; + + var connectBtn = new Button { Text = "수동 연결" }; + connectBtn.Clicked += (_, _) => + { + if (ApiClient.Instance.Configure(_manual.Text, _manualToken.Text)) _status.Text = "✅ 연결됨"; + else _status.Text = "❌ 연결 실패 — 주소/토큰 확인"; + }; + var forgetBtn = new Button { Text = "연결 해제", BackgroundColor = Colors.Gray }; + forgetBtn.Clicked += (_, _) => { ApiClient.Instance.Forget(); _status.Text = "해제됨"; }; + + Content = new ScrollView + { + Content = new VerticalStackLayout + { + Padding = 20, Spacing = 10, + Children = { _status, _scanner, _manual, _manualToken, connectBtn, forgetBtn } + } + }; + } +} + +/// M1 큐 페이지: 목록(2초 폴링) + URL 추가 + 제어. +public sealed class QueuePage : ContentPage +{ + private readonly Entry _url = new() { Placeholder = "YouTube·영상 URL 붙여넣기", Keyboard = Keyboard.Url }; + private readonly Label _summary = new(); + private readonly CollectionView _list = new(); + private readonly IDispatcherTimer _timer; + + public QueuePage() + { + Title = "다운로드 큐"; + var addBtn = new Button { Text = "PC에서 받기" }; + addBtn.Clicked += async (_, _) => + { + if (string.IsNullOrWhiteSpace(_url.Text)) return; + _summary.Text = "전송 중…"; + var ok = await ApiClient.Instance.AddAsync(_url.Text.Trim()); + _summary.Text = ok ? "✅ PC 대기열에 추가됨" : "❌ 실패 (연결 확인)"; + if (ok) _url.Text = ""; + await Refresh(); + }; + + _list.ItemTemplate = new DataTemplate(() => + { + var title = new Label { FontSize = 14, FontAttributes = FontAttributes.Bold, LineBreakMode = LineBreakMode.TailTruncation }; + title.SetBinding(Label.TextProperty, "Title"); + var status = new Label { FontSize = 12, TextColor = Colors.Gray }; + status.SetBinding(Label.TextProperty, new Binding("Percent", stringFormat: "{0:0}%")); + var bar = new ProgressBar { HeightRequest = 6 }; + bar.SetBinding(ProgressBar.ProgressProperty, new Binding("Percent", converter: new PercentConverter())); + var state = new Label { FontSize = 11, TextColor = Colors.Gray }; + state.SetBinding(Label.TextProperty, "Status"); + + var pause = new Button { Text = "⏸" }; + var resume = new Button { Text = "▶" }; + var cancel = new Button { Text = "✕", BackgroundColor = Colors.Gray }; + pause.Clicked += async (_, _) => { if (pause.BindingContext is QueueItem qi) { await ApiClient.Instance.ControlAsync(qi.Id, "pause"); await Refresh(); } }; + resume.Clicked += async (_, _) => { if (resume.BindingContext is QueueItem qi) { await ApiClient.Instance.ControlAsync(qi.Id, "resume"); await Refresh(); } }; + cancel.Clicked += async (_, _) => { if (cancel.BindingContext is QueueItem qi) { await ApiClient.Instance.ControlAsync(qi.Id, "cancel"); await Refresh(); } }; + + return new Frame + { + Margin = new Thickness(0, 4), Padding = 12, CornerRadius = 10, + Content = new VerticalStackLayout + { + Spacing = 6, + Children = { title, bar, new HorizontalStackLayout { Children = { state, new Label { Text = " " } } }, + new HorizontalStackLayout { Spacing = 8, Children = { pause, resume, cancel } } } + } + }; + }); + + _timer = Dispatcher.CreateTimer(); + _timer.Interval = TimeSpan.FromSeconds(2); + _timer.Tick += async (_, _) => await Refresh(); + Appearing += (_, _) => _timer.Start(); + Disappearing += (_, _) => _timer.Stop(); + + Content = new VerticalStackLayout + { + Padding = 16, Spacing = 10, + Children = { _url, addBtn, _summary, _list } + }; + } + + private async Task Refresh() + { + if (!ApiClient.Instance.IsConfigured) { _summary.Text = "⚠️ 먼저 연결하세요"; return; } + var items = await ApiClient.Instance.GetQueueAsync(); + _list.ItemsSource = items; + var err = ApiClient.Instance.LastError; + _summary.Text = err != null ? $"❌ {err}" : items.Count == 0 ? "대기 중인 항목이 없습니다" : $"{items.Count}건"; + } + + private sealed class PercentConverter : IValueConverter + { + public object Convert(object? value, Type t, object? p, System.Globalization.CultureInfo c) => Math.Clamp(value is double d ? d / 100.0 : 0, 0, 1); + public object ConvertBack(object? v, Type t, object? p, System.Globalization.CultureInfo c) => throw new NotSupportedException(); + } +} + +/// M2 라이브러리 페이지: 목록 → 탭하여 스트리밍 재생. +public sealed class LibraryPage : ContentPage +{ + private readonly CollectionView _list = new(); + + public LibraryPage() + { + Title = "라이브러리"; + _list.ItemTemplate = new DataTemplate(() => + { + var name = new Label { FontSize = 14, FontAttributes = FontAttributes.Bold, LineBreakMode = LineBreakMode.TailTruncation }; + name.SetBinding(Label.TextProperty, "FileName"); + var size = new Label { FontSize = 11, TextColor = Colors.Gray }; + size.SetBinding(Label.TextProperty, new Binding("SizeBytes", stringFormat: "{0:N0} bytes")); + var card = new Frame + { + Margin = new Thickness(0, 4), Padding = 12, CornerRadius = 10, + Content = new VerticalStackLayout { Children = { name, size } } + }; + // CollectionView.SelectionChanged 는 Android 에서 발화 불안정 → 템플릿 카드에 탭 제스처 + var tap = new TapGestureRecognizer(); + tap.Tapped += (_, _) => { if (card.BindingContext is LibraryItem item) OpenPlayer(item); }; + card.GestureRecognizers.Add(tap); + return card; + }); + _list.SelectionMode = SelectionMode.None; + + var refresh = new Button { Text = "새로고침" }; + refresh.Clicked += async (_, _) => await Reload(); + + Content = new VerticalStackLayout { Padding = 16, Children = { refresh, _list } }; + Appearing += async (_, _) => await Reload(); + } + + private async void OpenPlayer(LibraryItem item) + { + // 탭 자식엔 NavigationPage 가 없어 PushAsync 불가 → 루트에서 모달 push (닫기는 상단바 뒤로) + await Application.Current!.MainPage!.Navigation.PushModalAsync(new NavigationPage(new PlayerPage(item))); + } + + private async Task Reload() + { + if (!ApiClient.Instance.IsConfigured) { _list.ItemsSource = new[] { "먼저 연결하세요" }; return; } + _list.ItemsSource = await ApiClient.Instance.GetLibraryAsync(); + } +} + +/// M2 재생 페이지: Range 스트리밍 재생(MediaElement). +public sealed class PlayerPage : ContentPage +{ + private readonly CommunityToolkit.Maui.Views.MediaElement _media = new() + { + Aspect = Aspect.AspectFit, + ShouldAutoPlay = true, + ShouldShowPlaybackControls = true, + }; + + public PlayerPage(LibraryItem item) + { + Title = item.FileName; + _media.Source = CommunityToolkit.Maui.Views.MediaSource.FromUri(ApiClient.Instance.StreamUrl(item)); + Content = _media; + } + + protected override void OnDisappearing() + { + _media.Stop(); + _media.Handler?.DisconnectHandler(); // 재생 리소스 해제 + base.OnDisappearing(); + } +} diff --git a/VideoDownloader.Mobile/Platforms/Android/AndroidManifest.xml b/VideoDownloader.Mobile/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..5cdb4fd --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/VideoDownloader.Mobile/Platforms/Android/MainActivity.cs b/VideoDownloader.Mobile/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000..471765c --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/Android/MainActivity.cs @@ -0,0 +1,10 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; + +namespace VideoDownloader.Mobile; + +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity +{ +} diff --git a/VideoDownloader.Mobile/Platforms/Android/MainApplication.cs b/VideoDownloader.Mobile/Platforms/Android/MainApplication.cs new file mode 100644 index 0000000..22a2260 --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/Android/MainApplication.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Runtime; + +namespace VideoDownloader.Mobile; + +[Application] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/VideoDownloader.Mobile/Platforms/Android/Resources/values/colors.xml b/VideoDownloader.Mobile/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..c04d749 --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/VideoDownloader.Mobile/Platforms/MacCatalyst/AppDelegate.cs b/VideoDownloader.Mobile/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 0000000..5da0075 --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace VideoDownloader.Mobile; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/VideoDownloader.Mobile/Platforms/MacCatalyst/Entitlements.plist b/VideoDownloader.Mobile/Platforms/MacCatalyst/Entitlements.plist new file mode 100644 index 0000000..de4adc9 --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/MacCatalyst/Entitlements.plist @@ -0,0 +1,14 @@ + + + + + + + com.apple.security.app-sandbox + + + com.apple.security.network.client + + + + diff --git a/VideoDownloader.Mobile/Platforms/MacCatalyst/Info.plist b/VideoDownloader.Mobile/Platforms/MacCatalyst/Info.plist new file mode 100644 index 0000000..f2e0987 --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + UIDeviceFamily + + 2 + + LSApplicationCategoryType + public.app-category.lifestyle + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/VideoDownloader.Mobile/Platforms/MacCatalyst/Program.cs b/VideoDownloader.Mobile/Platforms/MacCatalyst/Program.cs new file mode 100644 index 0000000..3d9e6fd --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace VideoDownloader.Mobile; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/VideoDownloader.Mobile/Platforms/Windows/App.xaml b/VideoDownloader.Mobile/Platforms/Windows/App.xaml new file mode 100644 index 0000000..885b74e --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/Windows/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/VideoDownloader.Mobile/Platforms/Windows/App.xaml.cs b/VideoDownloader.Mobile/Platforms/Windows/App.xaml.cs new file mode 100644 index 0000000..f182b49 --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/Windows/App.xaml.cs @@ -0,0 +1,24 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace VideoDownloader.Mobile.WinUI; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} + diff --git a/VideoDownloader.Mobile/Platforms/Windows/Package.appxmanifest b/VideoDownloader.Mobile/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..54dd742 --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,46 @@ + + + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VideoDownloader.Mobile/Platforms/Windows/app.manifest b/VideoDownloader.Mobile/Platforms/Windows/app.manifest new file mode 100644 index 0000000..ceb516e --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/Windows/app.manifest @@ -0,0 +1,17 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + true + + + diff --git a/VideoDownloader.Mobile/Platforms/iOS/AppDelegate.cs b/VideoDownloader.Mobile/Platforms/iOS/AppDelegate.cs new file mode 100644 index 0000000..5da0075 --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace VideoDownloader.Mobile; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/VideoDownloader.Mobile/Platforms/iOS/Info.plist b/VideoDownloader.Mobile/Platforms/iOS/Info.plist new file mode 100644 index 0000000..0004a4f --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/VideoDownloader.Mobile/Platforms/iOS/Program.cs b/VideoDownloader.Mobile/Platforms/iOS/Program.cs new file mode 100644 index 0000000..3d9e6fd --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/iOS/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace VideoDownloader.Mobile; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/VideoDownloader.Mobile/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/VideoDownloader.Mobile/Platforms/iOS/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..24ab3b4 --- /dev/null +++ b/VideoDownloader.Mobile/Platforms/iOS/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,51 @@ + + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + + + + + + diff --git a/VideoDownloader.Mobile/Properties/launchSettings.json b/VideoDownloader.Mobile/Properties/launchSettings.json new file mode 100644 index 0000000..4f85793 --- /dev/null +++ b/VideoDownloader.Mobile/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "Project", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/VideoDownloader.Mobile/Resources/AppIcon/appicon.svg b/VideoDownloader.Mobile/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000..9d63b65 --- /dev/null +++ b/VideoDownloader.Mobile/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/VideoDownloader.Mobile/Resources/AppIcon/appiconfg.svg b/VideoDownloader.Mobile/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/VideoDownloader.Mobile/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/VideoDownloader.Mobile/Resources/Fonts/OpenSans-Regular.ttf b/VideoDownloader.Mobile/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..2cc82d2 Binary files /dev/null and b/VideoDownloader.Mobile/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/VideoDownloader.Mobile/Resources/Fonts/OpenSans-Semibold.ttf b/VideoDownloader.Mobile/Resources/Fonts/OpenSans-Semibold.ttf new file mode 100644 index 0000000..fcb5284 Binary files /dev/null and b/VideoDownloader.Mobile/Resources/Fonts/OpenSans-Semibold.ttf differ diff --git a/VideoDownloader.Mobile/Resources/Images/dotnet_bot.png b/VideoDownloader.Mobile/Resources/Images/dotnet_bot.png new file mode 100644 index 0000000..054167e Binary files /dev/null and b/VideoDownloader.Mobile/Resources/Images/dotnet_bot.png differ diff --git a/VideoDownloader.Mobile/Resources/Raw/AboutAssets.txt b/VideoDownloader.Mobile/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..89dc758 --- /dev/null +++ b/VideoDownloader.Mobile/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with your package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/VideoDownloader.Mobile/Resources/Splash/splash.svg b/VideoDownloader.Mobile/Resources/Splash/splash.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/VideoDownloader.Mobile/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/VideoDownloader.Mobile/Resources/Styles/Colors.xaml b/VideoDownloader.Mobile/Resources/Styles/Colors.xaml new file mode 100644 index 0000000..d57fcc6 --- /dev/null +++ b/VideoDownloader.Mobile/Resources/Styles/Colors.xaml @@ -0,0 +1,44 @@ + + + + + + #512BD4 + #ac99ea + #242424 + #DFD8F7 + #9880e5 + #2B0B98 + + White + Black + #D600AA + #190649 + #1f1f1f + + #E1E1E1 + #C8C8C8 + #ACACAC + #919191 + #6E6E6E + #404040 + #212121 + #141414 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoDownloader.Mobile/Resources/Styles/Styles.xaml b/VideoDownloader.Mobile/Resources/Styles/Styles.xaml new file mode 100644 index 0000000..5fef12a --- /dev/null +++ b/VideoDownloader.Mobile/Resources/Styles/Styles.xaml @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VideoDownloader.Mobile/TabbedRootPage.cs b/VideoDownloader.Mobile/TabbedRootPage.cs new file mode 100644 index 0000000..23b925d --- /dev/null +++ b/VideoDownloader.Mobile/TabbedRootPage.cs @@ -0,0 +1,14 @@ +namespace VideoDownloader.Mobile; + +/// 컴패니언 루트: 연결 / 큐 / 라이브러리 탭. +public sealed class TabbedRootPage : TabbedPage +{ + public TabbedRootPage() + { + ApiClient.Instance.Restore(); + Children.Add(new QueuePage()); + Children.Add(new LibraryPage()); + Children.Add(new ConnectPage()); + Title = "VD 원격"; + } +} diff --git a/VideoDownloader.Mobile/VideoDownloader.Mobile.csproj b/VideoDownloader.Mobile/VideoDownloader.Mobile.csproj new file mode 100644 index 0000000..09f5f09 --- /dev/null +++ b/VideoDownloader.Mobile/VideoDownloader.Mobile.csproj @@ -0,0 +1,76 @@ + + + + net10.0-android + + + + + + Exe + VideoDownloader.Mobile + true + true + enable + enable + + + SourceGen + + + VideoDownloader.Mobile + + + com.companyname.videodownloader.mobile + + + 1.0 + 1 + + + None + + 15.0 + 15.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +