Spot
Spot 是一款地理标记的视频分享应用,这意味着在 Spot 中录制的每个视频都会保存在一个位置。任何人都可以通过地图滚动浏览并找到在世界任何地方录制的视频。
发现 通过 Spot,您可以查看世界任何地方正在发生的事情。Spot 中的所有视频都带有地理标签,这意味着它们标记了录制它们的地点。这意味着如果您想了解纽约现在正在发生什么,只需在应用中前往纽约并查看人们发布的帖子即可。对于洛杉矶、巴黎、东京或世界任何其他地方也是如此!
创建 您可以录制 Spot 视频,让其他人知道您所在位置正在发生的事情。借助我们直观的视频录制器,您可以轻松捕捉精彩瞬间。
享受 您如何使用 Spot 取决于您!您或许可以在不亲身前往的情况下查看当地的节日,发现隐藏的当地美食餐厅,或者虚拟访问您最喜欢的城市。当您游览精彩的地方时,别忘了在 Spot 中保存您的记忆,以便世界了解那个地方!
Schema
create table if not exists public.users (
id uuid references auth.users on delete cascade not null primary key,
name varchar(18) not null unique,
description varchar(320) not null,
image_url text,
constraint username_validation check (char_length(name) >= 1)
);
comment on table public.users is 'Holds all of users profile information';
alter table public.users enable row level security;
create policy "Public profiles are viewable by everyone." on public.users for select using (true);
create policy "Can insert user" on public.users for insert with check (auth.uid() = id);
create policy "Can update user" on public.users for update using (auth.uid() = id) with check (auth.uid() = id);
create policy "Can delete user" on public.users for delete using (auth.uid() = id);
create table if not exists public.videos (
id uuid not null primary key DEFAULT uuid_generate_v4 (),
user_id uuid references public.users on delete cascade not null,
created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
url text not null,
image_url text not null,
thumbnail_url text not null,
gif_url text not null,
description varchar(320) not null,
location geography(POINT) not null
);
comment on table public.videos is 'Holds all the video videos.';
alter table public.videos enable row level security;
create policy "Videos are viewable by everyone. " on public.videos for select using (true);
create policy "Can insert videos" on public.videos for insert with check (auth.uid() = user_id);
create policy "Can update videos" on public.videos for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
create policy "Can delete videos" on public.videos for delete using (auth.uid() = user_id);
create table if not exists public.comments (
id uuid not null primary key DEFAULT uuid_generate_v4 (),
video_id uuid references public.videos on delete cascade not null,
user_id uuid references public.users on delete cascade not null,
created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
text varchar(320) not null,
constraint comment_length check (char_length(text) >= 1)
);
comment on table public.comments is 'Holds all of the comments created by the users.';
alter table public.comments enable row level security;
create policy "Comments are viewable by everyone. " on public.comments for select using (true);
create policy "Can insert comments" on public.comments for insert with check (auth.uid() = user_id);
create policy "Can update comments" on public.comments for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
create policy "Can delete comments" on public.comments for delete using (auth.uid() = user_id);
create table if not exists public.likes (
video_id uuid references public.videos on delete cascade not null,
user_id uuid references public.users on delete cascade not null,
created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
PRIMARY KEY (video_id, user_id)
);
comment on table public.likes is 'Holds all of the like data created by thee users.';
alter table public.likes enable row level security;
create policy "Likes are viewable by everyone. " on public.likes for select using (true);
create policy "Users can insert their own likes." on public.likes for insert with check (auth.uid() = user_id);
create policy "Users can delete own likes." on public.likes for delete using (auth.uid() = user_id);
create table if not exists public.follow (
following_user_id uuid references public.users on delete cascade not null,
followed_user_id uuid references public.users on delete cascade not null,
followed_at timestamp with time zone default timezone('utc' :: text, now()) not null,
primary key (following_user_id, followed_user_id)
);
comment on table public.follow is 'Creates follow follower relationships.';
alter table public.follow enable row level security;
create policy "Follows are viewable by everyone. " on public.follow for select using (true);
create policy "Users can follow anyone" on public.follow for insert with check (auth.uid() = following_user_id);
create policy "Users can unfollow their follows and ssers can remove their followers" on public.follow for delete using (auth.uid() = following_user_id or auth.uid() = followed_user_id);
create table if not exists public.blocks (
user_id uuid references public.users on delete cascade not null,
blocked_user_id uuid references public.users on delete cascade not null,
created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
primary key (user_id, blocked_user_id),
constraint username_validation check (user_id != blocked_user_id)
);
comment on table public.blocks is 'Holds information of who is blocking who.';
alter table public.blocks enable row level security;
create policy "Users can view who they are blocking." on public.blocks for select using (auth.uid() = user_id);
create policy "Users can block anyone by themselves. " on public.blocks for insert with check (auth.uid() = user_id);
create table if not exists public.reports (
id uuid not null primary key DEFAULT uuid_generate_v4 (),
user_id uuid references public.users on delete cascade not null,
video_id uuid references public.videos on delete cascade not null,
reason text not null,
created_at timestamp with time zone default timezone('utc' :: text, now()) not null
);
comment on table public.reports is 'Who reported which video for what reason.';
alter table public.reports enable row level security;
create policy "Users can view their own reports." on public.reports for select using (auth.uid() = user_id);
create policy "Users can report a video." on public.reports for insert with check (auth.uid() = user_id);
create or replace view video_comments
as
select
comments.id,
comments.text,
comments.created_at,
comments.video_id,
users.id as user_id,
users.name as user_name,
users.description as user_description,
users.image_url as user_image_url
from comments
join users on comments.user_id = users.id;
create or replace function nearby_videos(location text, user_id uuid)
returns table(id uuid, url text, image_url text, thumbnail_url text, gif_url text, location text, created_at timestamptz, description text, user_id uuid, user_name text, user_description text, user_image_url text)
as
$func$
select
videos.id,
videos.url,
videos.image_url,
videos.thumbnail_url,
videos.gif_url,
st_astext(videos.location) as location,
videos.created_at,
videos.description,
users.id as user_id,
users.name as user_name,
users.description as user_description,
users.image_url as user_image_url
from videos
join users on videos.user_id = users.id
where users.id not in (select blocked_user_id from blocks where user_id = user_id)
order by location <-> st_geogfromtext($1);
$func$
language sql;
create or replace function videos_in_bouding_box(min_lng decimal, min_lat decimal, max_lng decimal, max_lat decimal, user_id uuid)
returns table(id uuid, url text, image_url text, thumbnail_url text, gif_url text, location text, created_at timestamptz, description text, user_id uuid, user_name text, user_description text, user_image_url text)
as
$func$
select
videos.id,
videos.url,
videos.image_url,
videos.thumbnail_url,
videos.gif_url,
st_astext(videos.location) as location,
videos.created_at,
videos.description,
users.id as user_id,
users.name as user_name,
users.description as user_description,
users.image_url as user_image_url
from videos
join users on videos.user_id = users.id
where users.id not in (select blocked_user_id from blocks where user_id = user_id)
and location && ST_SetSRID(ST_MakeBox2D(ST_Point(min_lng, min_lat), ST_Point(max_lng, max_lat)),4326);
$func$
language sql;
create or replace function get_video_detail(video_id uuid, user_id uuid)
returns table(id uuid, url text, image_url text, thumbnail_url text, gif_url text, created_at timestamptz, description text, user_id uuid, user_name text, user_description text, user_image_url text, location text, like_count int, comment_count int, have_liked int)
as
$func$
select
videos.id,
videos.url,
videos.image_url,
videos.thumbnail_url,
videos.gif_url,
videos.created_at,
videos.description,
users.id as user_id,
users.name as user_name,
users.description as user_description,
users.image_url as user_image_url,
st_astext(videos.location) as location,
(select count(*) from likes where video_id = videos.id)::int as like_count,
(select count(*) from comments where video_id = videos.id)::int as comment_count,
(select count(*) from likes where video_id = videos.id and videos.user_id = $2)::int as have_liked
from videos
join users on videos.user_id = users.id
where videos.id = $1;
$func$
language sql;
create or replace view notifications
as
select
'like' as type,
videos.user_id as receiver_user_id,
null as comment_text,
videos.id as video_id,
videos.thumbnail_url as video_thumbnail_url,
likes.user_id as action_user_id,
users.name as action_user_name,
users.image_url as action_user_image_url,
likes.created_at
from likes
join users on likes.user_id = users.id
join videos on videos.id = likes.video_id
union all
select
'comment' as type,
videos.user_id as receiver_user_id,
comments.text as comment_text,
videos.id as video_id,
videos.thumbnail_url as video_thumbnail_url,
comments.user_id as action_user_id,
users.name as action_user_name,
users.image_url as action_user_image_url,
comments.created_at
from comments
join users on comments.user_id = users.id
join videos on videos.id = comments.video_id
union all
select
'follow' as type,
follow.followed_user_id as receiver_user_id,
null as commennt_text,
null as video_id,
null as video_thumbnail_url,
follow.following_user_id as action_user_id,
users.name as action_user_name,
users.image_url as action_user_image_url,
follow.followed_at as created_at
from follow
join users on follow.following_user_id = users.id
order by created_at desc;
-- Configure storage
insert into storage.buckets (id, name) values ('videos', 'videos');
insert into storage.buckets (id, name) values ('profiles', 'profiles');
create policy "Videos buckets are public" on storage.objects for select using (bucket_id = 'videos');
create policy "Profiles buckets are public" on storage.objects for select using (bucket_id = 'profiles');
create policy "uid has to be the first element in path_tokens" on storage.objects for insert with check (auth.uid()::text = path_tokens[1] and array_length(path_tokens, 1) = 2);
-- Needed to use extensions from the app
grant usage on schema extensions to anon;
grant usage on schema extensions to authenticated;
入门?
此项目包含 3 种风味
- 开发
- 测试
- 生产
要运行所需的风味,可以使用 VSCode/Android Studio 中的启动配置,或使用以下命令
# Development
$ flutter run --flavor development --target lib/main_development.dart --dart-define=SUPABASE_URL=[Supabase Url] --dart-define=SUPABASE_ANNON_KEY=[Supabase annon key]
# Staging
$ flutter run --flavor staging --target lib/main_staging.dart
# Production
$ flutter run --flavor production --target lib/main_production.dart --dart-define=SUPABASE_URL=[Supabase Url] --dart-define=SUPABASE_ANNON_KEY=[Supabase annon key]
*Spot 适用于 iOS 和 Android。
正在运行测试?
要运行所有单元和 widget 测试,请使用以下命令
$ flutter test --coverage --test-randomize-ordering-seed random
要查看生成的覆盖率报告,您可以使用 lcov。
# Generate Coverage Report
$ genhtml coverage/lcov.info -o coverage/
# Open Coverage Report
$ open coverage/index.html
正在处理翻译?
该项目依赖于 [flutter_localizations][flutter_localizations_link] 并遵循 [Flutter 官方国际化指南][internationalization_link]。
添加字符串
- 要添加新的本地化字符串,请打开位于
lib/l10n/arb/app_en.arb的app_en.arb文件。
{
"@@locale": "en",
"counterAppBarTitle": "Counter",
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
}
}
- 然后添加新的键/值和描述
{
"@@locale": "en",
"counterAppBarTitle": "Counter",
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
},
"helloWorld": "Hello World",
"@helloWorld": {
"description": "Hello World Text"
}
}
- 使用新字符串
import 'package:spot/l10n/l10n.dart';
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Text(l10n.helloWorld);
}
添加支持的区域设置
更新位于 ios/Runner/Info.plist 的 Info.plist 中的 CFBundleLocalizations 数组,以包含新的区域设置。
...
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>es</string>
</array>
...
添加翻译
- 对于每个支持的区域设置,请在
lib/l10n/arb中添加一个新的 ARB 文件。
├── l10n
│ ├── arb
│ │ ├── app_en.arb
│ │ └── app_es.arb
- 将翻译后的字符串添加到每个
.arb文件中
app_en.arb
{
"@@locale": "en",
"counterAppBarTitle": "Counter",
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
}
}
app_es.arb
{
"@@locale": "es",
"counterAppBarTitle": "Contador",
"@counterAppBarTitle": {
"description": "Texto mostrado en la AppBar de la página del contador"
}
}