M1 공유 타겟(U1)+M2 저장 진행 알림: ACTION_SEND 인텐트 필터+ShareTarget/SharePayload(RED 7건)+루트 타이머 큐 전송(am start 인텐트 E2E로 서버 큐 반영 실증), 오프라인 저장 Android ProgressBar 노티(5% 스로틀)→폰 저장 완료 전환(34MB E2E, Report(1) 되덮어쓰기 방지). 154/154 GREEN

This commit is contained in:
Yun Chan 2026-08-15 18:45:51 +09:00
parent 69094595a5
commit 04ff0ff93d
7 changed files with 166 additions and 6 deletions

View file

@ -163,6 +163,21 @@ public sealed class QueuePage : ContentPage
_summary.Text = err != null ? $"❌ {err}" : items.Count == 0 ? "대기 중인 항목이 없습니다" : $"{items.Count}건";
}
/// <summary>U1: 공유받은 링크 처리 결과 표시(루트 타이머가 호출).</summary>
public void ShowSharedResult(string message) => _summary.Text = message;
/// <summary>U1: 공유 타겟으로 받은 텍스트에서 URL 을 추출해 PC 대기열에 추가.</summary>
private async Task HandleSharedUrlAsync()
{
var shared = ShareTarget.Consume();
if (shared == null || !ApiClient.Instance.IsConfigured) return;
var url = SharePayload.ExtractUrl(shared);
if (url == null) { _summary.Text = "🔗 공유받은 텍스트에 URL 이 없습니다"; return; }
_summary.Text = "🔗 공유 링크 전송 중…";
var ok = await ApiClient.Instance.AddAsync(url);
_summary.Text = ok ? $"🔗 공유 링크 전송됨: {url}" : "❌ 공유 링크 전송 실패";
}
/// <summary>U5: 완료 로컬 알림(Android 13+ 는 최초 1회 권한 요청).</summary>
private static async Task NotifyCompletionAsync(string title)
{
@ -234,14 +249,19 @@ public sealed class LibraryPage : ContentPage
if (item == null) return;
if (AppOfflineStore.Default.Exists(item.Id)) { button.Text = "✓ 저장됨"; return; }
button.Text = "0%";
var notifId = 5000 + Math.Abs(item.Id.GetHashCode() % 1000); // M2 진행 알림: 항목별 고정 id 로 갱신
try
{
var dest = AppOfflineStore.Default.PathFor(item.Id);
var progress = new Progress<double>(p => MainThread.BeginInvokeOnMainThread(
() => button.Text = p >= 1 ? "✓ 저장됨" : $"{(int)(p * 100)}%"));
var progress = new Progress<double>(p => MainThread.BeginInvokeOnMainThread(() =>
{
button.Text = p >= 1 ? "✓ 저장됨" : $"{(int)(p * 100)}%";
if (p < 1) ShowSaveProgress(notifId, item.FileName, p); // 100% 는 완료 노티가 대체(되덮어쓰기 방지)
}));
await ApiClient.Instance.DownloadFileAsync(item, dest, progress, CancellationToken.None);
AppOfflineStore.Default.Record(item.Id, item.FileName);
button.Text = "✓ 저장됨";
ShowSaveDone(notifId, item.FileName);
}
catch (Exception e)
{
@ -249,6 +269,53 @@ public sealed class LibraryPage : ContentPage
}
}
/// <summary>M2: 오프라인 저장 진행 알림(같은 id 갱신) — 5% 단위 스로틀.</summary>
private static int _lastNotifiedPercent = -1;
private static void ShowSaveProgress(int notifId, string fileName, double p)
{
var percent = (int)(p * 100);
if (p < 1 && percent / 5 == _lastNotifiedPercent) return; // 알림 스팸 방지
_lastNotifiedPercent = percent / 5;
try
{
Plugin.LocalNotification.LocalNotificationCenter.Current.Show(new Plugin.LocalNotification.NotificationRequest
{
NotificationId = notifId,
Title = "폰에 저장 중",
Description = $"{fileName} — {percent}%",
Android =
{
ChannelId = "vd_save",
Ongoing = true,
AutoCancel = false,
ProgressBar = new Plugin.LocalNotification.AndroidOption.AndroidProgressBar
{
Max = 100,
Progress = percent,
IsIndeterminate = false,
},
},
});
}
catch { }
}
private static void ShowSaveDone(int notifId, string fileName)
{
try
{
Plugin.LocalNotification.LocalNotificationCenter.Current.Show(new Plugin.LocalNotification.NotificationRequest
{
NotificationId = notifId,
Title = "폰 저장 완료",
Description = fileName,
Android = { ChannelId = "vd_save", Ongoing = false, AutoCancel = true },
});
}
catch { }
}
private async void OpenPlayer(LibraryItem item)
{
// 탭 자식엔 NavigationPage 가 없어 PushAsync 불가 → 루트에서 모달 push (닫기는 상단바 뒤로)

View file

@ -1,10 +1,31 @@
using Android.App;
using Android.App;
using Android.Content;
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)]
// M1 공유 타겟(U1): 다른 앱의 공유 시트에 "PC에서 받기" 로 등록
[IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "text/plain", Label = "PC에서 받기")]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
ReceiveShare(Intent);
}
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
ReceiveShare(intent);
}
private static void ReceiveShare(Intent? intent)
{
if (intent?.Action != Intent.ActionSend) return;
var text = intent.GetStringExtra(Intent.ExtraText);
ShareTarget.Deliver(text);
}
}

View file

@ -0,0 +1,16 @@
namespace VideoDownloader.Mobile;
/// <summary>M1 공유 타겟(U1): 플랫폼(안드로이드 인텐트)이 전달한 공유 텍스트 큐.</summary>
public static class ShareTarget
{
private static string? _pending;
public static void Deliver(string? sharedText)
{
if (string.IsNullOrWhiteSpace(sharedText)) return;
Volatile.Write(ref _pending, sharedText);
}
/// <summary>대기 중 공유 텍스트를 꺼낸다(없으면 null).</summary>
public static string? Consume() => Interlocked.Exchange(ref _pending, null);
}

View file

@ -1,15 +1,33 @@
namespace VideoDownloader.Mobile;
/// <summary>컴패니언 루트: 연결 / 큐 / 라이브러리 탭.</summary>
/// <summary>컴팠니언 루트: 큐 / 라이브러리 / 저장 / 연결 탭 + 공유 타겟 처리.</summary>
public sealed class TabbedRootPage : TabbedPage
{
public TabbedRootPage()
{
ApiClient.Instance.Restore();
Children.Add(new QueuePage());
var queuePage = new QueuePage();
Children.Add(queuePage);
Children.Add(new LibraryPage());
Children.Add(new DownloadsPage());
Children.Add(new ConnectPage());
Title = "VD 원격";
// M1 공유 타겟(U1): 어떤 탭에 있든/다른 앱에서 공유받으면 URL 을 PC 큐로 전송 후 큐 탭 표시
Device.StartTimer(TimeSpan.FromSeconds(2), () =>
{
_ = HandleShareAsync(queuePage);
return true;
});
}
private static async Task HandleShareAsync(QueuePage queuePage)
{
var shared = ShareTarget.Consume();
if (shared == null || !ApiClient.Instance.IsConfigured) return;
var url = SharePayload.ExtractUrl(shared);
if (url == null) { queuePage.ShowSharedResult("🔗 공유받은 텍스트에 URL 이 없습니다"); return; }
var ok = await ApiClient.Instance.AddAsync(url);
queuePage.ShowSharedResult(ok ? $"🔗 공유 링크 전송됨: {url}" : "❌ 공유 링크 전송 실패");
}
}